VitrinaAPI
Errores

Leer un error y reaccionar

El sobre de cada falla y qué hacer con cada código.

Una respuesta correcta trae data. Una fallida trae error.

{
  "error": {
    "code": "NOT_FOUND",
    "message": "Sucursal no encontrada",
    "requestId": "bf44aad4-6554-45ea-95f5-bc215f11d373"
  }
}
CampoQué es
codeEl contrato. Es lo único por lo que debes ramificar.
messagePara una persona. Puede cambiar de redacción sin aviso.
requestIdEl identificador de tu llamada en nuestros logs. Regístralo.
detailsOpcional. Qué falló exactamente, cuando se puede decir.
field_errorsOpcional. Lo mismo que details, indexado por campo.

El code va en mayúsculas con guiones bajos y sale de una lista cerrada. La tienes completa en Códigos de error.

El message viene en español cuando nombra algo del dominio: «Sucursal no encontrada», «Cita no encontrada». Viene en inglés cuando describe un problema de la llamada: «Missing required scope: webhooks:write». No lo muestres tal cual a un usuario final y no lo parsees.

Los que vas a ver

401 UNAUTHORIZED: la credencial no sirve

Un mismo código, tres mensajes.

Sin header Authorization:

{ "error": { "code": "UNAUTHORIZED", "message": "Missing bearer token", "requestId": "fb978d51-e751-4ab6-903d-22ba53592320" } }

Con una key que no existe, o que ya fue revocada:

{ "error": { "code": "UNAUTHORIZED", "message": "Invalid API key", "requestId": "a1e8a7a9-de68-4d74-9c35-20158654a987" } }

Con una key vigente cuyo expires_at ya pasó:

{ "error": { "code": "UNAUTHORIZED", "message": "API key expired", "requestId": "89f197a7-8787-4620-bd8f-981bf2a939e9" } }

El primero casi siempre es un despliegue que perdió su variable de entorno. El segundo no distingue una key inexistente de una revocada. El tercero se evita rotando la key antes de su expires_at.

Reintentar no arregla ninguno de los tres.

403 FORBIDDEN: falta un scope

{ "error": { "code": "FORBIDDEN", "message": "Missing required scope: marketplace:read", "requestId": "2c5645c2-d902-4e6f-ad1c-8d924b063ba1" } }

El mensaje nombra el permiso que falta. Emite una key nueva que lo incluya: los scopes de una key no se editan. Está en Autenticación.

403 CONNECTED_APP_SENSITIVE_DATA: una aplicación conectada pidió un dato sensible

{ "error": { "code": "CONNECTED_APP_SENSITIVE_DATA", "message": "Esta operación entrega datos personales sensibles (…). Una aplicación conectada no puede leerlos: úsala con una API key o un token personal del workspace, emitido para este fin. …", "requestId": "7d0b3c2e-5a41-4f7e-9c11-2b8e6f0a4d93" } }

La credencial es de una aplicación conectada, un cliente autorizado por OAuth. El código sale en dos casos. Uno, la operación está marcada x-vitrina-sensitive. Dos, la respuesta no es JSON: una exportación, un archivo, un flujo. Eso pasa en un workspace que muestra a las personas con seudónimo ante aplicaciones conectadas, y lo que no pasa por ese filtro no sale.

Pedir más scopes no lo arregla: lo que está cerrado es esa clase de credencial. Si tu integración necesita el dato, usa una API key o un token personal del workspace. Está en Datos personales y de salud.

403 CONNECTED_APP_EXCLUDED_OPERATION: una aplicación conectada pidió algo que su permiso de escritura no incluye

{ "error": { "code": "CONNECTED_APP_EXCLUDED_OPERATION", "message": "Esta operación no está disponible para una aplicación conectada: el permiso de escritura que autorizaste no la incluye (crear o borrar agentes de IA). …", "requestId": "3f9a1c7e-2b64-4d08-8e5a-6c1d9b2f7a40" } }

La operación está publicada, pero el área de escritura que la pantalla de permisos ofrece promete no incluirla: hoy, crear y borrar agentes de IA («Agentes de IA»). Para una aplicación conectada no aparece en el catálogo de MCP y por la API directa responde esto, con cualquier scope. Una API key o un token personal del workspace con ai_agents:write la ejecuta normalmente.

404 NOT_FOUND: no existe, o no es tuyo

{ "error": { "code": "NOT_FOUND", "message": "Sucursal no encontrada", "requestId": "75a3d88b-8703-42a2-9c40-127fec0dfcc3" } }

Un recurso de otro workspace responde exactamente igual que uno inexistente.

400 VALIDATION_ERROR: la llamada está mal escrita

Es el único que te dice dónde:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": {
      "body": [{
        "path": "comuna_code",
        "message": "comuna_code must be the official 5-digit CUT code (13114 = Las Condes), never a comuna name and never a portal id",
        "code": "invalid_string"
      }]
    },
    "field_errors": {
      "comuna_code": "comuna_code must be the official 5-digit CUT code (13114 = Las Condes), never a comuna name and never a portal id"
    },
    "requestId": "4c1e5e23-ae89-4e57-a711-5e962e8f4a8b"
  }
}

details se agrupa por dónde estaba el problema: body, query o params. Un limit fuera de rango llega bajo query; un id que no es uuid, bajo params.

field_errors es el mismo contenido con la forma { campo: mensaje }. Existe para que un formulario pinte el error bajo el input correcto sin recorrer details. Solo aparece cuando el fallo se puede atribuir a un campo.

Nunca reintentes un 400 sin cambiar la llamada.

409 IDEMPOTENCY_KEY_CONFLICT: el mismo header con otro body

Todo POST publicado acepta el header Idempotency-Key sin que tengas que activarlo por operación. Publicado quiere decir de nivel estable o beta; las operaciones internas quedan fuera. Solo POST, porque es el único verbo que crea algo nuevo cada vez que se repite.

Repetir el mismo valor con el mismo body devuelve la respuesta original y agrega X-Idempotent-Replay: 1. Eso es lo que hace seguro reintentar un POST cuya respuesta nunca te llegó.

Repetirlo con otro body es otra cosa:

{
  "error": {
    "code": "IDEMPOTENCY_KEY_CONFLICT",
    "message": "Idempotency-Key '8592D625-BC6E-4A4B-90B0-C94CCF623D71' was previously used with a different request body",
    "requestId": "9973642b-1646-4a4f-9c2e-fef172ac73c4"
  }
}

Eso no es un reintento: es una operación nueva reusando un valor viejo. Genera un Idempotency-Key por operación.

422 OUTBOUND_BLOCKED y 409 OUTBOUND_WARNING: la política de envíos

Los dos veredictos que puede dar un envío hecho con una credencial. Traen dos campos que ningún otro error trae, reasons y hint, y piden lo contrario uno del otro. El 422 no se puede reintentar. El 409 sí, reenviando la misma llamada con acknowledge.

{
  "error": {
    "reasons": ["template_missing", "window_closed"],
    "hint": "La ventana de 24 horas está cerrada: usa una plantilla aprobada (POST /conversations/{id}/templates).",
    "code": "OUTBOUND_BLOCKED",
    "message": "This message was not sent: the outbound policy blocks it (template_missing, window_closed). A block cannot be acknowledged — see error.hint for what to do instead.",
    "requestId": "03c42c0e-0769-4659-b500-6464a85c3a73"
  }
}

Qué significa cada razón y cómo se reconoce una advertencia está en Enviar mensajes.

429 RATE_LIMITED: pasaste el techo

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
Retry-After: 1
{ "error": { "code": "RATE_LIMITED", "message": "Too many requests", "requestId": "c712a422-32d0-4ea2-bba4-e7e60067560a" } }

Es el único 4xx que se arregla esperando. Retry-After viene en segundos y lo calcula el mismo contador que te rechazó, así que espéralo en vez de inventar un backoff. El detalle está en Autenticación.

500 INTERNAL_ERROR: se rompió de nuestro lado

{ "error": { "code": "INTERNAL_ERROR", "message": "Internal server error", "requestId": "3c5f4e5e-34c1-4f97-8caa-98632613c895" } }

Nunca trae details: un 5xx no describe sus internas. Reintenta con backoff. Si persiste, escríbenos con el requestId; con eso encontramos la llamada exacta.

Trampa

Un mismo estado HTTP cubre errores que se arreglan distinto

409 es un conflicto de idempotencia (IDEMPOTENCY_KEY_CONFLICT: genera otro valor), uno de unicidad (UNIQUE_CONFLICT: cambia el dato) y uno de edición concurrente (VERSION_CONFLICT: alguien guardó después de tu lectura; vuelve a leer y reintenta con la versión nueva, ver Agentes de IA). Al revés también pasa: VALIDATION_ERROR llega como 400 cuando la llamada no pasó la validación y como 422 cuando se leyó bien pero apuntaba a algo inservible.

El estado te dice la familia; el code te dice qué hacer.

Todos los valores que puede tomar error.code, con su estado HTTP, están en Códigos de error.

En esta página