Vitrina API
Errores

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"
  }
}
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 («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.

En esta página