Enviadores

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.

URL base
https://api.enviadores.com.mx/api/v1

Inicio rápido

cURL
# 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 package409 RATE_PACKAGE_MISMATCH
cambia el CP de origen o de destino409 RATE_ADDRESS_MISMATCH
usas otra llave409 RATE_NOT_FOUND
usas otro X-PDV-ID409 RATE_PDV_MISMATCH
pasan 30 minutos409 RATE_EXPIRED

Autenticación

Manda tu clave en el header Authorization de cada petición:

Header
Authorization: Bearer ek_live_TU_CLAVE
PrefijoModo
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 UNAUTHORIZED de forma idéntica (sin revelar cuál caso fue).

Formato de respuesta

Toda respuesta JSON usa un sobre estable:

Éxito
{
  "success": true,
  "data": { … },
  "requestId": "req_…"
}
Error
{
  "success": false,
  "error": { "code": "…", "message": "…", "details": { … } },
  "requestId": "req_…"
}
  • error.details es opcional.
  • El requestId también viaja en el header X-Request-Id; inclúyelo al reportar un problema.
  • Los códigos de error.code son 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):

NivelSostenido (rpm)Ráfaga
Live standard60120
Live elevated240480
Claves de prueba3060
HeaderCuándo llega
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-ResetEn cada respuesta.
Retry-AfterEn 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…RespuestaQué hacer
con la misma clave y el mismo cuerpo, dentro de 15 minutosLa respuesta original, con el header Idempotent-Replay: true. Sin doble cargo.Nada.
con otro cuerpo (u otro X-PDV-ID)409 IDEMPOTENCY_KEY_PAYLOAD_MISMATCHManda una clave nueva.
pasados los 15 minutos409 IDEMPOTENCY_KEY_EXPIREDManda una clave nueva.
con una clave que ya usaste en la app web (o al revés)409 IDEMPOTENCY_KEY_REUSEDManda 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

El envío ES real y el cargo NO se revierte. Consulta 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

  1. Cada tarifa de POST /rates trae booking_locked (booleano). Si es true, esa tarifa NO es reservable por esta cuenta y POST /shipments responderá 403 SERVICE_UNLOCK_REQUIRED.
  2. 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.
  3. Filtra por booking_locked === false antes 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.

El sandbox no mueve dinero y por diseño no ejecuta los controles de riesgo, así que ahí 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):

CampoPara
packageuna caja
packages2–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 nombrando from.postal_code / to.postal_code antes 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:

CampoQué es
weight_kgPeso de la caja.
length_cm, width_cm, height_cmLargo, ancho y alto exteriores de la caja ya cerrada.

Sin las tres medidas:

EndpointResultado
POST /ratesSe calcula para una caja de 10×10×10 cm y regresa con size_estimated: true y un size_notice.
POST /shipmentsSe 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_id salvo que el usuario pida otra cosa.
  • La forma plana por omisión no cambia.

Seguro vs. valor declarado — dos cosas distintas

CampoQué esAl crear el envío
insurance.insured_valueContrata 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_value
POST /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:

422 VALIDATION_ERROR
{
  "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"
}
422 PACKAGE_DIMENSIONS_REQUIRED
{
  "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"
}
402 CREDIT_ERROR
{
  "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ódigoHTTP¿Reintentar?Qué hacer
UNAUTHORIZED401NoRevisa la llave (Bearer ek_live_/ek_test_); reintentar igual no ayuda.
INSUFFICIENT_SCOPE403NoLa llave no porta el scope requerido; usa una con permisos.
PDV_NOT_ALLOWED403NoEl X-PDV-ID no está en la allowlist de la llave (denegado por defecto).
SERVICE_UNLOCK_REQUIRED403NoPor 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_FOUND404NoEl recurso no existe o no pertenece a tu llave.
VALIDATION_ERROR422NoCorrige los campos listados en details.fields.
PACKAGE_DIMENSIONS_REQUIRED422Re-cotizaFalta 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_HIGH422NoEl valor declarado supera el máximo asegurable; redúcelo.
RATE_NOT_FOUND409Re-cotizaEl rate_id no existe para esta llave; vuelve a POST /rates.
RATE_EXPIRED409Re-cotizaPasaron los 30 min; vuelve a cotizar.
RATE_TAMPERED409Re-cotizaNunca alteres el rate_id; úsalo tal cual lo devolvió /rates.
RATE_PDV_MISMATCH409Re-cotizaCotiza y envía bajo el mismo X-PDV-ID.
RATE_PACKAGE_MISMATCH409Re-cotizaReenvía el mismo package que cotizaste, o vuelve a cotizar.
RATE_ADDRESS_MISMATCH409Re-cotizaLos CP de origen/destino del envío no son los que se cotizaron; cotiza de nuevo con las direcciones definitivas.
REQUOTE_REQUIRED409Re-cotizaEl 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_COLLISION409NoColisión de guía con otra cuenta (anomalía); el cargo se revirtió — contacta soporte.
IDEMPOTENCY_KEY_REUSED409NoLa 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_MISMATCH409NoLa 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_EXPIRED409NoLa operación con esa llave se completó hace más de 15 min; consulta GET /shipments o manda una llave nueva.
IDEMPOTENCY_KEY_INVALID400NoEl 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_ERROR402 / 500Sí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_LIMIT429NoTope diario alcanzado (de la cuenta o de la llave); espera la ventana de 24 h.
RATE_LIMITED429SíBackoff exponencial respetando Retry-After.
VENDOR_ERROR4xx/5xxDependeLa 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_ERROR500SíEl cargo se revirtió; reintenta.
ADDRESS_ERROR500SíFalló el registro de direcciones; reintenta.
POST_COMMIT_ERROR500NoEl 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_ERROR500Sí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-php

Cambia -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

EventoCuándodata
shipment.createdGuía comprada (cargo confirmado).{shipment}
shipment.collectedLa paquetería recogió el paquete.{shipment}
shipment.in_transitPrimer escaneo en tránsito.{shipment}
shipment.out_for_deliveryEn reparto.{shipment}
shipment.deliveredEntregado.{shipment}
shipment.exceptionLa paquetería reportó una incidencia.{shipment}
shipment.returnedDevuelto al remitente.{shipment}
shipment.cancelledLa cancelación quedó en firme.{shipment}
pickup.resolvedNuestro equipo cerró una solicitud de recolección (agendada o fallida).{pickup}

Todos los eventos usan este sobre; solo cambian event y data.

Registro

  1. 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)
  2. Guarda el secret de la respuesta 201. Se devuelve una sola vez: se deriva, no se almacena y no se puede volver a mostrar.
  3. Prueba la conexión: POST /webhooks/{id}/test envía un ping firmado de inmediato y reporta si tu endpoint respondió 2xx.

Verifica la firma

Cada entrega trae tres headers:

HeaderContenido
X-Enviadores-Signaturet=<unix>,v1=<hex>, donde v1 = HMAC-SHA256(secret, "<t>.<raw body>")
X-Enviadores-EventEl nombre del evento.
X-Enviadores-DeliveryEl id del evento: estable entre reintentos, úsalo para deduplicar.

Para aceptar una entrega:

  1. Recomputa el HMAC-SHA256 con tu secret sobre <t>.<raw body>: los bytes crudos del cuerpo, tal como llegaron.
  2. Compáralo con v1 en tiempo constante.
  3. Rechaza un t con más de 5 minutos de antigüedad.
Node.js
// 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

ReglaDetalle
RespuestaResponde 2xx en menos de 10 s y haz el trabajo después de responder. Nunca se siguen redirecciones (un 3xx cuenta como fallo).
ReintentosLas entregas fallidas se reintentan con espera creciente: 1 min, 5 min, 30 min, 2 h, 12 h; después el evento se descarta.
DesactivaciónTras 20 fallos consecutivos el webhook se desactiva (status: disabled, disabled_reason): bórralo y regístralo de nuevo.
Llave revocadaUn 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ó.
RepeticionesLos 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.
SandboxLas 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).
Scopeswebhooks: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 puerto 443 o 8443.
  • 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.

SaldoSaldo de prueba computado = $10,000 MXN por clave, menos el gasto de envíos de prueba no cancelados.
GuíasCon namespace SBX, disjuntas de todo formato real.
CancelaciónSíncrona y terminal (sin flujo de staff): el reembolso es inmediato.
RastreoNO se simula: GET /tracking resuelve solo guías reales.
Controles de riesgoNO 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ónLos 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_scenarioResultado
insufficient_funds402 CREDIT_ERROR con details {required, available, shortfall}, sin importar el saldo real (sintetizado — condición de crédito).
rate_expired409 RATE_EXPIRED (sintetizado — condición a nivel de validación).
vendor_error422 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 en webhooks (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 /shipments exige largo, ancho y alto de cada caja: sin ellos responde 422 PACKAGE_DIMENSIONS_REQUIRED (antes se generaba la guía para una caja de 10×10×10 cm). POST /rates sigue cotizando sin medidas, pero marca la respuesta con size_estimated: true y size_notice (campos nuevos, aditivos). En la vista agrupada, options[] pone primero las opciones reservables, así que options[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 MCP get_label), label_url es un enlace firmado a /v1/label-files/… que se abre sin llave y vence en el nuevo campo expires_at (30 días); pide uno nuevo con la misma llamada. label_url sigue siendo una URL de PDF.
  • 2026-09-04

    Varios bultos, vista agrupada y seguroAditivo

    Ahora se acepta packages (2–10 cajas, un solo envío) en POST /rates y POST /shipments (antes respondía 422); POST /rates?view=grouped devuelve una entrada por servicio distinto con su rango de precios y sus opciones reservables; el campo de seguro de la cotización ahora es insurance.insured_value (declared_value dentro de insurance queda como alias obsoleto) y POST /shipments acepta insurance y un declared_value raí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 endpoints POST/GET /webhooks, GET/DELETE /webhooks/{id} y POST /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 scopes webhooks: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 campo booking_locked en cada tarifa de POST /rates, y unlock_notice a 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 responde 403 SERVICE_UNLOCK_REQUIRED. Las integraciones existentes no se rompen — en cuentas sin restricción booking_locked siempre es false y unlock_notice es null. 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 fecha packages[].length_cm y sus pares llegan en null. El peso sí está disponible en todo el histórico.
  • 2026-07-27

    Lectura de envíosCambio de contrato

    /shipments acepta ?guia=, ?status= y ?from=/?to=; cada envío incluye ahora lo capturado al crearlo (packages, billable_weight_kg, declared_value, content) y el sender. Nuevo scope ampliado shipments:read:pdv para listar los envíos de todo un PDV con X-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-ID ya se valida en las rutas de envíos y cancelaciones — un PDV fuera de la allowlist responde 403 en lugar de ignorarse.
  • 2026-07-13

    Respuesta de /rates saneadaBreaking pre-adopción

    v1: respuesta de /rates saneada a snake_case; rechazo explícito de country≠MX / campos desconocidos (y de packages, 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.

Endpoint MCP (POST, JSON-RPC 2.0)
https://api.enviadores.com.mx/api/v1/mcp

Por qué usarlo

Un agente no puede gastar de más, por diseñoCada 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 doblecreate_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 verdadCotizaciones 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 confiarMismas 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 Code (un comando)
claude mcp add --transport http enviadores https://api.enviadores.com.mx/api/v1/mcp \  --header "Authorization: Bearer ek_test_..."
Cursor / VS Code / otros clientes (configuración JSON)
{  "mcpServers": {    "enviadores": {      "url": "https://api.enviadores.com.mx/api/v1/mcp",      "headers": { "Authorization": "Bearer ek_test_..." }    }  }}
O pruébalo directo (cURL)
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_recipient y envía por from_id/to_id — cargas más pequeñas y sin volver a teclear direcciones.

Herramientas

AccesoSignifica
LecturaConsulta datos, no cambia nada.
EscrituraCrea, cambia o elimina datos de la cuenta.
Escritura sensibleGasta 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.
HerramientaAccesoQué hace
validate_postal_codeLecturaValida un CP contra el catálogo oficial de SEPOMEX (estado, municipio, colonias).
get_shipping_ratesLecturaCotiza 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_shipmentEscritura sensibleCompra la guía de un rate_id cotizado. La única herramienta que gasta dinero — idempotente por contrato.
list_shipmentsLecturaLista los envíos de la cuenta con filtros (guía, estatus, fechas).
track_shipmentLecturaRastrea cualquier guía; estatus canónico más historial.
get_labelLecturaObtiene la URL del PDF de la guía de uno de tus envíos.
cancel_shipmentEscritura sensibleSolicita la cancelación (puede ser asíncrona según la paquetería).
get_balanceLecturaSaldo prepagado en MXN — el techo duro de gasto.
list_sendersLecturaLista los remitentes guardados; sus ids sirven como from_id — sin volver a teclear direcciones.
create_senderEscrituraGuarda un remitente una vez y envía por id después (deduplica por mismo contenido, sin duplicados).
list_recipientsLecturaLista los destinatarios guardados (por remitente); sus ids sirven como to_id.
create_recipientEscrituraGuarda un destinatario bajo un remitente; usa su id como to_id.
import_contactsEscrituraImporta remitentes/destinatarios en lote desde CSV o texto libre; dry_run por omisión.
schedule_pickupsandboxEscritura sensibleSolicita 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_pickupsandboxLecturaConsulta una solicitud de recolección: su estado y, ya acordada, la ventana confirmada.
register_webhookEscritura sensibleRegistra un endpoint https para eventos de envíos y recolecciones (sin polling). El secreto de firma se devuelve una sola vez.
list_webhooksLecturaLista los webhooks de la cuenta con estado, fallos consecutivos y última entrega.
test_webhookEscrituraEnvía un ping firmado al webhook ahora mismo y reporta la respuesta del endpoint.
delete_webhookEscrituraElimina un webhook; los eventos en cola se descartan.
list_pickupssandboxLecturaLista tus solicitudes de recolección con su estado — sin recordar el id de cada una.
whoamiLecturaIdentifica la conexión: cuenta, modo (pruebas o producción), permisos de la llave, saldo y capacidades.
list_transactionsLecturaLista los movimientos de saldo (cargos, reembolsos, recargas, ajustes) con importe con signo.
get_shipmentLecturaConsulta 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 sesionesUn 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.1Los 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ándarTodas 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 reservanNo 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.