Documentation

Errors

Every code the API can return, what it means and which ones are worth retrying.

An error uses the same envelope as a success, with status: "error". Branch on message_code, not on message.

JSON
{
  "status": "error",
  "message": "Ese chat ya tiene un turno de la API en curso.",
  "message_code": "api_v1_chat_busy",
  "data": { "message_id": 90212 },
  "meta": { "request_id": "9f1c0a3e-7b2d-4f18-9a55-2c6e0d1b4a77" }
}

Outages: error_details

When the cause is that a dependency is unavailable —not your mistake nor a business rule— the response adds meta.error_details. It is additive: the envelope root keeps the same five keys.

JSON
{
  "status": "error",
  "message": "No se pudo confirmar si la acción se aplicó. Consultá el estado del turno antes de reintentar.",
  "message_code": "api_v1_action_result_unknown",
  "data": [],
  "meta": {
    "request_id": "9f1c0a3e-7b2d-4f18-9a55-2c6e0d1b4a77",
    "error_details": {
      "schema_version": 1,
      "service": "core_ai",
      "service_code": "NC-SVC-01",
      "dependency": "core_control",
      "operation": "chat.action.resolve",
      "upstream_code": null,
      "failure_kind": "read_timeout",
      "operation_outcome": "unknown",
      "retryable": false,
      "retry_action": "check_status",
      "request_id": "9f1c0a3e-7b2d-4f18-9a55-2c6e0d1b4a77"
    }
  }
}
FieldWhat it says
service_code / serviceThe component verified as down, from a stable catalog (NC-SVC-01 conversation service, NC-SVC-11 database…). Useful for the ticket; do not branch on it.
failure_kindHow the connection failed: connect_timeout, read_timeout, stream_interrupted, store_unavailable
operation_outcomenot_started (nothing left), unknown (it left and it is unknown whether it applied) or partial.
retry_actionThe deciding field: retry (repeating is safe), check_status (check the state before repeating) or none.
retry_after_sOnly with retry_action: "retry", and then it also travels as the Retry-After header.
persisted / persistence_failedPresent when what failed was saving the outcome.

retryable is about the connection

A dispatched turn may have called the model and run tools before being cut. That is why its errors arrive with retryable: false and retry_action: "check_status": repeating it blindly could duplicate an effect that did happen.

By HTTP status

StatusWhat it meansRetry?
400Something mandatory is missing from the request (typically the Idempotency-Key).No, not without fixing it.
401Token missing, invalid, expired or revoked. Also if the credential or its owner stopped being valid.Yes, once, with a fresh token. If it repeats, it is the credential.
403A scope is missing, or the named resource is outside the scope.No: it is configuration.
404It does not exist or it is outside the scope. Both cases answer the same, by design.No.
409State conflict: idempotency, turn in progress, chat busy, interaction already resolved or with an unknown outcome.Depends on the code — see below.
413The body exceeds the maximum.No.
422The body does not match the contract: unknown field, wrong type, value out of range, or a text with references that cannot be used.No.
429The request limit was exceeded.Yes, honoring Retry-After.
500Internal error. The body does not say why; the detail stays in our log, keyed by request_id.Yes, with backoff.
502The conversation service failed or did not answer.Check the state first: the turn was dispatched and may have had effects. Then, with a new key.
503A dependency is unavailable. meta.error_details says which one and whether repeating is safe.Per retry_action: retry yes, with backoff; check_status, check first.
504The turn exceeded its maximum duration.Yes, with a new key. The previous turn is closed.

API codes

`message_code`StatusWhen
invalid_token401Covers every authentication rejection: missing header, invalid signature, expired or revoked token, stale credential, disabled user or company. The message tells the cases apart; the code does not.
insufficient_scope403The operation requires a scope the token lacks. The WWW-Authenticate header names it.
api_token_scope_not_allowed403A resource was attached without its companion scope. data.required_scopes lists them.
api_v1_not_found404The resource does not exist, or is outside the credential's scope.
api_v1_resource_not_allowed403The client explicitly named a resource it cannot use.
api_v1_resource_policy_create_denied403The credential is in allowlist mode over that type and cannot create new resources.
api_v1_validation_error422The body does not match the contract. data.errors lists field, message and type.
payload_too_large413The body exceeds the maximum allowed.
rate_limit_exceeded429The credential or IP bucket ran out.
api_v1_internal_error500Unhandled error. Quote the request_id.

Chat-specific

`message_code`StatusWhen and what to do
api_v1_idempotency_key_required400The header is missing. One is never generated for you.
api_v1_idempotency_conflict409The same key was used before with a different body. Use a new key: retrying with this one will always fail.
api_v1_turn_in_progress409The turn for that key is still running. Retry with the same key later.
api_v1_chat_busy409That chat already has a turn running, from the API or from the application (data.message_id says which). Wait, cancel it, or use another chat.
api_v1_sync_requires_auto409The synchronous send requires permission_mode: "auto". For manual, use the stream.
api_v1_content_too_long422content exceeds the maximum. data.max_chars states it.
api_v1_too_many_attachments422Too many attachments. data carries the limit and the offending field.
api_v1_metadata_too_large422Serialized metadata exceeds the maximum.
api_v1_action_not_found404The interaction does not exist, expired, or is not yours.
api_v1_action_not_pending409It was already resolved, another request claimed it first, or its outcome is unknown. Do not retry: applying the same approval twice is worse than not knowing.
api_v1_action_result_unknown409 / 503The decision left and it cannot be confirmed it applied (503), or the service received and rejected it (409). Either way the interaction is closed: check the turn, do not resend it.
api_v1_service_unavailable503The decision never left. The interaction goes back to pending and retrying is safe.
api_v1_interaction_backend_unavailable503An outage on our side, not your mistake. Retry.
api_v1_core_unavailable409 / 500 / 502 / 503A cancellation could not be delivered (502), or the synchronous turn could not confirm its close. data.code says which: persistence_failed, outcome_unknown or idempotency_outcome_unknown. In every case, check the state with the same key before retrying.
turn_timeout504The synchronous turn exceeded its maximum duration and was cut off. data carries chat_id and message_id.
upstream_error and other turn codes502A failed synchronous turn returns the turn's own code as message_code, with chat_id and message_id in data. It is exactly the body the key stores.
interaction_in_auto_mode502The synchronous turn asked for an interaction that channel cannot answer, and it was cut. Use the stream.
api_v1_library_embedding_unavailable503No embedding model is available to create libraries.

Skill and rule specific

`message_code`StatusWhen and what to do
prompt_refs_invalid422instructions cites resources that cannot be used. data[0] groups them into denied, invalid, unsupported, over_limit and cycle. Nothing was saved. See Resource references.
prompt_text_too_long422instructions exceeds the maximum: 12,000 characters for a skill, 4,000 for a rule. data[0].too_long carries the limit and the length.

OAuth errors

The /api/oauth/* endpoints do not use the envelope: they answer with the RFC shape, { "error", "error_description" }.

JSON
{
  "error": "invalid_scope",
  "error_description": "La credencial no tiene esos permisos: rules:write"
}
`error`StatusWhen
invalid_client401Invalid client or secret, revoked or expired credential, disabled user or company. One error for all of them.
unsupported_grant_type400grant_type is not client_credentials.
invalid_scope400A nonexistent scope was requested, or one the credential does not currently hold.
invalid_request400 / 413Credentials through two channels at once, or an oversized form.
server_error503Incomplete server configuration. Retry and let us know.

Retrying properly

import random
import time

RETRYABLE = {429, 500, 502, 503, 504}


def with_retries(call, attempts: int = 4):
    """Reintenta con backoff exponencial y jitter.

    Solo los status de `RETRYABLE`: un 4xx de contrato no mejora por insistir.
    """
    for attempt in range(attempts):
        response = call()
        if response.status_code not in RETRYABLE or attempt == attempts - 1:
            return response

        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2**attempt
        time.sleep(delay + random.uniform(0, 0.5))

    return response

Retrying a turn

A send that failed with 502 or 504 already consumed its `Idempotency-Key`: retrying with the same one returns the stored error, not a new turn. But that turn was dispatched and may have had effects (a tool that sent an email, for example): look at its messages first before generating a new key. api_v1_turn_in_progress and unconfirmed closes (persistence_failed, idempotency_outcome_unknown) ask you to repeat with the SAME key, to read what was stored.