Enviadores Public API
API REST pública de Enviadores: cotiza, genera guías, rastrea y cancela envíos.
Versión 1.2.0 · OpenAPI 3.1.0 · Referencia de endpoints →
API REST server-to-server para cotizar, generar guías, rastrear y cancelar envíos. Autentícate con una clave secreta que administras desde Mi cuenta → Claves de API (disponible para cuentas de administrador y de cliente). ¿Primera vez? Empieza por la guía para desarrolladores.
Inicio rápido
# Ensaya con ek_test_ (sandbox). Con ek_live_ esto genera una guía real y cobra créditos.# 0. Verifica tu clavecurl https://api.enviadores.com.mx/api/v1/balance -H "Authorization: Bearer ek_test_TU_CLAVE"# 1. Cotiza (todas las paqueterías)curl https://api.enviadores.com.mx/api/v1/rates \ -H "Authorization: Bearer ek_test_TU_CLAVE" \ -H "Content-Type: application/json" \ -d '{ "from": { "postal_code": "01000" }, "to": { "postal_code": "64000" }, "package": { "weight_kg": 1, "length_cm": 20, "width_cm": 15, "height_cm": 10 } }'# → { "services": [ { "id": "e2etest_E2ETestCarrier_E2EStandard_ab12cd34",# "service_name": "E2E Standard",# "pricing": { "total_price": 145.50, ... } }, ... ],# "rate_id_expires_in_seconds": 1800 }# (el id es OPACO: cópialo tal cual, no interpretes ni valides su formato)# 2. Crea el envío — pega en rate_id el id del paso 1 (Idempotency-Key fresco por envío)curl https://api.enviadores.com.mx/api/v1/shipments \ -H "Authorization: Bearer ek_test_TU_CLAVE" \ -H "Idempotency-Key: 6f9c2b1e-8d4a-4f3b-9c2e-1a7b8d4f3c2e" \ -H "Content-Type: application/json" \ -d '{ "rate_id": "PEGA_AQUI_EL_ID_DEL_PASO_1", "from": { "name": "Juan Pérez", "phone": "5512345678", "street": "Av. Insurgentes Sur", "number": "600", "colonia": "Del Valle", "city": "Ciudad de México", "state": "CDMX", "postal_code": "01000" }, "to": { "name": "María López", "phone": "8187654321", "street": "Av. Constitución", "number": "400", "colonia": "Centro", "city": "Monterrey", "state": "Nuevo León", "postal_code": "64000" }, "package": { "weight_kg": 1 }, "contenido": "Ropa" }'# 3. Descarga la etiqueta · 4. Cancela si hace faltacurl -L https://api.enviadores.com.mx/api/v1/labels/ENVIO_ID -H "Authorization: Bearer ek_test_TU_CLAVE" -o etiqueta.pdfcurl https://api.enviadores.com.mx/api/v1/cancellations -H "Authorization: Bearer ek_test_TU_CLAVE" \ -H "Content-Type: application/json" -d '{ "shipment_id": "ENVIO_ID" }'El rate_id no lleva direcciones ni paquete: en el paso 2 reenvía from, to y package con los MISMOS datos que cotizaste, con la MISMA llave y el mismo X-PDV-ID.
| Si entre la cotización y el envío… | Respuesta |
|---|---|
cambia el package | 409 RATE_PACKAGE_MISMATCH |
| cambia el CP de origen o de destino | 409 RATE_ADDRESS_MISMATCH |
| usas otra llave | 409 RATE_NOT_FOUND |
usas otro X-PDV-ID | 409 RATE_PDV_MISMATCH |
| pasan 30 minutos | 409 RATE_EXPIRED |
Autenticación
Manda tu clave en el header Authorization de cada petición:
Authorization: Bearer ek_live_TU_CLAVE| Prefijo | Modo |
|---|---|
ek_live_… | Producción: genera guías reales y cobra créditos. |
ek_test_… | Modo de prueba (sandbox) — ver Modo de prueba. |
- Las claves son secretas y solo para uso server-to-server: nunca las pongas en un navegador ni en una app móvil. Esta superficie no habilita CORS por diseño.
- Nunca envíes la clave por query string.
- Una clave desconocida, revocada o ausente responde
401 UNAUTHORIZEDde forma idéntica (sin revelar cuál caso fue).
Formato de respuesta
Toda respuesta JSON usa un sobre estable:
{
"success": true,
"data": { … },
"requestId": "req_…"
}{
"success": false,
"error": { "code": "…", "message": "…", "details": { … } },
"requestId": "req_…"
}error.detailses opcional.- El
requestIdtambién viaja en el headerX-Request-Id; inclúyelo al reportar un problema. - Los códigos de
error.codeson estables y aptos para lógica de máquina — ver Errores y reintentos.
Límites de tasa
El límite es por clave (token bucket):
| Nivel | Sostenido (rpm) | Ráfaga |
|---|---|---|
| Live standard | 60 | 120 |
| Live elevated | 240 | 480 |
| Claves de prueba | 30 | 60 |
| Header | Cuándo llega |
|---|---|
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset | En cada respuesta. |
Retry-After | En un 429 RATE_LIMITED: segundos a esperar, también en details.retry_after_ms. |
Ante un 429, respeta Retry-After con backoff exponencial.
Idempotencia
Manda un header Idempotency-Key en POST /shipments: un UUID fresco por operación lógica, de 1–128 caracteres ASCII imprimibles. La clave queda ligada al cuerpo exacto de la primera solicitud y al X-PDV-ID con que se envió.
| Si reintentas… | Respuesta | Qué hacer |
|---|---|---|
| con la misma clave y el mismo cuerpo, dentro de 15 minutos | La respuesta original, con el header Idempotent-Replay: true. Sin doble cargo. | Nada. |
con otro cuerpo (u otro X-PDV-ID) | 409 IDEMPOTENCY_KEY_PAYLOAD_MISMATCH | Manda una clave nueva. |
| pasados los 15 minutos | 409 IDEMPOTENCY_KEY_EXPIRED | Manda una clave nueva. |
| con una clave que ya usaste en la app web (o al revés) | 409 IDEMPOTENCY_KEY_REUSED | Manda una clave nueva. Desde 2026-08 las claves de esta API ya no comparten espacio de nombres con el carril web de la misma cuenta. |
Ante un POST_COMMIT_ERROR (500), no reintentes con una llave nueva
GET /shipments para recuperar la guía.Catálogo de servicios bloqueado
Algunas cuentas empiezan con el catálogo limitado: pueden ver todas las tarifas, pero solo pueden generar guías de:
- iMile Terrestre
- Estafeta Terrestre y Estafeta Día Siguiente sin recolección (en estas dos el remitente deja el paquete en una sucursal Estafeta y cualquier cargo extra —sobrepeso, servicios adicionales— se paga ahí).
Todo lo demás llega bloqueado, incluidos los servicios sin recolección de otros transportistas. Aplica a cuentas de registro propio que aún no verifican su identidad. No aplica a cuentas creadas por nuestro equipo.
Cómo detectarlo ANTES de crear el envío
- Cada tarifa de
POST /ratestraebooking_locked(booleano). Si estrue, esa tarifa NO es reservable por esta cuenta yPOST /shipmentsresponderá403 SERVICE_UNLOCK_REQUIRED. - Cuando el catálogo está limitado, la respuesta también trae
unlock_notice: un texto en español, listo para mostrarle al usuario, que explica cómo desbloquearlo. - Filtra por
booking_locked === falseantes de ofrecer opciones y nunca verás este error.
Cómo se desbloquea
Cualquiera de las dos, sin trámite con nosotros:
- verificar la identidad (INE) desde Mi cuenta, o
- acumular $2,500 MXN en recargas.
Al cumplirse, el catálogo completo queda disponible de inmediato y booking_locked pasa a false en la siguiente cotización.
El modo de prueba NO simula esta restricción.
booking_locked siempre llega en false y toda creación de envío funciona. Si tu integración se probó solo en sandbox, valida el catálogo de la cuenta con una cotización real (llave ek_live_) antes de salir a producción: cotizar no cuesta ni cobra.Bultos, seguro y valor declarado
Una caja o varias
Tanto en POST /rates como en POST /shipments, manda uno de los dos — nunca ambos (422):
| Campo | Para |
|---|---|
package | una caja |
packages | 2–10 cajas que viajan como UN solo envío |
- Una cotización con varios bultos solo devuelve servicios que pueden llevar varias cajas juntas.
- El peso facturable es la SUMA de las cajas.
- La petición de creación debe llevar la MISMA lista que cotizaste; una lista distinta responde
409 RATE_PACKAGE_MISMATCH. - Los códigos postales deben existir en el catálogo SEPOMEX: un CP inexistente (p. ej.
99999) responde 422 nombrandofrom.postal_code/to.postal_codeantes de consultar a ninguna paquetería.
Medidas de la caja
Cada caja lleva su peso y el largo, ancho y alto exteriores de la caja ya cerrada:
| Campo | Qué es |
|---|---|
weight_kg | Peso de la caja. |
length_cm, width_cm, height_cm | Largo, ancho y alto exteriores de la caja ya cerrada. |
Sin las tres medidas:
| Endpoint | Resultado |
|---|---|
POST /rates | Se calcula para una caja de 10×10×10 cm y regresa con size_estimated: true y un size_notice. |
POST /shipments | Se rechaza con 422 PACKAGE_DIMENSIONS_REQUIRED (no se cobra nada). |
Es decir: sin las tres medidas el precio es estimado y no se puede generar la guía.
Vista agrupada
POST /rates?view=grouped lista cada servicio de envío distinto UNA sola vez (paquetería + nivel de servicio, p. ej. DHL · Express) con su rango de precios (price_from–price_to) y sus options[] reservables:
- Primero las que esta cuenta puede reservar (
booking_locked:false), cada mitad de la más barata a la más cara. - Reserva
options[0].rate_idsalvo que el usuario pida otra cosa. - La forma plana por omisión no cambia.
Seguro vs. valor declarado — dos cosas distintas
| Campo | Qué es | Al crear el envío |
|---|---|---|
insurance.insured_value | Contrata cobertura para el envío. Cuando la póliza de la plataforma no está disponible se usa la cobertura de la propia paquetería y el precio mostrado ya la incluye (cada tarifa indica insurance_included / insurance_not_supported). | Omite insurance para aceptar la cobertura que cotizaste, o manda el mismo valor. Un valor distinto explícito responde 422 INSURANCE_MISMATCH (vuelve a cotizar — el total firmado nunca se recalcula); una tarifa cuya paquetería no ofrece cobertura responde 422 INSURANCE_NOT_AVAILABLE. |
declared_valuePOST /shipments | El valor de la mercancía declarado a la paquetería. No es una póliza, pero la paquetería puede cobrarlo. | Debe ser igual al valor con el que se cotizó la tarifa: un valor que la cotización no llevaba responde 422 DECLARED_VALUE_MISMATCH; solo un servicio que lo pide después de cotizar — 422 DECLARED_VALUE_REQUIRED — acepta uno nuevo. Si lo omites no se inventa ningún valor. |
Ejemplos de respuesta
Cuerpos de ejemplo (recortados, datos ficticios) del sobre de éxito de los endpoints principales. El id de una tarifa es OPACO: cópialo tal cual a POST /shipments — su formato no es parte del contrato y no debes interpretarlo ni validarlo (cambia entre el carril de prueba y el live).
Errores y reintentos
Todos los errores usan el sobre error.code estable. error.details lleva el contexto por código — la lista de campos en un 422, o los montos en un 402:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": { "fields": ["from.postal_code: must be a 5-digit Mexican postal code"] }
},
"requestId": "req_b0c1d2e3f4"
}{
"success": false,
"error": {
"code": "PACKAGE_DIMENSIONS_REQUIRED",
"message": "Indica largo, ancho y alto de la caja ya cerrada (length_cm, width_cm, height_cm) y cotiza de nuevo.",
"details": { "fields": ["package.length_cm", "package.width_cm", "package.height_cm"] }
},
"requestId": "req_d2e3f4a5b6"
}{
"success": false,
"error": {
"code": "CREDIT_ERROR",
"message": "Créditos insuficientes. Necesitas $145.50 MXN. Disponible: $80.00 MXN",
"details": { "required": 145.5, "available": 80, "shortfall": 65.5 }
},
"requestId": "req_c1d2e3f4a5"
}Referencia rápida
Si conviene reintentar y qué hacer con cada código:
| Código | HTTP | ¿Reintentar? | Qué hacer |
|---|---|---|---|
UNAUTHORIZED | 401 | No | Revisa la llave (Bearer ek_live_/ek_test_); reintentar igual no ayuda. |
INSUFFICIENT_SCOPE | 403 | No | La llave no porta el scope requerido; usa una con permisos. |
PDV_NOT_ALLOWED | 403 | No | El X-PDV-ID no está en la allowlist de la llave (denegado por defecto). |
SERVICE_UNLOCK_REQUIRED | 403 | No | Por ahora la cuenta solo puede reservar iMile Terrestre, y Estafeta Terrestre y Día Siguiente sin recolección. Re-cotiza y elige una tarifa con booking_locked:false. Se desbloquea al verificar identidad (INE) o al acumular $2,500 MXN en recargas — ver Catálogo de servicios bloqueado. |
NOT_FOUND | 404 | No | El recurso no existe o no pertenece a tu llave. |
VALIDATION_ERROR | 422 | No | Corrige los campos listados en details.fields. |
PACKAGE_DIMENSIONS_REQUIRED | 422 | Re-cotiza | Falta largo, ancho o alto de alguna caja (details.fields las nombra). Pide las medidas de la caja ya cerrada y cotiza de nuevo: el precio depende de ellas. No se cobró nada. |
INSURANCE_VALUE_TOO_HIGH | 422 | No | El valor declarado supera el máximo asegurable; redúcelo. |
RATE_NOT_FOUND | 409 | Re-cotiza | El rate_id no existe para esta llave; vuelve a POST /rates. |
RATE_EXPIRED | 409 | Re-cotiza | Pasaron los 30 min; vuelve a cotizar. |
RATE_TAMPERED | 409 | Re-cotiza | Nunca alteres el rate_id; úsalo tal cual lo devolvió /rates. |
RATE_PDV_MISMATCH | 409 | Re-cotiza | Cotiza y envía bajo el mismo X-PDV-ID. |
RATE_PACKAGE_MISMATCH | 409 | Re-cotiza | Reenvía el mismo package que cotizaste, o vuelve a cotizar. |
RATE_ADDRESS_MISMATCH | 409 | Re-cotiza | Los CP de origen/destino del envío no son los que se cotizaron; cotiza de nuevo con las direcciones definitivas. |
REQUOTE_REQUIRED | 409 | Re-cotiza | El precio no coincide con el de la cotización; vuelve a cotizar. No se espera en esta API (el total se toma de la misma fila firmada que verifica el servidor). |
GUIA_COLLISION | 409 | No | Colisión de guía con otra cuenta (anomalía); el cargo se revirtió — contacta soporte. |
IDEMPOTENCY_KEY_REUSED | 409 | No | La Idempotency-Key ya se usó en otro endpoint (desde 2026-08 el carril web cuenta como otro endpoint); genera una llave nueva. |
IDEMPOTENCY_KEY_PAYLOAD_MISMATCH | 409 | No | La misma llave se mandó con un cuerpo (o X-PDV-ID) distinto al de la primera solicitud; genera una llave nueva por operación. |
IDEMPOTENCY_KEY_EXPIRED | 409 | No | La operación con esa llave se completó hace más de 15 min; consulta GET /shipments o manda una llave nueva. |
IDEMPOTENCY_KEY_INVALID | 400 | No | El header Idempotency-Key llegó vacío, con más de 128 chars o con caracteres no imprimibles. Omitirlo es válido; mandarlo mal no. |
CREDIT_ERROR | 402 / 500 | Sí | 402 = saldo insuficiente (details: required, available, shortfall): recarga y reintenta con el MISMO Idempotency-Key. 500 = falla interna de créditos: reintenta con backoff. |
RISK_LIMIT | 429 | No | Tope diario alcanzado (de la cuenta o de la llave); espera la ventana de 24 h. |
RATE_LIMITED | 429 | Sí | Backoff exponencial respetando Retry-After. |
VENDOR_ERROR | 4xx/5xx | Depende | La paquetería falló ANTES del commit; el cargo se revirtió (details.refunded). El status refleja el de la paquetería: 502/503 = falla de transporte → reintenta con backoff; 422/400 = rechazo determinista (dirección, límites del servicio) → corrige antes de reintentar. |
SHIPMENT_ERROR | 500 | Sí | El cargo se revirtió; reintenta. |
ADDRESS_ERROR | 500 | Sí | Falló el registro de direcciones; reintenta. |
POST_COMMIT_ERROR | 500 | No | El envío ES real; NO reintentes con llave nueva — consulta GET /shipments. Con la MISMA Idempotency-Key el reintento repite este 500 (nunca crea un segundo envío) y pasados 15 min responde 409 IDEMPOTENCY_KEY_EXPIRED. |
SERVER_ERROR / RATES_ERROR | 500 | Sí | Error transitorio; reintenta con backoff. |
OpenAPI y SDKs
El documento OpenAPI 3.1 se sirve en https://api.enviadores.com.mx/api/v1/openapi.json. Genera un cliente PHP/Node/Python con openapi-generator:
npx @openapitools/openapi-generator-cli generate \
-i https://api.enviadores.com.mx/api/v1/openapi.json \
-g php -o ./enviadores-phpCambia -g php por -g typescript-fetch o -g python para otros lenguajes. También puedes importar el spec como colección de Postman.
Webhooks
En lugar de consultar GET /shipments o GET /tracking/{guia} una y otra vez (polling), registra un endpoint https:// público y recibe un POST firmado por cada evento. Hasta 5 webhooks activos por cuenta (por modo).
Eventos
| Evento | Cuándo | data |
|---|---|---|
shipment.created | Guía comprada (cargo confirmado). | {shipment} |
shipment.collected | La paquetería recogió el paquete. | {shipment} |
shipment.in_transit | Primer escaneo en tránsito. | {shipment} |
shipment.out_for_delivery | En reparto. | {shipment} |
shipment.delivered | Entregado. | {shipment} |
shipment.exception | La paquetería reportó una incidencia. | {shipment} |
shipment.returned | Devuelto al remitente. | {shipment} |
shipment.cancelled | La cancelación quedó en firme. | {shipment} |
pickup.resolved | Nuestro equipo cerró una solicitud de recolección (agendada o fallida). | {pickup} |
Todos los eventos usan este sobre; solo cambian event y data.
Registro
- Registra tu endpoint y los eventos que quieres recibir:cURL
curl -X POST https://api.enviadores.com.mx/api/v1/webhooks \ -H "Authorization: Bearer ek_live_..." \ -H "Content-Type: application/json" \ -d '{"url":"https://hooks.mitienda.com/enviadores","events":["shipment.created","shipment.delivered","shipment.exception"]}'# 201 → data.webhook.id = "wh_…", data.secret = "whsec_…" (se muestra UNA sola vez) - Guarda el
secretde la respuesta 201. Se devuelve una sola vez: se deriva, no se almacena y no se puede volver a mostrar. - Prueba la conexión:
POST /webhooks/{id}/testenvía unpingfirmado de inmediato y reporta si tu endpoint respondió 2xx.
Verifica la firma
Cada entrega trae tres headers:
| Header | Contenido |
|---|---|
X-Enviadores-Signature | t=<unix>,v1=<hex>, donde v1 = HMAC-SHA256(secret, "<t>.<raw body>") |
X-Enviadores-Event | El nombre del evento. |
X-Enviadores-Delivery | El id del evento: estable entre reintentos, úsalo para deduplicar. |
Para aceptar una entrega:
- Recomputa el HMAC-SHA256 con tu
secretsobre<t>.<raw body>: los bytes crudos del cuerpo, tal como llegaron. - Compáralo con
v1en tiempo constante. - Rechaza un
tcon más de 5 minutos de antigüedad.
// Node.js — verifica X-Enviadores-Signature sobre el cuerpo CRUDOimport { createHmac, timingSafeEqual } from 'node:crypto';export function verifyEnviadores(rawBody, signatureHeader, secret, toleranceSeconds = 300) { const parts = Object.fromEntries(signatureHeader.split(',').map((kv) => kv.split('='))); const t = Number(parts.t); if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false; const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const given = Buffer.from(parts.v1 ?? '', 'hex'); return given.length === 32 && timingSafeEqual(Buffer.from(expected, 'hex'), given);}Reglas de entrega
| Regla | Detalle |
|---|---|
| Respuesta | Responde 2xx en menos de 10 s y haz el trabajo después de responder. Nunca se siguen redirecciones (un 3xx cuenta como fallo). |
| Reintentos | Las entregas fallidas se reintentan con espera creciente: 1 min, 5 min, 30 min, 2 h, 12 h; después el evento se descarta. |
| Desactivación | Tras 20 fallos consecutivos el webhook se desactiva (status: disabled, disabled_reason): bórralo y regístralo de nuevo. |
| Llave revocada | Un webhook muere con la llave que lo registró: al revocar esa llave de API queda en status: disabled con disabled_reason: key_revoked y se descartan sus eventos en cola, así una llave filtrada no puede mantener vivo un receptor (al reautorizar un conector OAuth, su llave anterior se retira de la misma manera). |
| Cuerpo | {id, event, mode, created_at, data}; data.shipment / data.pickup tienen exactamente la forma de GET /shipments/{id} / GET /pickups/{id}. Los eventos de un envío llegan a la cuenta que lo creó. |
| Repeticiones | Los estatus no terminales pueden repetirse legítimamente (exception → in_transit → exception emite dos shipment.exception), mientras que la misma transición observada se entrega una sola vez; delivered, returned y cancelled se entregan como máximo una vez por envío. |
| Sandbox | Las llaves ek_test_ administran webhooks de sandbox, que solo reciben shipment.created / shipment.cancelled del carril de prueba y el ping (el sandbox no tiene rastreo de paquetería y nunca resuelve recolecciones). |
| Scopes | webhooks:write (registrar, probar, borrar) y webhooks:read (listar, consultar). Las llaves creadas con el conjunto de permisos por omisión los adquieren solas (conectores OAuth incluidos). |
Qué URL se acepta
Política de URL (SSRF):
- Solo
https://, en el puerto443o8443. - Con nombre de host público: sin
localhost,.local,.internal, IPs privadas o de loopback, hosts que resuelvan a ellas, ni el propio API de Enviadores. - Sin credenciales incrustadas.
- El ping de prueba reporta una clase de error general (
timeout,connection failed,non-2xx status), nunca el detalle de red.
Modo de prueba (sandbox)
Las claves ek_test_ autentican y consumen los mismos scopes y límites que las live, pero operan en un carril de persistencia paralela: escriben y leen en tablas de sandbox, nunca tocan las tablas de dinero de producción y NO mueven dinero real. Cada endpoint transaccional devuelve el MISMO esquema JSON que el carril live.
| Saldo | Saldo de prueba computado = $10,000 MXN por clave, menos el gasto de envíos de prueba no cancelados. |
| Guías | Con namespace SBX, disjuntas de todo formato real. |
| Cancelación | Síncrona y terminal (sin flujo de staff): el reembolso es inmediato. |
| Rastreo | NO se simula: GET /tracking resuelve solo guías reales. |
| Controles de riesgo | NO se simulan (el sandbox no mueve dinero): booking_locked siempre llega en false y toda creación funciona, aunque la cuenta tenga el catálogo limitado en producción. Verifícalo con una cotización live antes de salir — ver Catálogo de servicios bloqueado. |
| Retención | Los envíos de prueba se depuran a los 30 días. |
Fallas forzadas
Manda metadata.test_scenario en POST /shipments con uno de estos valores:
| test_scenario | Resultado |
|---|---|
insufficient_funds | 402 CREDIT_ERROR con details {required, available, shortfall}, sin importar el saldo real (sintetizado — condición de crédito). |
rate_expired | 409 RATE_EXPIRED (sintetizado — condición a nivel de validación). |
vendor_error | 422 VENDOR_ERROR con retryable: false — enrutado por el proveedor de prueba y aplanado EXACTAMENTE como ShipmentExecutor (código literal VENDOR_ERROR, status HTTP de la subclase, retryable solo en fallas de red). |
Cambios
Los cambios dentro de v1 serán aditivos; un cambio incompatible se anunciará aquí y saldría como /v2.
- 2026-11-12
Spec 1.2.0Solo documentación
Sin cambios de comportamiento. El documento OpenAPI declara ahora cada evento de webhook enwebhooks(OpenAPI 3.1) con su esquema y un ejemplo completo, así que los generadores de clientes y los agentes de IA ven el cuerpo exacto que recibirá tu endpoint. La referencia se mudó a su propia página, /desarrolladores/referencia, y cada endpoint trae un ejemplo listo para copiar en cURL, JavaScript, Python y PHP. Ver Webhooks. - 2026-11-04
Medidas de la caja
POST /shipmentsexige largo, ancho y alto de cada caja: sin ellos responde422 PACKAGE_DIMENSIONS_REQUIRED(antes se generaba la guía para una caja de 10×10×10 cm).POST /ratessigue cotizando sin medidas, pero marca la respuesta consize_estimated: trueysize_notice(campos nuevos, aditivos). En la vista agrupada,options[]pone primero las opciones reservables, así queoptions[0]nunca es una tarifa bloqueada si el servicio tiene alguna abierta. Las cuentas con catálogo limitado solo pueden reservar iMile Terrestre, y Estafeta Terrestre y Día Siguiente sin recolección. Ver Bultos, seguro y valor declarado. - 2026-09-21
Etiquetas servidas desde EnviadoresCompatible
GET /labels/{id}transmite nuestra copia y ya no redirige (302) a la URL de la paquetería. Con?format=json(herramienta MCPget_label),label_urles un enlace firmado a/v1/label-files/…que se abre sin llave y vence en el nuevo campoexpires_at(30 días); pide uno nuevo con la misma llamada.label_urlsigue siendo una URL de PDF. - 2026-09-04
Varios bultos, vista agrupada y seguroAditivo
Ahora se aceptapackages(2–10 cajas, un solo envío) enPOST /ratesyPOST /shipments(antes respondía 422);POST /rates?view=groupeddevuelve una entrada por servicio distinto con su rango de precios y sus opciones reservables; el campo de seguro de la cotización ahora esinsurance.insured_value(declared_valuedentro deinsurancequeda como alias obsoleto) yPOST /shipmentsaceptainsurancey undeclared_valueraíz aparte (declaración a la paquetería). Los códigos postales deben existir en SEPOMEX en ambos endpoints. Ver Bultos, seguro y valor declarado. - 2026-09-04
WebhooksAditivo
Nuevos endpointsPOST/GET /webhooks,GET/DELETE /webhooks/{id}yPOST /webhooks/{id}/test, con eventos de envíos y recolecciones firmados con HMAC-SHA256 (X-Enviadores-Signature), reintentos con espera creciente y desactivación automática. Nuevos scopeswebhooks:write/webhooks:read(las llaves con permisos por omisión los adquieren solas). Ver Webhooks. - 2026-08-30
Catálogo limitado y booking_lockedAditivo
Nuevo campobooking_lockeden cada tarifa dePOST /rates, yunlock_noticea nivel de la respuesta. Las cuentas de registro propio sin identidad verificada ven todas las tarifas pero solo pueden generar guías de Estafeta Terrestre y Día Siguiente sin recolección, e iMile Terrestre (desde el 2026-11-04; antes, cualquier servicio sin recolección); reservar una tarifa bloqueada responde403 SERVICE_UNLOCK_REQUIRED. Las integraciones existentes no se rompen — en cuentas sin restricciónbooking_lockedsiempre esfalseyunlock_noticeesnull. Ver Catálogo de servicios bloqueado. - 2026-07-28
Dimensiones guardadas con el envío
Las dimensiones declaradas (largo/ancho/alto) ahora se guardan con el envío. Antes solo se usaban para calcular el peso volumétrico y se descartaban, por lo que en envíos anteriores a esta fechapackages[].length_cmy sus pares llegan ennull. El peso sí está disponible en todo el histórico. - 2026-07-27
Lectura de envíosCambio de contrato
/shipmentsacepta?guia=,?status=y?from=/?to=; cada envío incluye ahora lo capturado al crearlo (packages,billable_weight_kg,declared_value,content) y elsender. Nuevo scope ampliadoshipments:read:pdvpara listar los envíos de todo un PDV conX-PDV-ID. La búsqueda?q=del directorio ahora abarca teléfono, email y dirección, con coincidencia por palabra. Cambio de contrato:X-PDV-IDya se valida en las rutas de envíos y cancelaciones — un PDV fuera de la allowlist responde403en lugar de ignorarse. - 2026-07-13
Respuesta de /rates saneadaBreaking pre-adopción
v1: respuesta de/ratessaneada a snake_case; rechazo explícito de country≠MX / campos desconocidos (y depackages, aceptado desde 2026-09-04); tope de gasto opcional por llave; allowlist de PDV denegada por defecto. - 2026-07-12
Lanzamiento de la API pública v1
MCP — conecta un agente de IA
Enviadores incluye un servidor Model Context Protocol nativo: Claude, Cursor, VS Code o cualquier cliente MCP puede cotizar, comprar guías, rastrear y cancelar envíos directamente, con las mismas llaves, alcances y límites que el API REST. HTTP con streaming (Streamable HTTP), sin estado, un mensaje JSON-RPC por POST.
Por qué usarlo
| Un agente no puede gastar de más, por diseño | Cada compra se ejecuta contra tu saldo prepagado más los límites de gasto de 24 horas que fija Enviadores, aplicados en nuestros servidores: no hay tarjeta que sobregirar ni forma de quedar en negativo, haga lo que haga el agente. |
| Los reintentos nunca cobran doble | create_shipment es idempotente: el servidor genera y devuelve una idempotency_key cuando el agente no manda una, y rechaza las mal formadas en lugar de adivinar. |
| Paquetería mexicana de verdad | Cotizaciones multi-paquetería, guías y rastreo con direcciones validadas contra SEPOMEX (a nivel colonia), en el idioma del propio agente. |
| Cero superficie nueva en la que confiar | Mismas llaves, mismos alcances y mismos límites que el API REST: una llave con alcance acotado puede darle a un agente poder para cotizar y rastrear sin capacidad de comprar. Sandbox gratis (ek_test_) para probar todo sin dinero real; las llaves live (ek_live_) requieren verificación de identidad en la cuenta. |
Conexión
claude mcp add --transport http enviadores https://api.enviadores.com.mx/api/v1/mcp \ --header "Authorization: Bearer ek_test_..."{ "mcpServers": { "enviadores": { "url": "https://api.enviadores.com.mx/api/v1/mcp", "headers": { "Authorization": "Bearer ek_test_..." } } }}curl -X POST https://api.enviadores.com.mx/api/v1/mcp \ -H "Authorization: Bearer ek_test_..." \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'- Empieza con una llave
ek_test_: corre en el sandbox — ensayo completo, paqueterías simuladas, sin dinero real. Administra tus llaves en Mi cuenta → Claves de API. - Consejo: guarda direcciones una vez con
create_sender/create_recipienty envía porfrom_id/to_id— cargas más pequeñas y sin volver a teclear direcciones.
Herramientas
| Acceso | Significa |
|---|---|
| Lectura | Consulta datos, no cambia nada. |
| Escritura | Crea, cambia o elimina datos de la cuenta. |
| Escritura sensible | Gasta dinero, tiene un efecto irreversible o en el mundo real, o envía datos de la cuenta a un tercero; el asistente debe pedir tu aprobación cada vez. |
| Herramienta | Acceso | Qué hace |
|---|---|---|
validate_postal_code | Lectura | Valida un CP contra el catálogo oficial de SEPOMEX (estado, municipio, colonias). |
get_shipping_rates | Lectura | Cotiza un envío nacional con todas las paqueterías; cada servicio una sola vez con su rango de precios y sus opciones reservables (rate_ids, expiran en 30 min). |
create_shipment | Escritura sensible | Compra la guía de un rate_id cotizado. La única herramienta que gasta dinero — idempotente por contrato. |
list_shipments | Lectura | Lista los envíos de la cuenta con filtros (guía, estatus, fechas). |
track_shipment | Lectura | Rastrea cualquier guía; estatus canónico más historial. |
get_label | Lectura | Obtiene la URL del PDF de la guía de uno de tus envíos. |
cancel_shipment | Escritura sensible | Solicita la cancelación (puede ser asíncrona según la paquetería). |
get_balance | Lectura | Saldo prepagado en MXN — el techo duro de gasto. |
list_senders | Lectura | Lista los remitentes guardados; sus ids sirven como from_id — sin volver a teclear direcciones. |
create_sender | Escritura | Guarda un remitente una vez y envía por id después (deduplica por mismo contenido, sin duplicados). |
list_recipients | Lectura | Lista los destinatarios guardados (por remitente); sus ids sirven como to_id. |
create_recipient | Escritura | Guarda un destinatario bajo un remitente; usa su id como to_id. |
import_contacts | Escritura | Importa remitentes/destinatarios en lote desde CSV o texto libre; dry_run por omisión. |
schedule_pickupsandbox | Escritura sensible | Solicita la recolección de una paquetería en la dirección del remitente para un envío ya creado. Es una solicitud, no una reserva: nuestro equipo la confirma y avisa el resultado por correo. |
get_pickupsandbox | Lectura | Consulta una solicitud de recolección: su estado y, ya acordada, la ventana confirmada. |
register_webhook | Escritura sensible | Registra un endpoint https para eventos de envíos y recolecciones (sin polling). El secreto de firma se devuelve una sola vez. |
list_webhooks | Lectura | Lista los webhooks de la cuenta con estado, fallos consecutivos y última entrega. |
test_webhook | Escritura | Envía un ping firmado al webhook ahora mismo y reporta la respuesta del endpoint. |
delete_webhook | Escritura | Elimina un webhook; los eventos en cola se descartan. |
list_pickupssandbox | Lectura | Lista tus solicitudes de recolección con su estado — sin recordar el id de cada una. |
whoami | Lectura | Identifica la conexión: cuenta, modo (pruebas o producción), permisos de la llave, saldo y capacidades. |
list_transactions | Lectura | Lista los movimientos de saldo (cargos, reembolsos, recargas, ajustes) con importe con signo. |
get_shipment | Lectura | Consulta un envío por su id: estatus, guía, paquetería, servicio, destino, paquete y precio. |
Qué es este servidor — y qué no
Preferimos que conozcas la categoría antes de construir encima: este es un servidor MCP enfocado y sin estado — herramientas de petición/respuesta sobre Streamable HTTP, hecho a propósito para operar envíos. No es una plataforma general de agentes.
| Sin streaming ni sesiones | Un mensaje JSON-RPC por POST, respuestas JSON simples, sin SSE. Correcto para llamadas de cotizar/guía/rastrear; incorrecto para cargas de trabajo de larga duración en flujo continuo. |
| Llaves de API y OAuth 2.1 | Los clientes con encabezado (Claude Code, Cursor, VS Code) se autentican con una llave ek_; los conectores web de claude.ai (agrega la URL del servidor) y el plugin oficial de ChatGPT (instálalo desde el directorio de plugins de ChatGPT) se conectan con el flujo OAuth integrado: inicia sesión con tu cuenta de Enviadores y autoriza en sandbox o producción. Cada autorización OAuth equivale a una llave de API con alcance acotado, así que aplican los mismos límites y controles de gasto. |
| 23 herramientas, envíos nacionales (MX) en el API estándar | Todas disponibles con llaves de producción. El envío internacional existe en la plataforma para cuentas con convenio de fulfillment o de socio — es una conversación, no un no rotundo. |
| Las recolecciones se solicitan, no se reservan | No las agendamos automáticamente con la paquetería: que un proveedor responda 200 no prueba que llegará un mensajero, y decirle a alguien que su recolección está confirmada cuando no lo está es la peor falla que podemos entregar. Nuestro equipo la gestiona y avisa el resultado por correo a quien la pidió, por eso schedule_pickup siempre responde awaiting_confirmation. Solo se puede solicitar recolección con Estafeta, FedEx, DHL, UPS y Paquetexpress; con cualquier otra paquetería el remitente lleva el paquete a esa paquetería. |
Nada de lo anterior es una puerta cerrada. Construimos soluciones a la medida para socios — streaming, capacidad dedicada, internacional, herramientas que tu flujo necesite y que esta página no lista. Si la oferta estándar no te alcanza, eso es el comienzo de una conversación, no el final: escríbenos con tu caso de uso.
Capacidad
Cada llamada a una herramienta cuenta como una petición contra el límite de tasa de tu llave, igual que llamar a la operación del API que ejecuta — consulta Límites de tasa para los números por llave. Este diseño está dimensionado para agentes interactivos y automatización constante, no para flotas masivas en paralelo.
Nuestro compromiso: si el volumen de tu agente supera este diseño, dínoslo antes de rodearlo — invertiremos en migrar el servidor a una pila más capaz antes de dejar que tu integración choque con un muro. También podemos acordar juntos capacidad dedicada para una integración específica.