Errores y formato de respuesta
El sobre de respuesta estándar, el objeto de error y cómo resolver cada código que puede devolver el API.
Todas las respuestas del API comparten la misma estructura, sean de éxito o de error. Apréndela una vez y te sirve para todos los endpoints.
El sobre de respuesta
Las respuestas exitosas envuelven el resultado en data, con metadatos opcionales en meta:
{
"success": true,
"data": { "id": "clxq1w2e3r4t5y6u7i8o9p0a", "status": "DRAFT" },
"meta": { "page": 1, "limit": 20, "total": 47, "totalPages": 3 }
}
Los errores llevan success: false y un objeto error:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"errors": {
"contactName": ["contactName must be longer than or equal to 1 characters"],
"postalCode": ["Postal code must be 4-6 digits"]
}
},
"timestamp": "2026-07-17T12:00:00.000Z",
"requestId": "req_abc123"
}
}
| Campo | Descripción |
|---|---|
code |
Código estable y legible por máquinas — usa este para programar, no message |
message |
Descripción legible por humanos; puede cambiar sin previo aviso |
details |
Contexto adicional específico del error (campos inválidos, pistas, IDs) |
timestamp |
Cuándo ocurrió el error |
requestId |
Identificador de la petición — inclúyelo cuando contactes a soporte |
Todos los códigos de error
| HTTP | Código | Cuándo ocurre | Cómo resolverlo |
|---|---|---|---|
| 400 | VALIDATION_ERROR |
El cuerpo o los parámetros no pasan validación | Revisa details.errors: lista cada campo inválido y por qué |
| 400 | INVALID_INPUT |
Combinación inválida (p. ej. dirección por ID e inline a la vez) | Revisa details; envía exactamente una variante por campo. Algunos casos traen un código más específico en details.code |
| 400 | API_VERSION_UNSUPPORTED |
Pediste una versión del API que ya llegó a su fecha de retiro | Sube a la versión vigente — ver versionamiento |
| 401 | UNAUTHORIZED |
Falta la credencial o está mal formada | Envía tu llave en X-API-Key o Authorization: Bearer |
| 401 | INVALID_API_KEY |
La llave no existe o fue revocada | Verifica la llave; genera una nueva si fue revocada |
| 401 | EXPIRED_API_KEY |
La llave expiró | Genera una llave nueva y actualiza tu integración |
| 402 | INSUFFICIENT_BALANCE |
El monedero no alcanza para la operación | Fondea tu monedero y reintenta |
| 403 | FORBIDDEN |
Credencial válida sin permisos suficientes (rol u organización) | Usa una credencial del rol/organización correctos |
| 403 | INSUFFICIENT_SCOPE |
A la llave le faltan alcances (vienen listados en details) |
Agrega los alcances faltantes a la llave |
| 403 | IP_NOT_ALLOWED |
La IP de origen no está en la lista de la llave | Agrega la IP al ipAllowlist de la llave o llama desde una IP permitida |
| 403 | PLAN_LIMIT_REACHED |
Alcanzaste el tope de tu plan (llaves de API, endpoints de webhook o miembros) | Sube de plan; details trae resource, limit y plan — ver planes |
| 403 | SOLE_OWNER_OF_ORGANIZATION |
Intentas eliminar tu cuenta siendo el único OWNER de una organización de equipo | Transfiere la propiedad a otro miembro y reintenta |
| 404 | RESOURCE_NOT_FOUND |
El recurso no existe o no pertenece a tu organización | Verifica el ID y la organización de tu llave |
| 409 | SHIPMENT_ALREADY_PROCESSED |
El envío ya no está en un estado modificable | Consulta el status actual; solo los DRAFT se editan |
| 409 | IDEMPOTENCY_KEY_REUSED |
Reusaste una Idempotency-Key con otro cuerpo u otro endpoint | Usa una llave nueva para cada operación distinta |
| 409 | IDEMPOTENCY_KEY_IN_USE |
Una petición concurrente con la misma llave sigue en curso | Espera un momento y reintenta con la misma llave |
| 409 | SHIPMENT_LABEL_IN_PROGRESS |
Dos compras concurrentes sobre el mismo envío | Reintenta en 1–2 segundos |
| 410 | RATES_EXPIRED |
Las cotizaciones del envío expiraron (24 h) | GET /v1/shipments/:id/rates para refrescar y elige un nuevo rateId |
| 422 | CARRIER_NOT_SUPPORTED |
Pediste rastreo de una paquetería que SendIt no puede consultar | Usa DHL, FEDEX o ESTAFETA — ver rastreadores |
| 422 | CARRIER_CREDENTIALS_REQUIRED |
Esa paquetería necesita credenciales de cuenta para rastrear | Conecta tus credenciales — ver cuentas de paquetería |
| 422 | ONE_CALL_BUY_RATES_PENDING |
Las paqueterías tardaron más de 8 s en cotizar durante una compra en una llamada | Sondea ratesPollUrl y compra con el rateId |
| 422 | ONE_CALL_BUY_NO_MATCHING_RATE |
Ninguna tarifa coincidió con la selección de tu objeto purchase |
Elige una de details.availableRates |
| 429 | RATE_LIMIT_EXCEEDED |
Excediste el límite de peticiones | Respeta Retry-After y aplica backoff exponencial — ver límites |
| 500 | INTERNAL_ERROR |
Error del servidor | Reintenta; si persiste, contacta a soporte con el requestId |
| 502 | CARRIER_ERROR |
La paquetería falló al generar la guía | Cualquier cargo ya fue reembolsado; reintenta o elige otra tarifa |
| 503 | CHECKOUT_NOT_CONFIGURED |
El plan que pediste no tiene checkout disponible | Contacta a ventas para ese plan |
Maneja errores por código, no por mensaje
const res = await fetch(url, options);
const body = await res.json();
if (!body.success) {
switch (body.error.code) {
case "RATES_EXPIRED":
// refresca cotizaciones y reintenta
break;
case "INSUFFICIENT_BALANCE":
// notifica al equipo de operaciones
break;
default:
log.error(body.error.requestId, body.error.code);
}
}
Revisa también details.code
Algunas validaciones devuelven 400 INVALID_INPUT con un código más específico anidado en error.details.code. Léelo cuando necesites distinguir el caso concreto:
details.code |
Cuándo ocurre |
|---|---|
PRECONDITION_FAILED |
El If-Match de una orden no coincide con su etag actual |
ORDER_HAS_ACTIVE_LABELS |
Intentas cancelar una orden con guías vigentes |
SHIPMENTS_NOT_FOUND |
Un lote referencia envíos que no existen o no son tuyos |
PRODUCT_NOT_FOUND |
Un artículo apunta a un producto inexistente |
LINE_ITEM_INCOMPLETE |
Un artículo inline no trae name o unitPrice |