Catálogo de Errores
Todos los errores siguen el mismo formato de respuesta:
{
"success": false,
"error": {
"message": "Descripción legible del error",
"code": "CODIGO_ERROR"
}
}Códigos de error
| HTTP | Código | Cuándo ocurre |
|---|---|---|
400 | INVALID_JSON | El body no es JSON válido |
400 | VALIDATION_ERROR | Campos faltantes o con formato incorrecto |
422 | MUNICIPIO_NO_CUBIERTO | Ciudad de destino sin cobertura activa (ver GET /municipios) |
400 | INVALID_URL | URL de webhook usa HTTP, es localhost, o IP privada |
400 | LIMIT_EXCEEDED | Ya existen 10 webhooks activos para la empresa |
401 | AUTH_REQUIRED | Header Authorization ausente, mal formado, o token inválido |
402 | PAYMENT_REQUIRED | Sin créditos disponibles ni plan activo para crear guías |
404 | NOT_FOUND | Recurso no existe o pertenece a otra empresa |
409 | INVALID_STATE | Operación no permitida en el estado actual (ej: editar guía que no está en SIN_RECOLLECTAR) |
403 | SANDBOX_NOT_ALLOWED | Operación de escritura intentada con token test (usa token live) |
409 | ALREADY_INACTIVE | El webhook ya estaba desactivado |
409 | WEBHOOK_INACTIVE | Intento de test en webhook inactivo |
429 | RATE_LIMIT_EXCEEDED | 100 requests/minuto superados |
429 | DAILY_GUIDE_LIMIT | 500 guías/día superadas |
500 | INTERNAL_ERROR | Error interno del servidor |
Detalle por código
VALIDATION_ERROR (400)
El mensaje incluye el detalle de cada campo inválido:
{
"success": false,
"error": {
"message": "[{\"path\":[\"costoProducto\"],\"message\":\"costoProducto es requerido para guías COD.\"}]",
"code": "VALIDATION_ERROR"
}
}El message es un array JSON serializado con objetos { path, message } — parsea el string para obtener todos los errores de validación.
AUTH_REQUIRED (401)
{
"success": false,
"error": {
"message": "Token de autenticación requerido.",
"code": "AUTH_REQUIRED"
}
}Causas comunes:
- Header
Authorizationausente - Formato incorrecto (debe ser
Bearer <token>, con espacio) - Token revocado o inválido
PAYMENT_REQUIRED (402)
{
"success": false,
"error": {
"message": "Sin créditos disponibles. Adquiere más créditos en el portal.",
"code": "PAYMENT_REQUIRED"
}
}Solo aplica a POST /guias y POST /guias/batch. Adquiere créditos en el portal de clientes o contacta a TPC para habilitar un plan personalizado.
NOT_FOUND (404)
{
"success": false,
"error": {
"message": "Guía no encontrada.",
"code": "NOT_FOUND"
}
}Las guías y webhooks solo son visibles para la empresa que los creó. Un NOT_FOUND puede indicar que el recurso existe pero pertenece a otra empresa (no diferenciamos por seguridad).
INVALID_STATE (409)
{
"success": false,
"error": {
"message": "No se puede anular una guía con estado \"EN_RUTA\". Solo es posible anular guías en estado SIN_RECOLLECTAR.",
"code": "INVALID_STATE"
}
}Cuándo ocurre:
PATCH /guias/{id}— la guía no está enSIN_RECOLLECTARDELETE /guias/{id}— la guía no está enSIN_RECOLLECTAR
RATE_LIMIT_EXCEEDED (429)
{
"success": false,
"error": {
"message": "Rate limit excedido. Intenta en un minuto.",
"code": "RATE_LIMIT_EXCEEDED"
}
}Espera hasta que X-RateLimit-Reset expire. Ver Rate Limits para estrategias de manejo.
DAILY_GUIDE_LIMIT (429)
{
"success": false,
"error": {
"message": "Límite diario de guías alcanzado (500/día). Reinicia mañana.",
"code": "DAILY_GUIDE_LIMIT"
}
}Este límite se reinicia a medianoche hora Guatemala (UTC-6), no en minutos. No reintentes hasta el día siguiente. El límite diario es configurable por cuenta — contacta a TPC si necesitas aumentarlo.
Estrategia de reintentos
| HTTP | ¿Reintentar? | Estrategia |
|---|---|---|
400 | No | Corrige el request antes de reintentar |
401 | No | Verifica y/o rota tu token |
402 | No | Adquiere créditos en el portal |
404 | No | Verifica el numeroGuia o id |
409 | No | Revisa el estado del recurso con un GET |
429 RATE_LIMIT_EXCEEDED | Sí | Espera hasta X-RateLimit-Reset |
429 DAILY_GUIDE_LIMIT | No hoy | Espera al día siguiente (medianoche hora Guatemala) |
500 | Sí | Exponential backoff: 1s → 2s → 4s (máx 3 intentos) |
async function requestConReintentos(url, options, maxIntentos = 3) {
for (let intento = 0; intento < maxIntentos; intento++) {
const res = await fetch(url, options)
// Éxito
if (res.ok) return res
const body = await res.clone().json().catch(() => ({}))
// No reintentar en errores del cliente
if (res.status < 500 && res.status !== 429) {
throw new Error(body.error?.message ?? `HTTP ${res.status}`)
}
// Rate limit: esperar hasta reset
if (res.status === 429 && body.error?.code === 'RATE_LIMIT_EXCEEDED') {
const resetAt = parseInt(res.headers.get('X-RateLimit-Reset') ?? '0')
const waitMs = Math.max((resetAt * 1000) - Date.now() + 500, 1000)
if (intento < maxIntentos - 1) {
await new Promise(r => setTimeout(r, waitMs))
continue
}
}
// Error 5xx: exponential backoff
if (res.status >= 500 && intento < maxIntentos - 1) {
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, intento)))
continue
}
throw new Error(body.error?.message ?? `HTTP ${res.status}`)
}
}Headers de diagnóstico
Todas las respuestas incluyen:
X-Request-Id: f47ac10b-58cc-4372-a567-0e02b2c3d479Incluye el valor de X-Request-Id al contactar a soporte para diagnóstico inmediato.