Every operation answers JSON in the same envelope, whether it succeeds or fails, so one piece of code can read any answer. The one exception is text answers for AI agents, which you ask for.
The envelope
| Field | On success | On failure |
|---|---|---|
success |
true |
false |
data |
The result: a record, a page, or what the operation declares. | null |
error |
null |
The error object. |
metadata |
{ "requestId": "…" } |
{ "requestId": "…" } |
A success, here from account.apiKeys.delete:
{
"success": true,
"data": { "id": "exampleClientId0001", "deleted": true },
"error": null,
"metadata": { "requestId": "00000000-0000-4000-8000-000000000001" }
}The status code tells success from failure too: successes answer 200, failures 4xx or 5xx. The TypeScript SDK resolves with the whole envelope and throws ApiError for any other status; the Rust SDK returns data.
The error object
A failure, here a request without a valid credential:
{
"success": false,
"data": null,
"error": {
"code": "UNAUTHORIZED",
"message": "A valid access token is required.",
"retryable": false,
"data": {}
},
"metadata": { "requestId": "00000000-0000-4000-8000-000000000002" }
}| Field | Meaning |
|---|---|
code |
The failure code, stable across releases. Branch on it. |
message |
What went wrong, for people. It never contains input values. Don’t parse it. |
retryable |
Whether repeating the same request may succeed. See Retries. |
data |
Diagnostics the code declares. Empty for most codes. |
For INVALID_INPUT, data.issues lists up to 10 problems, each with a path, an RFC 6901 JSON Pointer to the refused field (empty for the whole input), and a message.
Common codes
These codes can come from any service. Each operation’s page in the API reference lists the codes it answers, by status.
| Status | code |
retryable |
Meaning |
|---|---|---|---|
400 |
INVALID_INPUT |
false |
The request does not match the published schema. |
401 |
UNAUTHORIZED |
false |
A valid access token is required. See 401 or 403. |
403 |
FORBIDDEN |
false |
The token does not authorize this operation. |
404 |
NOT_FOUND |
false |
Nothing matches the path. |
405 |
HTTP_METHOD_NOT_ALLOWED |
false |
The HTTP method is not supported for this resource. |
413 |
REQUEST_TOO_LARGE |
false |
The request exceeds the allowed size. See Size limits. |
415 |
UNSUPPORTED_MEDIA_TYPE |
false |
The request content type is not supported. Send JSON bodies as application/json. |
429 |
RATE_LIMITED |
true |
The request rate limit was exceeded. Wait the seconds in Retry-After. |
500 |
INTERNAL_ERROR |
false |
The service could not complete the operation. |
502 |
OUTCOME_UNKNOWN |
false |
The operation may have been applied. Check its result before repeating it. |
503 |
UPSTREAM_UNAVAILABLE |
true |
The upstream service could not complete the request. |
504 |
UPSTREAM_TIMEOUT |
true |
The upstream service did not respond before its deadline. |
Services add their own codes for their own rules, such as API_KEY_NOT_FOUND (404) in Account or VERSION_CONFLICT (409) in Raíz.
Request ids
Every response carries an X-Request-Id header, and the envelope repeats it as metadata.requestId. Log it with every failure, and include it when you report a problem.
Responses to 429, and to some other failures as their reference shows, also carry Retry-After: the seconds to wait before another attempt.
Size limits
Each operation accepts a request and returns a response of up to a maximum size, which its page in the API reference shows under Limits. A larger body is refused with 413 REQUEST_TOO_LARGE; on MCP, the limit bounds the tool call’s input.
Most operations accept a body of up to 32 KiB. Operations that carry files accept more, such as raiz.knowledge.create (12 MiB) and raiz.contacts.import (16 MiB). Responses range from 256 KiB, as for account.organization.get, to 16 MiB for account.invoiceDocuments.get. The Rust SDK treats a response larger than its operation’s limit as invalid.
In the API’s OpenAPI document (https://api.getarbol.com/v1/<service>/openapi.json), each operation carries its limits in bytes, as x-max-body-bytes and x-max-output-bytes.
Text answers for AI agents
Some integration operations, such as SURA’s, Nueva EPS’s and ADRES’s, take an optional agentMode. With agentMode: true, they answer concise Spanish text meant for an AI agent, with the same authorization. Depending on the operation, the text comes as text/plain instead of JSON, or inside the envelope, in data.text; each operation’s page in the reference says which. Leave it out to get the structured result.