Documentation
Responses
The common envelope, pagination, headers and language.
Every /api/v1 response — success and error, collection and single resource — has the same shape. You write one parser and it serves all fifty operations.
{
"status": "success",
"message": "Operación exitosa",
"message_code": "api_v1_ok",
"data": { "…": "…" },
"meta": { "request_id": "9f1c0a3e-7b2d-4f18-9a55-2c6e0d1b4a77" }
}| Field | What it is |
|---|---|
status | success or error. |
message | Human-readable text, translated per Accept-Language. For people, not for code. |
message_code | Stable identifier in lower_snake_case. This is what you branch on. |
data | An object for a single resource, a list for a collection. Never null: when there is nothing, it is []. |
meta | Always request_id; on collections, also page, size and total. |
Heads up
Do not branch on message: it is interface text, it changes with the language and can be reworded without notice. message_code is contract.
Pagination
Collections paginate with page (starting at 1) and size (default 50, max 200). The total is in meta.total, so you know how many pages there are before walking them.
{
"status": "success",
"message": "Operación exitosa",
"message_code": "api_v1_ok",
"data": [ { "id": 31, "name": "Procedimientos de soporte" } ],
"meta": {
"request_id": "9f1c0a3e-7b2d-4f18-9a55-2c6e0d1b4a77",
"page": 1,
"size": 50,
"total": 128
}
}def all_pages(session, url, token, **params):
page = 1
while True:
payload = session.get(
url,
headers={"Authorization": f"Bearer {token}"},
params={**params, "page": page, "size": 200},
timeout=30,
).json()
yield from payload["data"]
meta = payload["meta"]
if meta["page"] * meta["size"] >= meta["total"]:
return
page += 1Note
page and size are accepted across the API, but they only matter on collections. Sending them on a POST breaks nothing; it also does nothing.
Analytics are not paginated
The eight `/analytics` endpoints and GET /catalog/countries return complete series, not collections: their meta carries only request_id, with no page, size or total, and pagination parameters are ignored. The iterator above does not apply there — it would cut a 365-day series at the first page —: what bounds an analytic is its date range.
Response headers
| Header | What it carries |
|---|---|
X-Request-Id | The same value as meta.request_id. Keep it in your logs: it is what to quote so we can trace a request. |
X-RateLimit-Limit | The credential's requests per minute. |
X-RateLimit-Remaining | How many are left in the window. |
X-RateLimit-Reset | Epoch at which the window resets. |
Retry-After | Only on a 429: seconds to wait. |
Idempotent-Replayed | true when the response is the replay of a previous turn. |
WWW-Authenticate | On 401 and 403: error="invalid_token" or error="insufficient_scope", scope="…". |
Language
The message field is translated using the Accept-Language header. `es` (default) and `en` are supported, with q-values and subtags: en-US resolves to en, and es-AR;q=0.9, en;q=0.8 to es. Anything else falls back to Spanish.
GET /api/v1/chats HTTP/1.1
Authorization: Bearer …
Accept-Language: en-US,en;q=0.9Note
message_code does not change with the language: it is the same identifier in both.
Request sizes
| Limit | Value | When exceeded |
|---|---|---|
/api/v1 JSON body | 256 KB | 413 payload_too_large |
/api/oauth/* form | 8 KB | 413 invalid_request |
A turn's content | 64,000 characters | 422 api_v1_content_too_long |
A turn's metadata | 2048 bytes | 422 api_v1_metadata_too_large |
| Attachments per type / total | 20 / 50 | 422 api_v1_too_many_attachments |
Note
The body cap is enforced at the transport layer, over what is actually received — not over the declared Content-Length. A lying header or a giant chunked upload is cut off just the same.
Unknown fields
Bodies are strict: sending a field the API does not accept returns 422 instead of being ignored. This is deliberate — a silently ignored field is a change you believed had been applied and never was.
{
"status": "error",
"message": "El request no cumple el contrato.",
"message_code": "api_v1_validation_error",
"data": {
"errors": [
{
"field": "body.anonymize",
"message": "Extra inputs are not permitted",
"type": "extra_forbidden"
}
]
},
"meta": { "request_id": "…" }
}Note
Validation detail never includes the value you sent: user content can live there.