Errores
El sobre que trae cada falla, los códigos que te vas a encontrar de verdad y qué hacer con cada uno.
Una respuesta correcta trae data. Una fallida trae error. Nunca las dos, y nunca ninguna.
{
"error": {
"code": "NOT_FOUND",
"message": "Vehículo no encontrado",
"requestId": "e3c36709-4fff-462d-8940-4ff9e8138ce6"
}
}| Campo | Qué es |
|---|---|
code | El contrato. Es lo único por lo que debes ramificar. |
message | Para una persona. Puede cambiar de redacción sin aviso. |
requestId | El identificador de tu llamada en nuestros logs. Regístralo. |
details | Opcional. Qué falló exactamente, cuando se puede decir. |
field_errors | Opcional. 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 («Vehículo no encontrado», «Sucursal no encontrada») y en inglés cuando describe un problema de la llamada («Missing required scope: marketplace:read»). No lo muestres tal cual a un usuario final y no lo parsees.
Los que vas a ver
401 UNAUTHORIZED — la credencial
Un mismo código, tres mensajes. Los tres los saqué de una corrida real.
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 tercero es el único que se ve venir, y por eso tiene mensaje propio. El del medio junta «no existe» con «fue revocada» a propósito: distinguirlos le diría a quien prueba llaves al azar si acertó alguna vez.
Reintentar no arregla ninguno de los tres.
403 FORBIDDEN — la credencial existe, el permiso no
{ "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.
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. Esa indistinguibilidad es la política, no un descuido: un 403 ahí sería confirmarte que el recurso existe en alguna parte.
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 en forma de { campo: mensaje }. Existe para que un formulario pueda pintar 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 — reusaste una llave con otro cuerpo
Todo POST acepta el header Idempotency-Key — solo POST, porque es el único verbo que crea algo nuevo cada vez que se repite. Repetir la misma llave con el mismo cuerpo devuelve la respuesta original y agrega X-Idempotent-Replay: 1; es lo que hace seguro reintentar un POST cuya respuesta nunca te llegó.
Repetirla con un cuerpo distinto 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 una llave vieja. Genera una llave por operación.
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 — es lo que nos deja encontrar la llamada exacta.
Trampa
Ramifica por `code`, no por el estado HTTP
Un mismo estado cubre causas que se resuelven distinto. 409 es un conflicto de
idempotencia (IDEMPOTENCY_KEY_CONFLICT: genera otra llave) y también uno de
unicidad (UNIQUE_CONFLICT: cambia el dato). Y al revés, un mismo código
aparece con dos estados: VALIDATION_ERROR es 400 cuando la llamada no pasó
la validación y 422 cuando se leyó bien pero apuntaba a algo inservible. El
estado te dice la familia; el code te dice qué hacer.