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.
{
"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.
{
"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"
}
}
}| Campo | Qué dice |
|---|---|
service_code / service | El 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_kind | Cómo falló la conexión: connect_timeout, read_timeout, stream_interrupted, store_unavailable… |
operation_outcome | not_started (no salió nada), unknown (salió y no se sabe si se aplicó) o partial. |
retry_action | El campo que decide: retry (repetir es seguro), check_status (consultá el estado antes de repetir) o none. |
retry_after_s | Solo con retry_action: "retry", y entonces también viaja como header Retry-After. |
persisted / persistence_failed | Aparecen 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
| Status | Qué significa | ¿Reintentar? |
|---|---|---|
400 | Falta algo obligatorio del request (típicamente el Idempotency-Key). | No, sin corregirlo. |
401 | Token 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. |
403 | Falta un scope, o el recurso nombrado está fuera del alcance. | No: es configuración. |
404 | No existe o está fuera del alcance. Los dos casos responden igual, a propósito. | No. |
409 | Conflicto de estado: idempotencia, turno en curso, chat ocupado, interacción ya resuelta o con resultado desconocido. | Depende del código — ver abajo. |
413 | El cuerpo supera el máximo. | No. |
422 | El cuerpo no cumple el contrato: campo desconocido, tipo equivocado, valor fuera de rango, o un texto con referencias que no se pueden usar. | No. |
429 | Se superó el límite de requests. | Sí, esperando Retry-After. |
500 | Error interno. El cuerpo no dice por qué; el detalle queda en nuestro log, cruzado por request_id. | Sí, con backoff. |
502 | El 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. |
503 | Una 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á. |
504 | El turno superó su tiempo máximo. | Sí, con una key nueva. El turno anterior quedó cerrado. |
Códigos de la API
| `message_code` | Status | Cuándo |
|---|---|---|
invalid_token | 401 | Cubre 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_scope | 403 | La operación exige un scope que el token no tiene. El header WWW-Authenticate lo nombra. |
api_token_scope_not_allowed | 403 | Se adjuntó un recurso sin su scope compañero. data.required_scopes dice cuáles. |
api_v1_not_found | 404 | El recurso no existe, o está fuera del alcance de la credencial. |
api_v1_resource_not_allowed | 403 | El cliente nombró explícitamente un recurso que no puede usar. |
api_v1_resource_policy_create_denied | 403 | La credencial está en allowlist sobre ese tipo y no puede crear recursos nuevos. |
api_v1_validation_error | 422 | El cuerpo no cumple el contrato. data.errors lista campo, mensaje y tipo. |
payload_too_large | 413 | El cuerpo supera el máximo admitido. |
rate_limit_exceeded | 429 | Se agotó la cubeta de la credencial o de la IP. |
api_v1_internal_error | 500 | Error no manejado. Citá el request_id. |
Específicos del chat
| `message_code` | Status | Cuándo y qué hacer |
|---|---|---|
api_v1_idempotency_key_required | 400 | Falta el header. Nunca se genera uno por vos. |
api_v1_idempotency_conflict | 409 | La misma key se usó antes con otro cuerpo. Usá una key nueva: reintentar con esta va a fallar siempre. |
api_v1_turn_in_progress | 409 | El turno de esa key todavía corre. Reintentá con la misma key más tarde. |
api_v1_chat_busy | 409 | Ese 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_auto | 409 | El envío sincrónico exige permission_mode: "auto". Para manual, usá el stream. |
api_v1_content_too_long | 422 | content supera el máximo. data.max_chars lo dice. |
api_v1_too_many_attachments | 422 | Demasiados adjuntos. data trae el límite y el campo culpable. |
api_v1_metadata_too_large | 422 | metadata serializado supera el máximo. |
api_v1_action_not_found | 404 | La interacción no existe, venció o no es de esta credencial. |
api_v1_action_not_pending | 409 | Ya 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_unknown | 409 / 503 | La 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_unavailable | 503 | La decisión no llegó a salir. La interacción vuelve a pending y reintentar es seguro. |
api_v1_interaction_backend_unavailable | 503 | Indisponibilidad nuestra, no un error tuyo. Reintentá. |
api_v1_core_unavailable | 409 / 500 / 502 / 503 | No 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_timeout | 504 | El turno sincrónico superó su tiempo máximo y se cortó. data trae chat_id y message_id. |
upstream_error y otros del turno | 502 | Un 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_mode | 502 | El turno sincrónico pidió una interacción que ese canal no puede contestar y se cortó. Usá el stream. |
api_v1_library_embedding_unavailable | 503 | No hay modelo de embedding disponible para crear bibliotecas. |
Específicos de habilidades y reglas
| `message_code` | Status | Cuándo y qué hacer |
|---|---|---|
prompt_refs_invalid | 422 | instructions 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_long | 422 | instructions 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" }.
{
"error": "invalid_scope",
"error_description": "La credencial no tiene esos permisos: rules:write"
}| `error` | Status | Cuándo |
|---|---|---|
invalid_client | 401 | Cliente o secreto inválidos, credencial revocada o vencida, usuario o empresa inhabilitados. Un solo error para todos. |
unsupported_grant_type | 400 | grant_type no es client_credentials. |
invalid_scope | 400 | Se pidió un scope inexistente o que la credencial no tiene vigente. |
invalid_request | 400 / 413 | Credenciales por dos canales a la vez, o formulario demasiado grande. |
server_error | 503 | Configuració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 responseReintentar 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ó.