Leya API Reference

A REST API over a corpus of primary Latin American legal sources — statutes, decrees, congressional initiatives, court rulings, regulations and official gazettes. It exists so an LLM can cite real law instead of recalling it: every result carries a stable document id, a source attribution, a link to the official document, and corpus-freshness metadata.

Get an API keyOpenAPI specllms-full.txt

Authentication

Every /v1 request needs a bearer token. Keys are self-serve at /api-keys, hashed at rest, and shown once at creation. leya_test_ keys hit the same corpus and exist so you can keep development traffic separable in your own logs.

curl -H "Authorization: Bearer $LEYA_API_KEY" \
  "https://api.leya.lawyer/v1/search?q=matrimonio+civil&country=GT"

Rate limits

Per key, sliding window. /v1/search allows 60 requests/minute — tighter than the rest because each search computes a query embedding. Other /v1 reads allow 300/minute.

Successful responses carry X-RateLimit-Limit and X-RateLimit-Window so a client can pace itself before being rejected. A 429 carries Retry-After in seconds, plus X-RateLimit-Reset. Wait, retry once, then report the failure — don't loop.

Credits

Calls are prepaid in credits. A new account gets 1,000 free credits on its first request — roughly 300 searches — so evaluating costs nothing. Credits are $0.01 each ($10 per 1,000).

EndpointCredits
GET /v1/search3
GET /v1/parcels/{ccc}2
GET /v1/parcels2
GET /v1/documents/{id}1
GET /v1/documents/{id}/citation1
GET /v1/cadastre/coverage1
GET /v1/me, /v1/credits, /v1/sources, /v1/filtersfree

Search costs most because it is the only endpoint that spends money per call, on a query embedding. Introspection and top-up are free on purpose: an account at zero must still be able to see why and buy more. Credits are charged before the handler runs and refunded automatically on a 5xx; charged responses carry X-Credits-Charged.

GET /v1/credits is the live source of truth — balance, price, the full cost table and recent ledger entries. Prefer it over the table above.

Running out

A call you cannot afford returns 402 with code insufficient_credits and an error.credits object giving balance, required, shortfall, top_up_usd and top_up_url. It is retryable: false: a rate limit clears by waiting, an empty balance never does, and an agent that backs off will loop forever.

Buying credits

POST /v1/credits/purchase?credits=N speaks the Machine Payments Protocol — the HTTP payment scheme in IETF draft draft-ryan-httpauth-payment, co-authored by Stripe and Tempo. Settlement is in stablecoins on Tempo. The whole exchange is one request cycle: call it, get a 402 with a WWW-Authenticate: Payment challenge naming the price, settle, retry with Authorization: Payment. Any MPP client does this transparently.

tempo request -X POST \
  "https://api.leya.lawyer/v1/credits/purchase?credits=1000" \
  -H "X-Leya-Api-Key: $LEYA_API_KEY"

Note the header: this route takes the key in X-Leya-Api-Key, because the payment scheme owns Authorization and two schemes cannot share it. Purchases are idempotent per settlement — replaying a credential does not buy twice.

Endpoints

Base URL https://api.leya.lawyer

GET/v1/search
ParamTypeNotes
qstring, requiredNatural-language or keyword query. Spanish works best.
countryISO-2GT, DO. Omit to search every covered jurisdiction.
source_idstringRestrict to one dataset. Values from /v1/sources.
document_typestringCorpus-defined, e.g. decreto, iniciativa, sentencia.
legal_areastringCorpus-defined, e.g. laboral, mercantil, fiscal.
as_ofYYYY-MM-DDExcludes documents known to be outside force then. Unknown-date rows are kept.
limitint 1–50Default 10. 51+ returns 422 rather than clamping.
cursorstringnext_cursor from the previous page. Opaque; only valid for the same q and filters.

Returns {query, results[], result_count, limit, truncated}. Results carry document_id, source_id, title, snippet (~500 chars), score, url, effective_from, effective_to, publication_date and source_last_indexed.

Pagination. When has_more is true, pass next_cursor back as cursor with q and every filter unchanged. The cursor is a keyset position in one query's score ordering, so it carries a fingerprint of the query that produced it — replaying it elsewhere is a 422, not a plausible wrong page. Only claim an exhaustive search once has_more is false.

Empty results mean no coverage. country, document_type and legal_area are validated against the corpus: an unknown value is a 422 listing what's accepted, so a zero-result page is genuine absence rather than a typo. source_id is not validated this way — take those from /v1/sources.

GET/v1/documents/{document_id}

Full text of one document. Pass a document_id from a search result verbatim. Returns body_markdown plus has_body and body_length.

has_body: false means the document is indexed but its text has not been extracted. That is not an empty document, and it is not a reason to supply the text from memory — report it as a gap. Bodies are unbounded; check body_length before loading a full code into a constrained context.

GET/v1/documents/{document_id}/citation

A citation string for inline use, plus the components it was built from.

{
  "formatted": "Decreto 1441, Congreso de la República (1961)",
  "style": "latam-standard",
  "document_type": "decreto",
  "number": "1441",
  "issuing_body": "Congreso de la República",
  "year": 1961
}

style: latam-standard is the full <Tipo> <Número>, <Órgano Emisor> (<Año>) form. style: title-fallback means the corpus lacks that metadata, so the string is title plus id and the citation is partial.

Use formatted verbatim in both cases. Completing a partial citation from memory produces a fabricated authority — the specific failure this API exists to prevent. A null component means unknown.

GET/v1/filters

The values country, document_type and legal_area actually accept, each with a document count. Read this before filtering — it's the only way to tell a typo'd filter from real no-coverage.

{
  "countries":      [{"value": "GT",      "document_count": 960}],
  "document_types": [{"value": "decreto", "document_count": 412}],
  "legal_areas":    [{"value": "laboral", "document_count": 87}]
}
GET/v1/sources

Registered datasets and the source_id values the search filter accepts.

GET/v1/me

Key introspection: the identity a key belongs to, its key_type, its rate_limits, and requests_last_30d. Read your budget from here rather than hard-coding the numbers above.

GET/api/corpus/status

No auth. Per-source and per-country document counts, date spans, last ingest time and status. This is the authoritative answer to “what does Leya cover?” — prefer it over any static list. Rendered for humans at /status.

GET/v1/parcels/{ccc}

One cadastral parcel from Guatemala’s RIC, by código catastral (e.g. 18-04-09-03971). Returns the parcel, a coverage block, and overlaps[] — the protected areas, sacred sites, archaeological sites, watersheds and forestry-incentive zones it intersects, each with an overlap_fraction so containment is distinguishable from a boundary graze.

This is not a property registry. The ccc is RIC’s identifier, not the Registro General de la Propiedad’s finca/folio/libro, and no mapping between them is published. There is no owner, title or encumbrance data here.

RIC has surveyed a minority of the country — nine departments publish zero parcels — so a 404 usually means unsurveyed, not nonexistent. The error carries a coverage object saying which.

GET/v1/cadastre/coverage

Per-department parcel counts: what the cadastral dataset does and does not contain. Read this before drawing any conclusion from an empty parcel result. See also /v1/parcels for listing and /v1/cadastre/layers for the overlay keys.

Errors

Every /v1 failure uses one envelope. Branch on code; never parse message. retryable tells you whether repeating the identical request could succeed.

{
  "error": {
    "code": "rate_limited",
    "message": "rate limit of 60 requests per 60s exceeded",
    "status": 429,
    "retryable": true,
    "docs": "https://leya.lawyer/docs/api#errors"
  }
}
codeHTTPRetryableMeaning
unauthenticated401noMissing, malformed, or revoked key.
insufficient_credits402noOut of credits. Waiting never clears it — top up or stop.
payment_required402noPurchase route only: step one of the MPP handshake, not a failure.
forbidden403noKey valid but not permitted for this resource.
not_found404noNo such document. Re-search; do not invent it.
parcel_not_found404noNo such parcel. Read `coverage`: usually unsurveyed, not nonexistent.
method_not_allowed405noWrong HTTP method for that path.
invalid_request422noBad parameter. `fields[]` names each one.
rate_limited429yesHonour `Retry-After`, then retry once.
internal_error500yesServer fault. Retry once.
unavailable503yesTemporarily over capacity. Retry once.

Dates are often unknown

Many documents have a null effective_from: the corpus does not know when they entered into force. Treat null as unknown — never as today and never as undated — and say so when a date is load-bearing. publication_date is a free-form string because sources write it inconsistently; prefer effective_from for comparisons.

A complete research loop

  1. 1. /v1/search with the question in Spanish and the relevant country.
  2. 2. Pick one to three results by title and snippet. Don't fetch everything.
  3. 3. /v1/documents/{id} for each, and quote only from that text.
  4. 4. /v1/documents/{id}/citation for each document you cite.
  5. 5. Answer with every claim attached to a retrieved document. Say where the corpus was silent, where a date was unknown, and where the search may not have been exhaustive.

Not legal advice

Leya returns primary sources; it does not give legal advice. Anything built on it is a draft for attorney review.