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.
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).
| Endpoint | Credits |
|---|---|
| GET /v1/search | 3 |
| GET /v1/parcels/{ccc} | 2 |
| GET /v1/parcels | 2 |
| GET /v1/documents/{id} | 1 |
| GET /v1/documents/{id}/citation | 1 |
| GET /v1/cadastre/coverage | 1 |
| GET /v1/me, /v1/credits, /v1/sources, /v1/filters | free |
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
| Param | Type | Notes |
|---|---|---|
| q | string, required | Natural-language or keyword query. Spanish works best. |
| country | ISO-2 | GT, DO. Omit to search every covered jurisdiction. |
| source_id | string | Restrict to one dataset. Values from /v1/sources. |
| document_type | string | Corpus-defined, e.g. decreto, iniciativa, sentencia. |
| legal_area | string | Corpus-defined, e.g. laboral, mercantil, fiscal. |
| as_of | YYYY-MM-DD | Excludes documents known to be outside force then. Unknown-date rows are kept. |
| limit | int 1–50 | Default 10. 51+ returns 422 rather than clamping. |
| cursor | string | next_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.
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.
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.
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}]
}Registered datasets and the source_id values the search filter accepts.
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.
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.
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.
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"
}
}| code | HTTP | Retryable | Meaning |
|---|---|---|---|
| unauthenticated | 401 | no | Missing, malformed, or revoked key. |
| insufficient_credits | 402 | no | Out of credits. Waiting never clears it — top up or stop. |
| payment_required | 402 | no | Purchase route only: step one of the MPP handshake, not a failure. |
| forbidden | 403 | no | Key valid but not permitted for this resource. |
| not_found | 404 | no | No such document. Re-search; do not invent it. |
| parcel_not_found | 404 | no | No such parcel. Read `coverage`: usually unsurveyed, not nonexistent. |
| method_not_allowed | 405 | no | Wrong HTTP method for that path. |
| invalid_request | 422 | no | Bad parameter. `fields[]` names each one. |
| rate_limited | 429 | yes | Honour `Retry-After`, then retry once. |
| internal_error | 500 | yes | Server fault. Retry once. |
| unavailable | 503 | yes | Temporarily 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.
/v1/searchwith the question in Spanish and the relevantcountry. - 2. Pick one to three results by title and snippet. Don't fetch everything.
- 3.
/v1/documents/{id}for each, and quote only from that text. - 4.
/v1/documents/{id}/citationfor each document you cite. - 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.