Todas las respuestas de error siguen el mismo formato JSON:
{
"error": "CODIGO_MAYUSCULAS",
"message": "Detalle legible",
"details": { /* opcional, contexto extra */ }
}
Códigos
| HTTP | error | Causa | Acción del cliente |
|---|
400 | BAD_REQUEST | Body inválido / falta campo requerido | Corregir payload |
400 | INFERENCE_FAILED | El gateway no pudo deducir emisor/receptor en un endpoint _auto | Reintentar con la versión sin _auto y emisor explícito |
401 | NO_API_KEY | Falta el header x-api-key | Agregarlo |
401 | INVALID_API_KEY | Key revocada, mal formada o secreto incorrecto | Generar una nueva |
401 | UNAUTHORIZED | Sesión Supabase ausente/expirada (rutas de gestión) | Volver a iniciar sesión |
402 | QUOTA_EXCEEDED | Cuota mensual del usuario agotada | Upgrade de plan; no reintentar |
404 | NOT_FOUND | Banxico no devolvió un CEP con esos parámetros | Revisar fecha, monto, clave_rastreo |
429 | RATE_LIMITED | Excediste per_minute de la key | Backoff con jitter y reintentar |
500 | INTERNAL_ERROR | Falla interna no clasificada | Reintentar 1 vez; reportar a soporte si persiste |
502 | UPSTREAM_ERROR | El backend de Banxico devolvió un error transitorio | El gateway ya hace un reintento; si persiste, esperar |
Patrón de manejo recomendado
async function downloadCep(req: CepReq) {
const res = await fetch("/api/v1/cep/xml_auto", {
method: "POST",
headers: { "x-api-key": process.env.VERIPAY_KEY!, "content-type": "application/json" },
body: JSON.stringify(req),
});
if (res.ok) return Buffer.from(await res.arrayBuffer());
const err = await res.json();
switch (err.error) {
case "RATE_LIMITED":
await sleep(300 + Math.random() * 500);
return downloadCep(req);
case "QUOTA_EXCEEDED":
throw new UpgradeRequiredError();
case "INFERENCE_FAILED":
return downloadCepExplicit(req);
case "NOT_FOUND":
return null;
default:
throw new Error(`${err.error}: ${err.message}`);
}
}
Nunca reintentes un 402 ni un 400 — el resultado no cambiará y consumirás cuota de error logging.