Documentación

Errores

Todos los códigos que puede devolver la API, qué significan y cuáles vale la pena reintentar.

Un error usa el mismo envelope que un éxito, con status: "error". Ramificá sobre message_code, no sobre 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" }
}

Indisponibilidad: error_details

Cuando la causa es que una dependencia no está disponible —y no un error tuyo ni una regla de negocio—, la respuesta suma meta.error_details. Es aditivo: la raíz del envelope sigue teniendo las mismas cinco claves.

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"
    }
  }
}
CampoQué dice
service_code / serviceEl componente que se comprobó caído, de un catálogo estable (NC-SVC-01 servicio de conversación, NC-SVC-11 base de datos…). Útil para el ticket; no ramifiques sobre él.
failure_kindCómo falló la conexión: connect_timeout, read_timeout, stream_interrupted, store_unavailable
operation_outcomenot_started (no salió nada), unknown (salió y no se sabe si se aplicó) o partial.
retry_actionEl campo que decide: retry (repetir es seguro), check_status (consultá el estado antes de repetir) o none.
retry_after_sSolo con retry_action: "retry", y entonces también viaja como header Retry-After.
persisted / persistence_failedAparecen cuando lo que falló fue guardar el resultado.

retryable habla de la conexión

Un turno que se despachó pudo haber llamado al modelo y ejecutado herramientas antes de cortarse. Por eso sus errores llegan con retryable: false y retry_action: "check_status": repetirlo a ciegas podría duplicar un efecto que sí ocurrió.

Por código HTTP

StatusQué significa¿Reintentar?
400Falta algo obligatorio del request (típicamente el Idempotency-Key).No, sin corregirlo.
401Token ausente, inválido, expirado o revocado. También si la credencial o su titular dejaron de ser válidos.Sí, una vez, con un token nuevo. Si vuelve, es la credencial.
403Falta un scope, o el recurso nombrado está fuera del alcance.No: es configuración.
404No existe o está fuera del alcance. Los dos casos responden igual, a propósito.No.
409Conflicto de estado: idempotencia, turno en curso, chat ocupado, interacción ya resuelta o con resultado desconocido.Depende del código — ver abajo.
413El cuerpo supera el máximo.No.
422El cuerpo no cumple el contrato: campo desconocido, tipo equivocado, valor fuera de rango, o un texto con referencias que no se pueden usar.No.
429Se superó el límite de requests.Sí, esperando Retry-After.
500Error interno. El cuerpo no dice por qué; el detalle queda en nuestro log, cruzado por request_id.Sí, con backoff.
502El servicio de conversación falló o no respondió.Primero mirá el estado: el turno se despachó y pudo tener efectos. Después, con una key nueva.
503Una dependencia no está disponible. meta.error_details dice cuál y si repetir es seguro.Según retry_action: retry sí, con backoff; check_status, primero consultá.
504El turno superó su tiempo máximo.Sí, con una key nueva. El turno anterior quedó cerrado.

Códigos de la API

`message_code`StatusCuándo
invalid_token401Cubre todo el rechazo de autenticación: header ausente, firma inválida, token vencido o revocado, credencial desactualizada, usuario o empresa inhabilitados. El message distingue el caso; el código, no.
insufficient_scope403La operación exige un scope que el token no tiene. El header WWW-Authenticate lo nombra.
api_token_scope_not_allowed403Se adjuntó un recurso sin su scope compañero. data.required_scopes dice cuáles.
api_v1_not_found404El recurso no existe, o está fuera del alcance de la credencial.
api_v1_resource_not_allowed403El cliente nombró explícitamente un recurso que no puede usar.
api_v1_resource_policy_create_denied403La credencial está en allowlist sobre ese tipo y no puede crear recursos nuevos.
api_v1_validation_error422El cuerpo no cumple el contrato. data.errors lista campo, mensaje y tipo.
payload_too_large413El cuerpo supera el máximo admitido.
rate_limit_exceeded429Se agotó la cubeta de la credencial o de la IP.
api_v1_internal_error500Error no manejado. Citá el request_id.

Específicos del chat

`message_code`StatusCuándo y qué hacer
api_v1_idempotency_key_required400Falta el header. Nunca se genera uno por vos.
api_v1_idempotency_conflict409La misma key se usó antes con otro cuerpo. Usá una key nueva: reintentar con esta va a fallar siempre.
api_v1_turn_in_progress409El turno de esa key todavía corre. Reintentá con la misma key más tarde.
api_v1_chat_busy409Ese chat ya tiene un turno en curso, de la API o de la aplicación (data.message_id dice cuál). Esperá, cancelalo, o usá otro chat.
api_v1_sync_requires_auto409El envío sincrónico exige permission_mode: "auto". Para manual, usá el stream.
api_v1_content_too_long422content supera el máximo. data.max_chars lo dice.
api_v1_too_many_attachments422Demasiados adjuntos. data trae el límite y el campo culpable.
api_v1_metadata_too_large422metadata serializado supera el máximo.
api_v1_action_not_found404La interacción no existe, venció o no es de esta credencial.
api_v1_action_not_pending409Ya se resolvió, otro request la reclamó primero, o su resultado quedó desconocido. No la reintentes: aplicar dos veces la misma aprobación es peor que no saber.
api_v1_action_result_unknown409 / 503La decisión salió y no se puede confirmar que se aplicó (503), o el servicio la recibió y la rechazó (409). En los dos casos la interacción queda cerrada: consultá el turno, no la reenvíes.
api_v1_service_unavailable503La decisión no llegó a salir. La interacción vuelve a pending y reintentar es seguro.
api_v1_interaction_backend_unavailable503Indisponibilidad nuestra, no un error tuyo. Reintentá.
api_v1_core_unavailable409 / 500 / 502 / 503No se pudo entregar una cancelación (502), o el turno sincrónico no pudo confirmar su cierre. data.code dice cuál: persistence_failed, outcome_unknown o idempotency_outcome_unknown. En todos, consultá el estado con la misma key antes de reintentar.
turn_timeout504El turno sincrónico superó su tiempo máximo y se cortó. data trae chat_id y message_id.
upstream_error y otros del turno502Un turno sincrónico que falló devuelve como message_code el código del propio turno, con chat_id y message_id en data. Es exactamente el cuerpo que guarda la key.
interaction_in_auto_mode502El turno sincrónico pidió una interacción que ese canal no puede contestar y se cortó. Usá el stream.
api_v1_library_embedding_unavailable503No hay modelo de embedding disponible para crear bibliotecas.

Específicos de habilidades y reglas

`message_code`StatusCuándo y qué hacer
prompt_refs_invalid422instructions cita recursos que no se pueden usar. data[0] los agrupa en denied, invalid, unsupported, over_limit y cycle. Nada se guardó. Ver Referencias a recursos.
prompt_text_too_long422instructions supera el máximo: 12 000 caracteres en una habilidad, 4 000 en una regla. data[0].too_long trae el límite y el largo.

Errores de OAuth

Los endpoints /api/oauth/* no usan el envelope: responden con la forma del RFC, { "error", "error_description" }.

JSON
{
  "error": "invalid_scope",
  "error_description": "La credencial no tiene esos permisos: rules:write"
}
`error`StatusCuándo
invalid_client401Cliente o secreto inválidos, credencial revocada o vencida, usuario o empresa inhabilitados. Un solo error para todos.
unsupported_grant_type400grant_type no es client_credentials.
invalid_scope400Se pidió un scope inexistente o que la credencial no tiene vigente.
invalid_request400 / 413Credenciales por dos canales a la vez, o formulario demasiado grande.
server_error503Configuración incompleta del servidor. Reintentá y avisanos.

Reintentar bien

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

Reintentar un turno

Un envío que falló con 502 o 504 ya consumió su `Idempotency-Key`: reintentar con la misma devuelve el error guardado, no un turno nuevo. Pero ese turno se despachó y pudo tener efectos (una herramienta que mandó un correo, por ejemplo): mirá sus mensajes antes de generar una key nueva. api_v1_turn_in_progress y los cierres sin confirmar (persistence_failed, idempotency_outcome_unknown) piden repetir con la MISMA key, para leer lo que quedó.