Enviadores Public API
API REST pública de Enviadores: cotiza, genera guías, rastrea y cancela envíos.
Versión 1.0.0 · OpenAPI 3.1.0
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 clave
curl 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 falta
curl -L https://api.enviadores.com.mx/api/v1/labels/ENVIO_ID -H "Authorization: Bearer ek_test_TU_CLAVE" -o etiqueta.pdf
curl 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: reenvía from/to/package en el paso 2, y deben ser los MISMOS datos que cotizaste — un package distinto responde 409 RATE_PACKAGE_MISMATCH; una dirección distinta NO se rechaza hoy y se cobra después como sobrepeso o falla en la paquetería. Cotiza y envía con la MISMA llave (y mismo X-PDV-ID): de lo contrario 409 RATE_NOT_FOUND/RATE_PDV_MISMATCH; a los 30 minutos, 409 RATE_EXPIRED.
Autenticación
Envía tu clave en el header Authorization: Bearer ek_live_… (o ek_test_… para el 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, y 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. En éxito: { "success": true, "data": {…}, "requestId": "…" }. En error: { "success": false, "error": { "code", "message", "details"? }, "requestId" }. 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.
Límites de tasa
El límite es por clave (token bucket). Cada respuesta incluye X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset. Un 429 RATE_LIMITED incluye Retry-After (segundos) y details.retry_after_ms. Niveles: live standard 60 rpm sostenido / ráfaga 120; elevated 240/480; claves de prueba 30/60. Respeta Retry-After con backoff exponencial.
Idempotencia
Manda un header Idempotency-Key (UUID fresco por operación lógica, 1–128 caracteres ASCII imprimibles) en POST /shipments. Un reintento con la misma clave y el mismo cuerpo repite la respuesta original (header Idempotent-Replay: true) sin doble cargo, dentro de una ventana de 15 minutos. La clave queda ligada al cuerpo exacto de la primera solicitud y al X-PDV-ID con que se envió: reusarla con otro cuerpo responde 409 IDEMPOTENCY_KEY_PAYLOAD_MISMATCH y reusarla pasados los 15 minutos responde 409 IDEMPOTENCY_KEY_EXPIRED — en ambos casos 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: una clave usada en la app web y reusada aquí (o al revés) responde 409 IDEMPOTENCY_KEY_REUSED. 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 con servicios sin recolección (el remitente lleva el paquete a una sucursal y cualquier cargo extra —sobrepeso, servicios adicionales— se paga en mostrador). 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 /rates trae booking_locked (booleano). Si es true, esa tarifa NO es reservable por esta cuenta y POST /shipments responderá 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 === 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.
Multi-package, insurance and declared value · Bultos, seguro y valor declarado
Boxes. Send package for one box or packages (2–10 boxes travelling as ONE shipment) on both POST /rates and POST /shipments — never both (422). A multi-package quote only returns services that can ship several boxes together, the billable weight is the SUM of the boxes, and the create request must carry the SAME list you quoted (a different set answers 409 RATE_PACKAGE_MISMATCH). Postal codes must exist in the SEPOMEX catalog: a nonexistent CP (e.g. 99999) answers 422 naming from.postal_code / to.postal_code before any carrier is asked.
Grouped view. POST /rates?view=grouped lists each distinct shipping service ONCE (carrier + service level, e.g. DHL · Express) with its price range (price_from–price_to) and its bookable options[], cheapest first — book options[0].rate_id unless the user asks otherwise. The flat default shape is unchanged.
Insurance vs declared value — two different things. insurance.insured_value buys coverage for the shipment; when the platform policy is unavailable the carrier's own coverage is used and the price shown already includes it (each rate says insurance_included / insurance_not_supported). On create, omit insurance to accept the coverage you quoted, or send the same value; an explicit different value answers 422 INSURANCE_MISMATCH (re-quote — the signed total is never recalculated), and a rate whose carrier offers no coverage answers 422 INSURANCE_NOT_AVAILABLE. declared_value (POST /shipments) is the value of the goods stated to the carrier — not a policy, but a carrier may price it, so it must equal the value the rate was quoted with (a value the quote did not carry answers 422 DECLARED_VALUE_MISMATCH; only a service that asks for it after the quote — 422 DECLARED_VALUE_REQUIRED — accepts a new one). Nothing is invented when you omit it.
Español. Bultos: manda package (una caja) o packages (2–10 cajas en un mismo envío) en /rates y /shipments — nunca ambos (422); con varios bultos solo se cotizan servicios que los llevan juntos, el peso facturable es la suma y al crear debes mandar la MISMA lista cotizada. Los CP deben existir en SEPOMEX (si no, 422 nombrando el campo). Vista agrupada: ?view=grouped devuelve cada servicio una sola vez con su rango de precios y sus opciones reservables, de menor a mayor. Seguro vs. valor declarado: insurance.insured_value contrata cobertura para el envío — la póliza de la plataforma o, si no está disponible, la cobertura de la propia paquetería, ya incluida en el precio mostrado; al crear, omite insurance para aceptar la cobertura cotizada o manda el mismo valor (un valor distinto → 422 INSURANCE_MISMATCH; paquetería sin cobertura → 422 INSURANCE_NOT_AVAILABLE). declared_value es el valor de la mercancía declarado a la paquetería: no es seguro, pero la paquetería puede cobrarlo, así que debe ser el mismo con el que se cotizó (un valor que la cotización no llevaba → 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).
POST /rates · 200
{
"success": true,
"data": {
"services": [
{
"id": "e2etest_E2ETestCarrier_E2EStandard_ab12cd34",
"carrier": "Estafeta",
"service_name": "Terrestre",
"service_type": "standard_economy",
"tier": "standard_economy",
"delivery_window": "ground",
"pickup_included": false,
"address_delivery": true,
"pricing": { "total_price": 145.50, "currency": "MXN", "iva_included": true, "insurance_premium": 0, "insured_value": null },
"delivery": { "estimated_days": "3-5", "estimated_date": null, "min_days": 3, "max_days": 5 },
"insurance_included": false,
"insurance_not_supported": false,
"zona_extendida": false
},
{
"id": "e2etest_E2ETestCarrier_E2EExpress_7f0a16b2",
"carrier": "FedEx",
"service_name": "Express Nacional",
"service_type": "express",
"tier": "express",
"delivery_window": "next_day",
"pickup_included": true,
"address_delivery": true,
"pricing": { "total_price": 289.00, "currency": "MXN", "iva_included": true, "insurance_premium": 0, "insured_value": null },
"delivery": { "estimated_days": "1-2", "estimated_date": null, "min_days": 1, "max_days": 2 },
"insurance_included": false,
"insurance_not_supported": false,
"zona_extendida": false
}
],
"fetched_at": "2026-07-13T18:42:05Z",
"stale_after": "2026-07-13T18:47:05Z",
"meta": { "billable_weight": 1, "volumetric_weight": 0.6, "zone": 5 },
"rate_id_expires_in_seconds": 1800
},
"requestId": "req_a1b2c3d4e5"
}POST /shipments · 200
{
"success": true,
"data": {
"shipment": {
"id": "20260713-000123",
"guia": "1234567890",
"carrier": "Estafeta",
"service": "Terrestre",
"status": "created",
"total": 145.5,
"currency": "MXN",
"label_url": "/api/v1/labels/20260713-000123",
"tracking_pending": false,
"created_at": "2026-07-13T18:45:12Z"
}
},
"requestId": "req_b2c3d4e5f6"
}GET /tracking/{guia} · 200
{
"success": true,
"data": {
"guia": "1234567890",
"carrier": "Estafeta",
"status": "in_transit",
"status_label": "En tránsito",
"origin": { "city": "Ciudad de México", "state": "CDMX" },
"destination": { "city": "Monterrey", "state": "Nuevo León" },
"created_at": "2026-07-13T18:45:12Z",
"events": [
{ "timestamp": "2026-07-13T20:10:00Z", "description": "Recolectado", "location": "Ciudad de México, CDMX" },
{ "timestamp": "2026-07-14T09:30:00Z", "description": "En tránsito al destino", "location": "Querétaro, QRO" }
]
},
"requestId": "req_c3d4e5f6a7"
}GET /balance · 200
{
"success": true,
"data": {
"balance": 4820.50,
"held": 145.50,
"available": 4675.00,
"currency": "MXN",
"scope": { "type": "user", "id": "U0000001" }
},
"requestId": "req_d4e5f6a7b8"
}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"
}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 de 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 | La cuenta aún no puede reservar servicios CON recolección. Re-cotiza y elige una tarifa con booking_locked:false (sin recolección). 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. |
| 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 · Eventos salientes
Instead of polling GET /shipments or GET /tracking/{guia}, register a public https:// endpoint and receive one signed POST per event. Up to 5 active webhooks per account (per mode). The signing secret is returned exactly once at registration — it is derived, never stored, and cannot be shown again.
Español: en lugar de hacer polling, registra un endpoint https:// público y recibe un POST firmado por cada evento. Hasta 5 webhooks activos por cuenta (por modo). El secreto de firma se devuelve una sola vez al registrar — no se almacena ni se vuelve a mostrar.
Events · Eventos
| Event | When · Cuándo | data |
|---|---|---|
| shipment.created | Label bought (charge committed). Español: Guía comprada (cargo confirmado). | {shipment} |
| shipment.collected | Carrier picked the package up. Español: La paquetería recogió el paquete. | {shipment} |
| shipment.in_transit | First in-transit scan. Español: Primer escaneo en tránsito. | {shipment} |
| shipment.out_for_delivery | Out for delivery. Español: En reparto. | {shipment} |
| shipment.delivered | Delivered. Español: Entregado. | {shipment} |
| shipment.exception | Carrier reported an incident. Español: La paquetería reportó una incidencia. | {shipment} |
| shipment.returned | Returned to sender. Español: Devuelto al remitente. | {shipment} |
| shipment.cancelled | Cancellation became final. Español: La cancelación quedó en firme. | {shipment} |
| pickup.resolved | Our team closed a pickup request (scheduled or failed). Español: Nuestro equipo cerró una solicitud de recolección (agendada o fallida). | {pickup} |
Register · Registro
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_…" (shown ONCE · se muestra UNA vez)Keep secret from the 201 response; then POST /webhooks/{id}/test sends a signed ping right away and reports whether your endpoint answered 2xx. Español: guarda el secret de la respuesta 201; POST /webhooks/{id}/test envía un ping firmado de inmediato y reporta si tu endpoint respondió 2xx.
Verify the signature · Verifica la firma
Every delivery carries X-Enviadores-Signature: t=<unix>,v1=<hex> where v1 = HMAC-SHA256(secret, "<t>.<raw body>"), plus X-Enviadores-Event (event name) and X-Enviadores-Delivery (the event id — stable across retries, use it to deduplicate). Recompute over the raw bytes, compare in constant time, reject a t older than 5 minutes. Español: recomputa el HMAC sobre los bytes crudos, compara en tiempo constante y rechaza un t con más de 5 minutos.
// Node.js — verify X-Enviadores-Signature over the RAW body · sobre el cuerpo crudo
import { 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);
}Delivery rules · Reglas de entrega
- Answer 2xx within 10 s; do the work after responding. Redirects are never followed (a 3xx is a failure). Español: responde 2xx en menos de 10 s y procesa después; no se siguen redirecciones.
- Failed deliveries retry with backoff: 1 min, 5 min, 30 min, 2 h, 12 h, then the event is dropped. After 20 consecutive failures the webhook is disabled (status: disabled, disabled_reason) — delete it and register it again. Español: reintentos con espera creciente y desactivación automática tras 20 fallos consecutivos.
- Body: {id, event, mode, created_at, data}; data.shipment / data.pickup have exactly the shape of GET /shipments/{id} / GET /pickups/{id}. Events for a shipment go to the account that created it. Español: mismo formato que los recursos de lectura; los eventos de un envío llegan a la cuenta que lo creó.
- URL policy (SSRF): https:// only on port 443 or 8443, public hostname (no localhost, .local, .internal, private/loopback IPs, hosts resolving to them, or the Enviadores API itself), no embedded credentials. The test ping reports a coarse error class (timeout, connection failed, non-2xx status) — never raw network detail. Español: solo https en 443/8443 y hosts públicos; el ping reporta una clase de error gruesa, nunca el detalle de red.
- A webhook dies with the key that registered it: revoking that API key sets status: disabled with disabled_reason: key_revoked and discards its queued events, so a leaked key cannot keep a listener alive (re-authorizing an OAuth connector retires its previous key the same way). Español: al revocar la llave que registró un webhook (o al reautorizar un conector OAuth), el webhook se desactiva (key_revoked) y sus eventos en cola se descartan.
- Repeats: non-terminal statuses can legitimately recur (exception → in_transit → exception emits two shipment.exception), while the same observed transition is delivered once; delivered, returned and cancelled are delivered at most once per shipment. Español: los estatus no terminales pueden repetirse; los terminales llegan a lo sumo una vez.
- Sandbox: ek_test_ keys manage sandbox webhooks, which receive only the test lane's shipment.created / shipment.cancelled and the ping (the sandbox has no carrier tracking and never resolves pickups). Español: las llaves de prueba administran webhooks de sandbox, que solo reciben creación/cancelación y el ping.
- Scopes: webhooks:write (register, test, delete) and webhooks:read (list, get). Keys created with the default permission set gain them automatically (OAuth connectors included). Español: las llaves con permisos por omisión los adquieren solas (conectores OAuth incluidos).
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 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.
- La cancelación es síncrona y terminal (sin flujo de staff): el reembolso es inmediato.
- El rastreo NO se simula: GET /tracking resuelve solo guías reales.
- Los 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 (metadata.test_scenario en POST /shipments)
- 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
- 2026-09-21 — Las etiquetas se sirven siempre desde Enviadores. 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. Compatible: label_url sigue siendo una URL de PDF.
- 2026-09-04 — Additive. packages (2–10 boxes, one shipment) is now accepted on POST /rates and POST /shipments (it used to answer 422); POST /rates?view=grouped returns one entry per distinct service with its price range and bookable options; the quote insurance field is now insurance.insured_value (declared_value inside insurance stays as a deprecated alias) and POST /shipments accepts insurance and a separate top-level declared_value (carrier declaration). Postal codes must exist in SEPOMEX on both endpoints. See Multi-package, insurance and declared value. Español: se acepta packages en /rates y /shipments (antes 422); nueva vista ?view=grouped; el seguro de la cotización se llama insurance.insured_value (alias obsoleto declared_value dentro de insurance); /shipments acepta insurance y un declared_value raíz (valor declarado a la paquetería); los CP deben existir en SEPOMEX.2026-09-04 — Webhooks: 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). Aditivo. Ver Webhooks.
- 2026-08-30 — 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 sin recolección; reservar una tarifa bloqueada responde 403 SERVICE_UNLOCK_REQUIRED. Aditivo: 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 — 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 — Lado de lectura de envíos: /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 — v1: respuesta de /rates saneada a snake_case (breaking pre-adopción); 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.
Los cambios dentro de v1 serán aditivos; un cambio incompatible se anunciará aquí y saldría como /v2.
MCP — connect an AI agent · conecta un agente de IA
Enviadores runs a native Model Context Protocol server: Claude, Cursor, VS Code or any MCP client can quote, buy labels, track and cancel shipments directly — using the same API keys, scopes and limits as the REST API. Streamable HTTP, stateless, one JSON-RPC message per POST.
Español: Enviadores incluye un servidor MCP nativo — Claude, Cursor, VS Code o cualquier cliente MCP puede cotizar, comprar guías, rastrear y cancelar envíos con las mismas llaves, alcances y límites del API REST.
Why teams use it · Por qué usarlo
- An agent structurally cannot overspend. Every purchase runs against your prepaid balance plus the key's daily spend limit, enforced on our servers — there is no card to overdraw and no way to go negative, no matter what the agent does. Español: un agente no puede gastar de más — saldo prepagado y límite diario de la llave, aplicados en el servidor.
- Retries never double-charge. create_shipment is idempotent: the server generates and returns an idempotency_key when the agent doesn't send one, and rejects malformed ones instead of guessing. Español: los reintentos con la misma clave nunca duplican el cargo.
- Mexican shipping for real — multi-carrier quotes, labels and tracking with SEPOMEX-validated addresses (colonia-level), in an agent's own language. Español: paquetería mexicana de verdad, con CP y colonias validados contra SEPOMEX.
- Zero new surface to trust. Same keys, same scopes, same limits as the REST API — a scoped key can give an agent quote-and-track powers with no ability to buy. Free sandbox (ek_test_) to try everything with no real money; live keys (ek_live_) require identity verification on the account. Español: mismas llaves y alcances del API; sandbox gratis, llaves live con verificación de identidad.
Connect · Conexión
Claude Code (one command · un comando):
claude mcp add --transport http enviadores https://api.enviadores.com.mx/api/v1/mcp \
--header "Authorization: Bearer ek_test_..."Cursor / VS Code / other clients (JSON config · configuración JSON):
{
"mcpServers": {
"enviadores": {
"url": "https://api.enviadores.com.mx/api/v1/mcp",
"headers": { "Authorization": "Bearer ek_test_..." }
}
}
}Or probe it raw · o pruébalo directo:
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"}'Start with an ek_test_ key: it runs in the sandbox — full rehearsal, simulated carriers, no real money. Manage keys in Mi cuenta → Claves de API. Tip: save addresses once with create_sender/create_recipient and ship by from_id/to_id — smaller payloads, no retyped addresses. Español: empieza con una llave ek_test_ (sandbox, sin dinero real); guarda direcciones una vez y envía por id.
Tools · Herramientas
| Tool | What it does · Qué hace |
|---|---|
| validate_postal_code | Validate a CP against the official SEPOMEX catalog (state, municipality, colonias).Valida un CP contra SEPOMEX (estado, municipio, colonias). |
| get_shipping_rates | Quote a domestic shipment across all carriers; each service once with its price range and bookable options (rate_ids, 30-min expiry).Cotiza con todas las paqueterías; cada servicio una vez con su rango de precios y sus opciones (rate_ids, expiran en 30 min). |
| create_shipment | Buy the label for a quoted rate_id. The only spending tool — idempotent by contract.Compra la guía de un rate_id cotizado. La única herramienta que gasta — idempotente. |
| list_shipments | List account shipments with filters (guía, status, dates).Lista los envíos de la cuenta con filtros. |
| track_shipment | Track any guía; canonical status plus history.Rastrea cualquier guía; estatus canónico e historial. |
| get_label | Fetch the label PDF URL for one of your shipments.Obtiene la URL del PDF de la guía de un envío propio. |
| cancel_shipment | Request cancellation (may be asynchronous per carrier).Solicita cancelación (puede ser asíncrona según paquetería). |
| get_balance | Prepaid balance in MXN — the hard spending ceiling.Saldo prepagado en MXN — el techo duro de gasto. |
| list_senders | List saved senders; their ids work as from_id — no retyping addresses.Lista remitentes guardados; sus ids sirven como from_id. |
| create_sender | Save a sender once, ship by id afterwards (same-content dedupe, no duplicates).Guarda un remitente una vez y envía por id después. |
| list_recipients | List saved recipients (per sender); ids work as to_id.Lista destinatarios guardados; sus ids sirven como to_id. |
| create_recipient | Save a recipient under a sender; use its id as to_id.Guarda un destinatario bajo un remitente; usa su id como to_id. |
| import_contacts | Bulk-import senders/recipients from CSV or free text; dry_run by default.Importa remitentes/destinatarios en lote desde CSV o texto libre; dry_run por omisión. |
| schedule_pickupsandbox | Request a carrier pickup at the sender’s address for an existing shipment. A request, not a booking — our team confirms it and emails the outcome.Solicita la recolección a domicilio de un envío ya creado. Es una solicitud, no una reserva: nuestro equipo la confirma y avisa por correo. |
| get_pickupsandbox | Read a pickup request: its state and, once arranged, the confirmed window.Consulta una solicitud de recolección: estado y, ya confirmada, la ventana. |
| register_webhook | Register an https endpoint for shipment/pickup events (no polling). The signing secret is returned exactly once.Registra un endpoint https para eventos de envíos y recolecciones (sin polling). El secreto de firma se muestra una sola vez. |
| list_webhooks | List the account’s webhooks with status, consecutive failures and last delivery.Lista los webhooks de la cuenta con estado, fallos consecutivos y última entrega. |
| test_webhook | Send a signed ping to a webhook right now and report the endpoint’s answer.Envía un ping firmado al webhook ahora mismo y reporta la respuesta del endpoint. |
| delete_webhook | Delete a webhook; queued events for it are discarded.Elimina un webhook; los eventos en cola se descartan. |
| list_pickupssandbox | List your pickup requests with their state — no need to remember each request id.Lista tus solicitudes de recolección con su estado — sin recordar el id de cada una. |
| whoami | Identify the connection: account, sandbox vs production, the key’s scopes, balance and capabilities.Identifica la conexión: cuenta, modo (pruebas o producción), permisos de la llave, saldo y capacidades. |
| list_transactions | List balance movements (charges, refunds, top-ups, adjustments) with signed amounts.Lista los movimientos de saldo (cargos, reembolsos, recargas, ajustes) con importe con signo. |
| get_shipment | Read one shipment by id: status, guía, carrier, service, destination, package and price.Consulta un envío por su id: estatus, guía, paquetería, servicio, destino, paquete y precio. |
What this server is — and isn't · Qué es este servidor — y qué no
We'd rather you know the category before you build on it: this is a focused, stateless MCP server — request/response tools over Streamable HTTP, purpose-built for shipping operations. Not a general agent platform. Español: es un servidor MCP enfocado y sin estado, hecho para operar envíos — no una plataforma general de agentes.
- No streaming, no sessions. One JSON-RPC message per POST, plain JSON responses, no SSE. Right for quote/label/track calls; wrong for long-running streamed workloads. Español: sin streaming ni sesiones — correcto para estas herramientas, no para cargas de trabajo en flujo continuo.
- API keys and OAuth 2.1. Header clients (Claude Code, Cursor, VS Code) authenticate with an
ek_key; claude.ai web connectors and ChatGPT (developer mode) connect through the built-in OAuth flow — add the server URL, sign in with your Enviadores account and approve in sandbox or production. Every OAuth grant maps onto a scoped API key, so the same limits and spend controls apply. Español: llaves de API y OAuth 2.1 — los conectores de claude.ai y ChatGPT (modo desarrollador) se conectan con tu cuenta y eliges sandbox o producción al autorizar. - 23 tools, Mexico-domestic on the standard API. All live on production keys. International shipping exists on the platform for accounts with a fulfillment/partner agreement — it's a conversation, not a hard no. Español: 23 herramientas, envíos nacionales (MX) en el API estándar; el envío internacional existe para cuentas con convenio de fulfillment — pregúntanos.
- Pickups are requests, not bookings. We don't auto-schedule with the carrier: a vendor returning
200is not proof a courier will arrive, and telling someone their pickup is confirmed when it isn't is the worst failure we can ship. Our team arranges it and emails the requester the outcome, soschedule_pickupalways answersawaiting_confirmation. Estafeta, DHL, FedEx and Paquetexpress. Español: la recolección se solicita, no se reserva: nuestro equipo la confirma con la paquetería y avisa por correo — preferimos ser lentos y ciertos.
None of the above is a closed door. We build ad-hoc solutions for partners — streaming, dedicated capacity, international, tools your workflow needs that this page doesn't list. If the standard offering doesn't fit, that's the start of a conversation, not the end of one: write to us with your use case. Español: nada de lo anterior es una puerta cerrada — construimos soluciones a la medida para socios; si la oferta estándar no te alcanza, escríbenos con tu caso.
Capacity · Capacidad
Every tool call counts as two requests against your key's rate limit (the MCP call plus the API call it performs) — see Límites de tasa for the per-key numbers. This design is sized for interactive agents and steady automation, not massive parallel fleets. Español: cada herramienta cuenta como dos peticiones contra el límite de tu llave; el diseño está dimensionado para agentes interactivos y automatización constante, no para flotas masivas en paralelo.
Our commitment: if your agent volume outgrows this design, tell us before you work around it — we will invest in moving the server to a more capable stack rather than let your integration hit a wall. Dedicated capacity for a specific integration is likewise something we can agree on together. Español: si tu volumen supera este diseño, dínoslo antes de rodearlo — invertiremos en migrar a una pila más capaz antes de frenar tu integración; también podemos acordar capacidad dedicada para tu caso.
rates
Cotización multi-paquetería
Cotizar en todas las paqueterías
Cotiza con todas las paqueterías activas y devuelve tarifas con id (rate_id) listas para POST /shipments. Requiere scope rates:read. Solo rutas domésticas MX (country ≠ MX → 422). Bultos: UN paquete (package) o VARIOS en un mismo envío (packages, 2–10 bultos; mandar ambos → 422). Con packages solo se devuelven ofertas reservables como envío multi-bulto (los servicios que no lo soportan no aparecen) y el peso facturable es la SUMA de los bultos. 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 las paqueterías (antes se cotizaba por zona a ciegas). Las tarifas de referencia de mostrador (market) NO se incluyen: todo rate_id devuelto es enviable. Seguro: insurance.insured_value (alias obsoleto declared_value) contrata cobertura para el envío: la póliza de la plataforma (prima incluida y firmada dentro de pricing.total_price) o, si esa póliza no está disponible, la cobertura de la propia paquetería — el precio mostrado ya la incluye y cada tarifa lo indica con insurance_included / insurance_not_supported. *Insurance: insured_value buys coverage; when the platform policy is unavailable the carrier's own coverage is used and the price shown already includes it.* Vista agrupada: ?view=grouped devuelve cada servicio distinto UNA sola vez (paquetería + nivel de servicio) con su rango de precios y sus opciones reservables (ver RatesGroupedResponse); la forma plana por defecto no cambia. La respuesta es la MISMA para toda llave (los costos de proveedor nunca se exponen).
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
| view | query | — | grouped: colapsa services[] en un elemento por servicio distinto (RatesGroupedResponse, con view: "grouped"). Omitido: la forma plana (RatesResponse). |
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| from | RatesEndpointParty | — | |
| from_id | string | — | Id de un remitente del directorio (GET /senders). Mutuamente excluyente con from. |
| to | RatesEndpointParty | — | |
| to_id | string | — | Id de un destinatario del directorio (GET /recipients). Mutuamente excluyente con to. |
| package | object | — | |
| packages | PackagesList | — | |
| insurance | InsuranceRequest | — |
Ejemplo
{
"from": {
"postal_code": "01000"
},
"to": {
"postal_code": "64000"
},
"package": {
"weight_kg": 1,
"length_cm": 20,
"width_cm": 15,
"height_cm": 10
}
}Respuestas
| Código | Descripción |
|---|---|
| 200 | Cotización exitosa (forma autenticada multi-paquetería). |
| 400 | INVALID_JSON (cuerpo vacío/no-JSON) · MISSING_ROUTE · MISSING_PACKAGE · WEIGHT_EXCEEDED (peso facturable > 70 kg). |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 422 | VALIDATION_ERROR (details.fields lista los campos — incluye country ≠ MX, package y packages juntos, packages fuera de 2–10, un CP inexistente en SEPOMEX (from.postal_code: el CP 99999 no existe en el catálogo SEPOMEX) y campos desconocidos a nivel raíz) · INSURANCE_VALUE_TOO_HIGH (valor asegurado sobre el máximo asegurable). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | RATES_ERROR — la cotización falló; reintenta. |
Ejemplo · 200
{
"success": true,
"data": {
"services": [
{
"id": "e2etest_E2ETestCarrier_E2EStandard_ab12cd34",
"carrier": "Estafeta",
"service_name": "Terrestre",
"service_type": "standard_economy",
"tier": "standard_economy",
"delivery_window": "ground",
"pickup_included": false,
"address_delivery": true,
"pricing": {
"total_price": 145.5,
"currency": "MXN",
"iva_included": true,
"insurance_premium": 0,
"insured_value": null
},
"delivery": {
"estimated_days": "3-5",
"estimated_date": null,
"min_days": 3,
"max_days": 5
},
"insurance_included": false,
"insurance_not_supported": false,
"zona_extendida": false
},
{
"id": "e2etest_E2ETestCarrier_E2EExpress_7f0a16b2",
"carrier": "FedEx",
"service_name": "Express Nacional",
"service_type": "express",
"tier": "express",
"delivery_window": "next_day",
"pickup_included": true,
"address_delivery": true,
"pricing": {
"total_price": 289,
"currency": "MXN",
"iva_included": true,
"insurance_premium": 0,
"insured_value": null
},
"delivery": {
"estimated_days": "1-2",
"estimated_date": null,
"min_days": 1,
"max_days": 2
},
"insurance_included": false,
"insurance_not_supported": false,
"zona_extendida": false
}
],
"fetched_at": "2026-07-13T18:42:05Z",
"stale_after": "2026-07-13T18:47:05Z",
"meta": {
"billable_weight": 1,
"volumetric_weight": 0.6,
"zone": 5
},
"rate_id_expires_in_seconds": 1800
},
"requestId": "req_a1b2c3d4e5"
}shipments
Creación y consulta de envíos
Listar envíos (paginado)
Por defecto: solo envíos creados por el usuario de la llave, para TODOS los roles — una llave admin ve únicamente sus propios envíos de cuenta de servicio.
Alcance por PDV (opt-in). Una llave admin que envía X-PDV-ID y tiene el scope shipments:read:pdv lista en cambio los envíos de ese punto de venta, sin importar qué operador los creó — el mismo fondo que ya responden /senders, /recipients y /balance bajo ese header. Sin el scope, el header no amplía nada y sigues viendo solo lo tuyo (los integradores que ya lo envían no cambian de resultados).
Cambio de contrato (2026-07): X-PDV-ID ahora se valida en esta ruta aunque no amplíe nada. Antes se ignoraba por completo, así que un PDV inexistente o fuera de la allowlist de la llave respondía 200 con lista vacía mientras /senders respondía 403; ahora responde 403 PDV_NOT_ALLOWED como todas las rutas hermanas.
Orden: más recientes primero. Requiere scope shipments:read (+ shipments:read:pdv para el alcance por PDV).
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| page | query | — | |
| limit | query | — | |
| guia | query | — | Búsqueda exacta por número de guía. Es la forma de llegar a un envío cuando solo tienes el número que ve el cliente (el id interno no es adivinable). Devuelve una página de 0 o 1 elementos, sujeta a la misma visibilidad que el resto del listado. |
| status | query | — | Filtra por estatus canónico — exactamente el mismo valor que devuelve el campo status de cada envío. Un valor fuera del enum es 422 VALIDATION_ERROR, nunca una lista vacía silenciosa. |
| from | query | — | Día calendario INCLUSIVO (zona horaria de negocio America/Mexico_City) desde el cual listar, por fecha de creación. Solo YYYY-MM-DD. |
| to | query | — | Día calendario INCLUSIVO (America/Mexico_City) hasta el cual listar — el día completo, hasta las 23:59:59 locales. from posterior a to es 422. |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Página de envíos. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 422 | VALIDATION_ERROR — filtro inválido (details.fields). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| shipments | Shipment[] | Sí | |
| pagination | Pagination | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"shipments": [
{
"id": "20260713-000123",
"guia": "1234567890",
"carrier": "Estafeta",
"service": "Terrestre",
"status": "in_transit",
"status_label": "En tránsito",
"total": 145.5,
"currency": "MXN",
"created_at": "2026-07-13T18:45:12Z",
"tracking_pending": false,
"label_url": "/api/v1/labels/20260713-000123",
"packages": [
{
"weight_kg": 1,
"length_cm": 20,
"width_cm": 15,
"height_cm": 10
}
],
"billable_weight_kg": 1,
"declared_value": null,
"content": "Ropa",
"sender": {
"name": "Juan Pérez",
"city": "Ciudad de México",
"state": "CDMX",
"postal_code": "01000"
},
"recipient": {
"name": "María López",
"city": "Monterrey",
"state": "Nuevo León",
"postal_code": "64000"
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1
}
},
"requestId": "req_e5f6a7b8c9"
}Crear un envío (genera la guía y cobra créditos)
Crea el envío contra un rate_id vigente (≤ 30 min) cotizado con la MISMA llave (y mismo PDV, si aplica). Solo domésticos MX (customs/ocurre, country ≠ MX o campos desconocidos → 422). Bultos: los MISMOS que se cotizaron — package (uno) o packages (2–10 en un solo envío; ambos → 422); un conjunto distinto al cotizado responde 409 RATE_PACKAGE_MISMATCH. CP: origen/destino inline deben existir en SEPOMEX (422 nombrando from.postal_code/to.postal_code, antes de retener saldo). Seguro vs. valor declarado (no son lo mismo): insurance.insured_value es la póliza de plataforma que YA se cotizó — debe coincidir con la cotización (422 INSURANCE_MISMATCH si difiere o falta; el total firmado nunca se recalcula al crear); declared_value es el valor de la mercancía que se declara a la paquetería (MXN, informativo; algunos servicios lo exigen — DECLARED_VALUE_REQUIRED). Remitente/destinatario van inline (la plataforma crea o reutiliza los registros del directorio con dedupe exacto normalizado) o por referencia (from_id/to_id del directorio — la fila guardada se usa tal cual). El cargo sigue el ciclo autorizar→capturar/anular — un envío rechazado nunca deja fondos retenidos. Usa `Idempotency-Key` en todo intento.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| Idempotency-Key | header | — | UUID fresco por operación lógica (1–128 caracteres ASCII imprimibles; presente pero inválida → 400 IDEMPOTENCY_KEY_INVALID). Un reintento con la misma llave Y el mismo cuerpo repite la respuesta 2xx original (Idempotent-Replay: true) sin doble cargo, durante 15 minutos. La llave queda ligada al cuerpo exacto de la primera solicitud y al X-PDV-ID: cuerpo distinto → 409 IDEMPOTENCY_KEY_PAYLOAD_MISMATCH; pasados los 15 min → 409 IDEMPOTENCY_KEY_EXPIRED. Cambio 2026-08: ya NO se comparte espacio de nombres con el carril web del mismo usuario (antes una llave se repetía entre /v1/shipments y la app web; ahora eso responde 409 IDEMPOTENCY_KEY_REUSED). Fuertemente recomendado en POST /shipments. |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| rate_id | string | Sí | El id de una tarifa de POST /rates cotizada con esta misma llave (y mismo X-PDV-ID, si se usó), con menos de 30 minutos. Trátalo como OPACO: cópialo tal cual; su formato no es parte del contrato (difiere entre el carril de prueba y el live) y no debe interpretarse ni validarse. |
| from | ShipmentParty | — | |
| from_id | string | — | Id de un remitente del directorio. Mutuamente excluyente con from. |
| to | ShipmentParty | — | |
| to_id | string | — | Id de un destinatario del directorio (debe pertenecer al from_id enviado). Mutuamente excluyente con to; requiere from_id. |
| package | object | — | |
| packages | PackagesList | — | |
| insurance | object | — | La cobertura que YA se cotizó (insured_value buys coverage; when the platform policy is unavailable the carrier's own coverage is used and the price shown already includes it). Omitir insurance acepta la cobertura cotizada; mandar un insured_value DISTINTO → 422 INSURANCE_MISMATCH (el total firmado nunca se recalcula al crear). Una tarifa cuya paquetería no ofrece cobertura (insurance_not_supported: true) → 422 INSURANCE_NOT_AVAILABLE. |
| declared_value | number | — | Valor de la mercancía DECLARADO A LA PAQUETERÍA (MXN). NO es seguro, pero la paquetería puede cobrarlo: debe ser EXACTAMENTE el valor con el que se cotizó (el asegurado) — un valor que la cotización no llevaba responde 422 DECLARED_VALUE_MISMATCH, salvo en servicios que lo tratan como informativo (la recuperación de DECLARED_VALUE_REQUIRED). Si se omite se declara el valor cotizado; si la cotización no llevaba ninguno, no se inventa. *declared_value must equal the value the rate was quoted with; a carrier may price it.* |
| contenido | string | — | Descripción del contenido (aparece en la guía cuando la paquetería lo soporta). |
| reference | string | — | RESERVADO — aceptado pero no persistido en v1. |
| metadata | object | — | RESERVADO en el carril live (aceptado, no persistido). En el carril SANDBOX, metadata.test_scenario fuerza una falla para rehearsal (ver x-test-mode). |
Ejemplo
{
"rate_id": "e2etest_E2ETestCarrier_E2EStandard_ab12cd34",
"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"
}Respuestas
| Código | Descripción |
|---|---|
| 200 | Envío creado (o repetido idempotentemente — header Idempotent-Replay: true; o deduplicado contra una guía existente — duplicate: true, en cuyo caso carrier/service pueden ser null). |
| 400 | INVALID_JSON · VALIDATION_FAILED (rechazo del núcleo, details.validationErrors) · INVALID_VENDOR · OCURRE_CARRIER_MISMATCH · IDEMPOTENCY_KEY_INVALID (header presente pero vacío, >128 chars o con caracteres no imprimibles; omitirlo es válido, mandarlo mal no). |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 402 | CREDIT_ERROR — créditos insuficientes. details: {required, available, shortfall}. Recarga y reintenta con el MISMO Idempotency-Key. |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 409 | RATE_NOT_FOUND (cotiza de nuevo) · RATE_EXPIRED (>30 min) · RATE_TAMPERED · RATE_PDV_MISMATCH (se cotizó bajo otro PDV) · RATE_PACKAGE_MISMATCH (el package/packages enviado difiere del cotizado — medidas, peso o valor declarado — reenvía el MISMO conjunto que cotizaste, o cotiza de nuevo) · GUIA_COLLISION · IDEMPOTENCY_KEY_REUSED (misma llave en otro endpoint — desde 2026-08 el carril web cuenta como otro endpoint) · IDEMPOTENCY_KEY_PAYLOAD_MISMATCH (misma llave, cuerpo o X-PDV-ID distintos: manda una llave nueva) · IDEMPOTENCY_KEY_EXPIRED (la llave ya se completó hace más de 15 min: consulta GET /shipments o manda una llave nueva) · RATE_ADDRESS_MISMATCH (la cotización corresponde a OTRA ruta: los CP de origen/destino con los que se está creando el envío no son los que se cotizaron — cotiza de nuevo con las direcciones definitivas; desde 2026-08 esto rechaza en lugar de solo registrarse) · REQUOTE_REQUIRED (el precio del envío no coincide con el de la cotización; no se espera en este carril — el total se toma de la MISMA fila firmada que el ejecutor vuelve a verificar — y se documenta por completitud: cotiza de nuevo). |
| 422 | VALIDATION_ERROR (contrato público, details.fields — incluye CP inexistente en SEPOMEX) · INSURANCE_NOT_AVAILABLE (se mandó insurance sobre una tarifa cuya paquetería no ofrece cobertura — venía con insurance_not_supported: true; cotiza de nuevo y elige una con insurance_included, o crea sin insurance) · INSURANCE_MISMATCH (insurance.insured_value explícito distinto de la cobertura cotizada — details {quoted_insured_value, requested_insured_value}; omitirlo acepta la cobertura cotizada) · DECLARED_VALUE_MISMATCH (declared_value distinto del que se cotizó, o presente cuando la cotización no llevaba ninguno y la paquetería lo cobra — details {quoted_declared_value, requested_declared_value}; cotiza de nuevo) · DECLARED_VALUE_REQUIRED (el servicio exige declared_value; no se cobró) · PRICING_CONFIG_ERROR (margen mal configurado; contacta soporte) · MISSING_PHONE (remitente y/o destinatario sin teléfono válido de 10 dígitos; details.missing lista cuál). |
| 429 | RISK_LIMIT (tope de guías/gasto diario de la cuenta, o tope de gasto de la LLAVE en ventana móvil de 24 h — este último con details {daily_cap_mxn, spent_24h_mxn, attempted_mxn}) · RATE_LIMITED (límite por llave; ver Retry-After). |
| 500 | SHIPMENT_ERROR (falla pre-commit; el cargo se revirtió — details.refunded) · ADDRESS_ERROR (no se pudo registrar remitente/destinatario; reintenta) · CREDIT_ERROR (falla interna de créditos) · `POST_COMMIT_ERROR` — EL ENVÍO ES REAL y el cargo NO se revierte (`details.committed: true`). NO reintentes con llave nueva: consulta `GET /shipments`. |
| 502 | VENDOR_ERROR — la paquetería rechazó/falló ANTES del commit; el cargo se revirtió (details.refunded). details.vendorError: true; el status HTTP refleja el de la paquetería (4xx/5xx). Reintenta solo si retryable aplica (falla de red). Toda falla de paquetería trae `details.nextStep.action` — fix_data (corrige los campos de details.nextStep.fields: {party, field, label, problem, min?, max?}; código SHIPMENT_DATA_INVALID, 422), choose_other_option (otra opción de la misma cotización; SERVICE_UNAVAILABLE / CARRIER_ACCOUNT_BALANCE), check_shipments (la guía podría existir: consulta GET /shipments antes de reintentar; LABEL_MAY_EXIST), retry (reintenta una vez con el MISMO Idempotency-Key), add_declared_value, adjust_package. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| shipment | object | Sí | |
| duplicate | boolean | — | Presente (true) cuando la paquetería devolvió una guía ya registrada y la plataforma deduplicó sin doble cargo. |
| recovered | boolean | — | Presente (true) cuando la etiqueta se recuperó tras un error transitorio del proveedor. |
Ejemplo · 200
{
"success": true,
"data": {
"shipment": {
"id": "20260713-000123",
"guia": "1234567890",
"carrier": "Estafeta",
"service": "Terrestre",
"status": "created",
"total": 145.5,
"currency": "MXN",
"label_url": "/api/v1/labels/20260713-000123",
"tracking_pending": false,
"created_at": "2026-07-13T18:45:12Z"
}
},
"requestId": "req_b2c3d4e5f6"
}Consultar un envío
Misma visibilidad que el listado: propios por defecto, o los del PDV con X-PDV-ID + scope shipments:read:pdv. Devuelve exactamente los mismos campos que cada elemento de GET /shipments (incluyendo packages, pesos y remitente) — el detalle no es más rico que la lista, por diseño. Si solo tienes el número de guía, usa GET /shipments?guia=….
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | Id del envío (p. ej. 20260712-000123). |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | El envío. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — no existe o no es visible para la llave (respuesta idéntica en ambos casos). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| shipment | Shipment | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"shipment": {
"id": "20260713-000123",
"guia": "1234567890",
"carrier": "Estafeta",
"service": "Terrestre",
"status": "in_transit",
"status_label": "En tránsito",
"total": 145.5,
"currency": "MXN",
"created_at": "2026-07-13T18:45:12Z",
"tracking_pending": false,
"label_url": "/api/v1/labels/20260713-000123",
"packages": [
{
"weight_kg": 1,
"length_cm": 20,
"width_cm": 15,
"height_cm": 10
}
],
"billable_weight_kg": 1,
"declared_value": null,
"content": "Ropa",
"sender": {
"name": "Juan Pérez",
"city": "Ciudad de México",
"state": "CDMX",
"postal_code": "01000"
},
"recipient": {
"name": "María López",
"city": "Monterrey",
"state": "Nuevo León",
"postal_code": "64000"
}
}
},
"requestId": "req_f6a7b8c9d0"
}tracking
Rastreo público
Rastrear cualquier guía
Datos públicos de rastreo (mismo payload PII-mínimo de la página pública: estatus canónico, ciudad/estado de origen y destino, línea de tiempo con nombres de receptor depurados). No requiere que la guía sea propia. Agnóstico al modo de la llave (funciona igual con ek_test_). Requiere scope tracking:read.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| guia | path | Sí | Número de guía (coincidencia EXACTA). |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Estado de rastreo. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — genérico y de forma estable para toda guía desconocida/ inválida (sin oráculo de existencia). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| guia | string | Sí | |
| carrier | string | null | Sí | |
| status | string | Sí | |
| status_label | string | Sí | |
| origin | object | Sí | |
| destination | object | Sí | |
| created_at | string | null | Sí | |
| events | object[] | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"guia": "1234567890",
"carrier": "Estafeta",
"status": "in_transit",
"status_label": "En tránsito",
"origin": {
"city": "Ciudad de México",
"state": "CDMX"
},
"destination": {
"city": "Monterrey",
"state": "Nuevo León"
},
"created_at": "2026-07-13T18:45:12Z",
"events": [
{
"timestamp": "2026-07-13T20:10:00Z",
"description": "Recolectado",
"location": "Ciudad de México, CDMX"
},
{
"timestamp": "2026-07-14T09:30:00Z",
"description": "En tránsito al destino",
"location": "Querétaro, QRO"
}
]
},
"requestId": "req_c3d4e5f6a7"
}cancellations
Solicitudes de cancelación
Solicitar la cancelación de un envío propio
Crea una solicitud de cancelación (estado inicial pending). El avance del estado es operado por el staff/los procesos de la plataforma — consulta GET /cancellations/{id}. El reembolso esperado es lo que se cobró por el envío. Requiere scope cancellations:write.
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| shipment_id | string | Sí | Id del envío propio a cancelar. |
| reason_code | "wrong_address" | "duplicate" | "customer_changed_mind" | "damaged" | "never_shipped" | "not_picked_up" | "other" | — | |
| reason_text | string | — |
Ejemplo
{
"shipment_id": "20260713-000123",
"reason_code": "customer_changed_mind"
}Respuestas
| Código | Descripción |
|---|---|
| 201 | Solicitud creada. |
| 400 | INVALID_JSON · REQUEST_FAILED (rechazo del servicio). |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | FORBIDDEN (rechazo de permiso del servicio) · INSUFFICIENT_SCOPE · PDV_NOT_ALLOWED. |
| 404 | NOT_FOUND — el envío no existe o no pertenece al usuario de la llave (respuesta idéntica). |
| 409 | CONFLICT — el envío ya está cancelado o ya existe una solicitud abierta (details.existing_request_id). |
| 422 | VALIDATION_ERROR (details.fields). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | REQUEST_ERROR. |
Respuesta 201 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | integer | Sí | |
| display_id | string | Sí | Live: formato CR%08d. Sandbox: SBX-CR-<id>. |
| status | "pending" | "cancelled" | Sí | Live devuelve pending (la solicitud entra a la cola de staff). Sandbox devuelve cancelled (cancelación síncrona y terminal — ver x-test-mode). |
| refund_status | "not_applicable" | "refunded" | Sí | Live: not_applicable (reembolso lo resuelve el staff). Sandbox: refunded (reembolso inmediato). |
| refund_amount_expected | number | null | — | |
| cancellation_deadline | string | null | — | ISO-8601 UTC (Z). Siempre null en sandbox. |
Ejemplo · 201
{
"success": true,
"data": {
"id": 42,
"display_id": "CR00000042",
"status": "pending",
"refund_status": "not_applicable",
"refund_amount_expected": 145.5,
"cancellation_deadline": "2026-07-14T18:45:12Z"
},
"requestId": "req_a7b8c9d0e1"
}Consultar una solicitud de cancelación
Visibilidad idéntica a la del envío que cancela (propios por defecto; los del PDV con X-PDV-ID + scope shipments:read:pdv) — deliberadamente en paralelo, para que un envío que puedes leer nunca tenga una cancelación que no.
Máquina de estados: pending → approved → in_progress → cancelled | rejected_by_carrier; otros estados: not_cancellable (la guía no admite cancelación vía API — terminal), reported_to_carrier (reporte manual ante la paquetería, sigue abierta), rejected, failed, expired. Reembolso: not_applicable → refund_in_progress → refunded | refund_denied (los reembolsos son pass-through de la paquetería; refunded cubre también el reembolso manual por override administrativo). Requiere scope cancellations:read.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | La solicitud. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — no existe o el envío subyacente no pertenece al usuario de la llave (respuesta idéntica). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | GET_ERROR. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| cancellation | Cancellation | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"cancellation": {
"id": 42,
"display_id": "CR00000042",
"shipment_id": "20260713-000123",
"status": "cancelled",
"refund_status": "refunded",
"reason_code": "customer_changed_mind",
"reason_text": null,
"refund_amount_expected": 145.5,
"refund_amount_actual": 145.5,
"cancellation_deadline": "2026-07-14T18:45:12Z",
"created_at": "2026-07-13T19:02:33Z",
"updated_at": "2026-07-15T10:12:05Z",
"resolved_at": "2026-07-15T10:12:05Z"
}
},
"requestId": "req_b8c9d0e1f2"
}pickups
Recolecciones a domicilio (solicitudes; las confirma nuestro equipo)
Listar tus solicitudes de recolección
Las solicitudes de recolección de la cuenta, de la más reciente a la más antigua — qué pediste, para cuándo, y si nuestro equipo ya la gestionó.
Visibilidad idéntica a la del listado de envíos: por omisión las recolecciones de los envíos que creó el usuario de la llave; con X-PDV-ID validado y el scope ampliado shipments:read:pdv, las de ese punto de venta (las dos ramas son EXCLUYENTES). Mientras la programación de recolecciones no esté habilitada en la cuenta, este listado responde 200 con una página vacía — un listado de algo que no puedes usar no tiene nada que reportar. Requiere scope pickups:read.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| page | query | — | |
| limit | query | — | |
| status | query | — | Filtra por estatus público de la recolección — el mismo valor que devuelve el campo status. awaiting_confirmation = la registramos y nuestro equipo aún la está gestionando; scheduled = ya quedó con la paquetería. Un valor fuera del enum es 422 VALIDATION_ERROR. |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Página de solicitudes de recolección. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 422 | VALIDATION_ERROR — status fuera del enum o paginación inválida (campos en details.fields). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | GET_ERROR. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| pickups | Pickup[] | Sí | |
| pagination | Pagination | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"pickups": [
{
"id": 42,
"display_id": "PR00000042",
"shipment_id": "20260830-000123",
"status": "awaiting_confirmation",
"carrier": "Estafeta",
"requested_window_start": "2026-08-31T16:00:00Z",
"requested_window_end": "2026-08-31T23:00:00Z",
"confirmed_window_start": null,
"confirmed_window_end": null,
"confirmation_number": null,
"created_at": "2026-08-30T10:00:00Z",
"updated_at": "2026-08-30T10:00:02Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1
}
},
"requestId": "req_a7b8c9d0e1"
}Solicitar la recolección de un envío propio
Solicita que la paquetería pase por el paquete al domicilio del remitente, en lugar de que el remitente lo lleve a sucursal.
Esto es una solicitud, no una reserva. No agendamos con la paquetería de forma automática: el equipo de Enviadores la gestiona y avisa por correo al solicitante cuando queda. Por eso la respuesta siempre regresa status: awaiting_confirmation — nunca reportes al usuario que ya hay mensajero confirmado. Consulta GET /pickups/{id} para el estado final, o espera el correo.
Por qué. Un 200 de una paquetería no es prueba de que el mensajero vaya a llegar, y el peor error posible aquí es decirle a alguien que su recolección está confirmada y dejarlo esperando. Preferimos ser lentos y ciertos.
Disponible sólo para Estafeta, DHL, FedEx y Paquetexpress (422 PICKUP_CARRIER_NOT_ELIGIBLE para el resto; el paquete siempre puede entregarse en sucursal). Una sola recolección abierta por envío.
pickup_date, ready_time y close_time son hora local de México (America/Mexico_City); la respuesta devuelve las ventanas en UTC (Z).
La recolección no cobra saldo; cualquier cargo de la paquetería llega después por la vía de sobrecargos, igual que en el carril web.
La dirección de recolección es la del propio envío — este endpoint no la cambia. Requiere scope pickups:write.
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| shipment_id | string | Sí | Id de un envío propio con guía, no entregado ni cancelado. |
| pickup_date | string | Sí | Fecha local (America/Mexico_City). Hoy o después. |
| ready_time | string | Sí | HH:MM 24h local — desde cuándo está listo el paquete. |
| close_time | string | Sí | HH:MM 24h local — hasta cuándo puede pasar el mensajero. Debe ser posterior a ready_time. |
| notes | string | — | Indicación para el mensajero. |
Ejemplo
{
"shipment_id": "20260830-000123",
"pickup_date": "2026-08-31",
"ready_time": "10:00",
"close_time": "17:00",
"notes": "Timbre azul, preguntar por Ana"
}Respuestas
| Código | Descripción |
|---|---|
| 201 | Solicitud registrada — status siempre es awaiting_confirmation. Un 201 significa que recibimos la solicitud, NO que haya mensajero confirmado. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | PICKUP_UNAVAILABLE — la programación de recolecciones aún no está habilitada en esta cuenta (el paquete puede entregarse en sucursal; escríbenos para agendarla manualmente). · PICKUP_UNLOCK_REQUIRED — la cuenta todavía sólo puede usar servicios sin recolección; se desbloquea al verificar identidad (INE) en Mi cuenta o al acumular $2,500 MXN en recargas. · FORBIDDEN · INSUFFICIENT_SCOPE · PDV_NOT_ALLOWED. |
| 404 | NOT_FOUND — el envío no existe o no es de esta llave (respuesta idéntica, sin oráculo de existencia). |
| 409 | CONFLICT — ya existe una recolección abierta para este envío (details.existing_request_id). |
| 422 | VALIDATION_ERROR (campos en details.fields) · PICKUP_CARRIER_NOT_ELIGIBLE — la paquetería del envío no está en la lista (details.eligible_carriers); el paquete puede entregarse en sucursal · REQUEST_FAILED — el envío no admite recolección (sin guía, entregado, cancelado, o fecha/hora imposible). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | REQUEST_ERROR. |
Respuesta 201 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| pickup | Pickup | Sí |
Ejemplo · 201
{
"success": true,
"data": {
"pickup": {
"id": 42,
"display_id": "PR00000042",
"shipment_id": "20260830-000123",
"status": "awaiting_confirmation",
"carrier": "Estafeta",
"requested_window_start": "2026-08-31T16:00:00Z",
"requested_window_end": "2026-08-31T23:00:00Z",
"confirmed_window_start": null,
"confirmed_window_end": null,
"confirmation_number": null,
"created_at": "2026-08-30T10:00:00Z",
"updated_at": "2026-08-30T10:00:02Z"
}
},
"requestId": "req_a7b8c9d0e1"
}Consultar una recolección
Estado de una solicitud de recolección propia — úsalo para dar seguimiento mientras está en awaiting_confirmation (el solicitante también recibe un correo cuando se resuelve). Visibilidad idéntica a la del envío al que pertenece, en paralelo deliberado con GET /cancellations/{id}. Requiere scope pickups:read.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | La recolección. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — no existe o el envío subyacente no pertenece al usuario de la llave (respuesta idéntica). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | GET_ERROR. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| pickup | Pickup | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"pickup": {
"id": 42,
"display_id": "PR00000042",
"shipment_id": "20260830-000123",
"status": "awaiting_confirmation",
"carrier": "Estafeta",
"requested_window_start": "2026-08-31T16:00:00Z",
"requested_window_end": "2026-08-31T23:00:00Z",
"confirmed_window_start": null,
"confirmed_window_end": null,
"confirmation_number": null,
"created_at": "2026-08-30T10:00:00Z",
"updated_at": "2026-08-30T10:00:02Z"
}
},
"requestId": "req_a7b8c9d0e1"
}balance
Saldo de créditos
Consultar el saldo que cargarían tus envíos
Sin X-PDV-ID: el saldo del usuario de la llave. Con X-PDV-ID (llaves admin, misma allowlist que POST /shipments): el fondo del punto de venta — exactamente el principal que un envío bajo ese header cargaría. available es lo gastable; held son autorizaciones pendientes; balance = available + held. Requiere scope balance:read.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Saldo del principal. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| balance | number | Sí | available + held (total del principal en el libro mayor). |
| held | number | Sí | Autorizaciones pendientes (holds). |
| available | number | Sí | Lo gastable por un envío nuevo. |
| currency | "MXN" | Sí | |
| scope | object | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"balance": 4820.5,
"held": 145.5,
"available": 4675,
"currency": "MXN",
"scope": {
"type": "user",
"id": "U0000001"
}
},
"requestId": "req_d4e5f6a7b8"
}labels
Etiquetas (guías)
Descargar la etiqueta de un envío propio
Transmite la copia de la etiqueta que guarda la plataforma (PDF/ZPL); si aún no existe, la obtiene de la paquetería, la guarda y la transmite. Nunca redirige a la URL de la paquetería. Con ?format=json responde JSON (LabelLink) en lugar de bytes. Solo envíos creados por el usuario de la llave (todos los roles). Requiere scope labels:read.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| envio_id | path | Sí | |
| format | query | — | json = en vez de transmitir bytes responde JSON con un enlace firmado y con vencimiento a la copia de Enviadores (LabelLink). Es la vía para agentes (la herramienta MCP get_label): un agente no puede consumir el PDF transmitido, así que un enlace que se abre sin llave es la única respuesta útil. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Bytes de la etiqueta (copia de la plataforma). Con ?format=json: application/json con LabelLink. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — no existe, no es del usuario de la llave, o no hay etiqueta disponible (respuesta idéntica en todos los casos). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| shipment_id | string | Sí | |
| guia | string | — | Solo en sandbox (la vía en vivo omite el campo). |
| label_url | string | null | Sí | En vivo: enlace firmado a la copia de Enviadores (https://api.enviadores.com.mx/api/v1/label-files/{envio_id}/{exp}/{firma}.pdf). Se abre sin llave, vence en expires_at (30 días; pide uno nuevo con esta misma llamada) y nunca es la URL de la paquetería. En sandbox: enlace a la etiqueta de prueba. null cuando la etiqueta aún no está disponible; note lo explica. |
| expires_at | string | — | Solo en vivo: cuándo deja de funcionar label_url (UTC). |
| format | "pdf" | Sí | |
| sandbox | boolean | Sí | true con llaves ek_test_: etiqueta de prueba sin validez. |
| note | string | — | Presente solo cuando hay algo que aclarar (etiqueta sandbox, o sin URL directa). |
addresses
Directorio: remitentes y destinatarios
Listar remitentes del directorio
Remitentes visibles para la llave. Visibilidad: llaves de cliente y llaves admin SIN X-PDV-ID ven solo los registros creados por el usuario de la llave; una llave admin con X-PDV-ID validado ve exactamente el directorio de ese punto de venta (el mismo que usa el personal de mostrador). Nunca hay lectura global en este carril. Requiere scope addresses:read. Este endpoint SÍ devuelve dirección completa y teléfono — el guardián es la visibilidad, no el recorte de campos.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
| page | query | — | |
| limit | query | — | |
| postal_code | query | — | Filtro exacto por CP (5 dígitos; otro formato → 422). |
| q | query | — | Búsqueda de texto sobre nombre, razón social, RFC, teléfono, email y dirección (calle/colonia/municipio/estado/CP). Cada palabra debe aparecer en ALGÚN campo, no necesariamente en el mismo: arturo meijueiro encuentra un registro cuyo nombre es ARTURO y cuyo apellido solo aparece en el email. Insensible a acentos y mayúsculas; se consideran las primeras 5 palabras. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Página de remitentes. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 422 | VALIDATION_ERROR — filtro inválido (details.fields). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| senders | Sender[] | Sí | |
| pagination | Pagination | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"senders": [
{
"id": "C0012345",
"name": "Juan Pérez",
"apellido_paterno": null,
"apellido_materno": null,
"company": null,
"rfc": null,
"phone": "5512345678",
"email": "[email protected]",
"street": "Av. Insurgentes Sur",
"number": "600",
"colonia": "Del Valle",
"city": "Ciudad de México",
"state": "CDMX",
"postal_code": "01000",
"created_at": "2026-07-13T18:40:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1
}
},
"requestId": "req_c9d0e1f2a3"
}Crear (o reutilizar) un remitente
Create-or-reuse con el MISMO dedupe exacto normalizado que usa POST /shipments al registrar direcciones inline, evaluado sobre las filas visibles para la llave: si ya existe un remitente idéntico visible, se devuelve ese (created: false, 200); si no, se crea (created: true, 201). El postal_code debe tener 5 dígitos y EXISTIR en el catálogo postal (SEPOMEX); si city/state vienen, deben ser coherentes con el CP (un desajuste responde 422 nombrando el valor esperado) y si se omiten se AUTOCOMPLETAN del catálogo — la misma validación de exactitud que POST /contacts/import. Requiere scope addresses:write. Con X-PDV-ID (llave admin), la fila creada pertenece al directorio de ese PDV y queda visible para su personal.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| name | string | Sí | Nombre de pila (nombre). |
| apellido_paterno | string | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| apellido_materno | string | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| company | string | — | Razón social. |
| rfc | string | — | |
| phone | string | Sí | |
| string | — | ||
| street | string | Sí | |
| number | string | — | Número exterior. |
| colonia | string | Sí | |
| city | string | — | Opcional: si se omite se autocompleta del catálogo SEPOMEX según el CP; si viene, debe coincidir con el CP (si no → 422 nombrando la ciudad esperada). |
| state | string | — | Opcional: si se omite se autocompleta del catálogo SEPOMEX según el CP; si viene, debe coincidir con el CP (si no → 422 nombrando el estado esperado). |
| postal_code | string | Sí | 5 dígitos; debe EXISTIR en el catálogo postal (SEPOMEX). |
| country | "MX" | — | Opcional. Solo MX — cualquier otro valor responde 422. |
Ejemplo
{
"name": "Juan Pérez García",
"phone": "5512345678",
"email": "[email protected]",
"street": "Av. Insurgentes Sur",
"number": "600",
"colonia": "Del Valle",
"city": "Ciudad de México",
"state": "CDMX",
"postal_code": "01000"
}Respuestas
| Código | Descripción |
|---|---|
| 200 | Remitente idéntico ya existente reutilizado (created: false). |
| 201 | Remitente creado (created: true). |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 422 | VALIDATION_ERROR — campos faltantes/inválidos o desconocidos (details.fields). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | ADDRESS_ERROR — no se pudo registrar; reintenta. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| sender | Sender | Sí | |
| created | boolean | Sí |
Respuesta 201 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| sender | Sender | Sí | |
| created | boolean | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"sender": {
"id": "C0012345",
"name": "Juan Pérez",
"apellido_paterno": null,
"apellido_materno": null,
"company": null,
"rfc": null,
"phone": "5512345678",
"email": "[email protected]",
"street": "Av. Insurgentes Sur",
"number": "600",
"colonia": "Del Valle",
"city": "Ciudad de México",
"state": "CDMX",
"postal_code": "01000",
"created_at": "2026-07-13T18:40:00Z"
},
"created": false
},
"requestId": "req_e1f2a3b4c5"
}Ejemplo · 201
{
"success": true,
"data": {
"sender": {
"id": "C0012345",
"name": "Juan Pérez",
"apellido_paterno": null,
"apellido_materno": null,
"company": null,
"rfc": null,
"phone": "5512345678",
"email": "[email protected]",
"street": "Av. Insurgentes Sur",
"number": "600",
"colonia": "Del Valle",
"city": "Ciudad de México",
"state": "CDMX",
"postal_code": "01000",
"created_at": "2026-07-13T18:40:00Z"
},
"created": true
},
"requestId": "req_d0e1f2a3b4"
}Consultar un remitente
Requiere scope addresses:read. 404 idéntico para inexistente y no-visible (sin oráculo).
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | El remitente. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| sender | Sender | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"sender": {
"id": "C0012345",
"name": "Juan Pérez",
"apellido_paterno": null,
"apellido_materno": null,
"company": null,
"rfc": null,
"phone": "5512345678",
"email": "[email protected]",
"street": "Av. Insurgentes Sur",
"number": "600",
"colonia": "Del Valle",
"city": "Ciudad de México",
"state": "CDMX",
"postal_code": "01000",
"created_at": "2026-07-13T18:40:00Z"
}
},
"requestId": "req_f2a3b4c5d6"
}Editar un remitente
Actualización PARCIAL de un remitente visible. Requiere scope addresses:write. Solo los campos presentes cambian; se debe enviar al menos uno. Los requeridos pueden cambiarse pero no vaciarse; los opcionales aceptan null para limpiarse. Los apellidos NO forman parte del dedupe, así que PATCH es la forma de completarlos en una fila creada sin ellos. 404 idéntico para inexistente y no-visible (sin oráculo).
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| name | string | — | |
| apellido_paterno | string | null | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| apellido_materno | string | null | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| company | string | null | — | |
| rfc | string | null | — | |
| phone | string | — | |
| string | null | — | ||
| street | string | — | |
| number | string | null | — | |
| colonia | string | — | |
| city | string | — | |
| state | string | — | |
| postal_code | string | — | |
| country | "MX" | — |
Ejemplo
{
"name": "Juan Pérez García",
"email": null
}Respuestas
| Código | Descripción |
|---|---|
| 200 | El remitente actualizado. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 409 | DUPLICATE_ADDRESS — la edición dejaría este remitente idéntico a OTRO ya visible para la llave. La fila no se modifica. |
| 422 | VALIDATION_ERROR (details.fields) — campo desconocido, requerido vaciado, country ≠ MX, apellido (deprecated) > 50, o cuerpo sin campos editables. |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| sender | Sender | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"sender": {
"id": "C0012345",
"name": "Juan Pérez García",
"apellido_paterno": null,
"apellido_materno": null,
"company": null,
"rfc": null,
"phone": "5512345678",
"email": "[email protected]",
"street": "Av. Insurgentes Sur",
"number": "600",
"colonia": "Del Valle",
"city": "Ciudad de México",
"state": "CDMX",
"postal_code": "01000",
"created_at": "2026-07-13T18:40:00Z"
}
},
"requestId": "req_a3b4c5d6e7"
}Listar destinatarios del directorio
Misma visibilidad que GET /senders. Un destinatario siempre pertenece a un remitente (sender_id). Requiere scope addresses:read.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
| page | query | — | |
| limit | query | — | |
| sender_id | query | — | Solo destinatarios de este remitente. |
| postal_code | query | — | |
| q | query | — | Búsqueda de texto sobre nombre, alias, teléfono, email y dirección (calle/colonia/ciudad/estado/CP). Cada palabra debe aparecer en ALGÚN campo, no necesariamente en el mismo. Insensible a acentos y mayúsculas; se consideran las primeras 5 palabras. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Página de destinatarios. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 422 | VALIDATION_ERROR — filtro inválido (details.fields). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| recipients | Recipient[] | Sí | |
| pagination | Pagination | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"recipients": [
{
"id": "D0067890",
"sender_id": "C0012345",
"alias": "Oficina",
"name": "María López",
"phone": "8187654321",
"email": null,
"street": "Av. Constitución",
"number": "400",
"colonia": "Centro",
"city": "Monterrey",
"state": "Nuevo León",
"postal_code": "64000",
"referencia": "Edificio azul, junto a la farmacia",
"delivery_instructions": "Entregar en recepción",
"created_at": "2026-07-13T18:41:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1
}
},
"requestId": "req_b4c5d6e7f8"
}Crear (o reutilizar) un destinatario
Create-or-reuse bajo el remitente sender_id (debe ser visible para la llave; si no → 404 ciego). Mismo dedupe normalizado que el carril de envíos; en una reutilización, alias/referencia/delivery_instructions NO se aplican (no forman parte de la identidad — la fila existente se devuelve intacta). El postal_code debe tener 5 dígitos y EXISTIR en el catálogo postal (SEPOMEX); si city/state vienen, deben ser coherentes con el CP (un desajuste responde 422 nombrando el valor esperado) y si se omiten se AUTOCOMPLETAN del catálogo — la misma validación de exactitud que POST /contacts/import. Requiere scope addresses:write.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| sender_id | string | Sí | Remitente dueño (visible para la llave; si no → 404 ciego). |
| alias | string | — | Nombre descriptivo ("Casa", "Oficina"). |
| name | string | Sí | |
| phone | string | Sí | |
| string | — | ||
| street | string | Sí | |
| number | string | — | |
| colonia | string | Sí | |
| city | string | — | Opcional: si se omite se autocompleta del catálogo SEPOMEX según el CP; si viene, debe coincidir con el CP (si no → 422 nombrando la ciudad esperada). |
| state | string | — | Opcional: si se omite se autocompleta del catálogo SEPOMEX según el CP; si viene, debe coincidir con el CP (si no → 422 nombrando el estado esperado). |
| postal_code | string | Sí | 5 dígitos; debe EXISTIR en el catálogo postal (SEPOMEX). |
| country | "MX" | — | |
| referencia | string | — | Referencias del domicilio. |
| delivery_instructions | string | — |
Ejemplo
{
"sender_id": "C0012345",
"alias": "Oficina",
"name": "María López",
"phone": "8187654321",
"street": "Av. Constitución",
"number": "400",
"colonia": "Centro",
"city": "Monterrey",
"state": "Nuevo León",
"postal_code": "64000",
"referencia": "Edificio azul, junto a la farmacia",
"delivery_instructions": "Entregar en recepción"
}Respuestas
| Código | Descripción |
|---|---|
| 200 | Destinatario idéntico ya existente reutilizado (created: false). |
| 201 | Destinatario creado (created: true). |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — sender_id inexistente o no visible (respuesta idéntica). |
| 422 | VALIDATION_ERROR — campos faltantes/inválidos o desconocidos (details.fields). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | ADDRESS_ERROR — no se pudo registrar; reintenta. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| recipient | Recipient | Sí | |
| created | boolean | Sí |
Respuesta 201 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| recipient | Recipient | Sí | |
| created | boolean | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"recipient": {
"id": "D0067890",
"sender_id": "C0012345",
"alias": "Oficina",
"name": "María López",
"phone": "8187654321",
"email": null,
"street": "Av. Constitución",
"number": "400",
"colonia": "Centro",
"city": "Monterrey",
"state": "Nuevo León",
"postal_code": "64000",
"referencia": "Edificio azul, junto a la farmacia",
"delivery_instructions": "Entregar en recepción",
"created_at": "2026-07-13T18:41:00Z"
},
"created": false
},
"requestId": "req_d6e7f8a9b0"
}Ejemplo · 201
{
"success": true,
"data": {
"recipient": {
"id": "D0067890",
"sender_id": "C0012345",
"alias": "Oficina",
"name": "María López",
"phone": "8187654321",
"email": null,
"street": "Av. Constitución",
"number": "400",
"colonia": "Centro",
"city": "Monterrey",
"state": "Nuevo León",
"postal_code": "64000",
"referencia": "Edificio azul, junto a la farmacia",
"delivery_instructions": "Entregar en recepción",
"created_at": "2026-07-13T18:41:00Z"
},
"created": true
},
"requestId": "req_c5d6e7f8a9"
}Consultar un destinatario
Requiere scope addresses:read. 404 idéntico para inexistente y no-visible (sin oráculo).
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | El destinatario. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| recipient | Recipient | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"recipient": {
"id": "D0067890",
"sender_id": "C0012345",
"alias": "Oficina",
"name": "María López",
"phone": "8187654321",
"email": null,
"street": "Av. Constitución",
"number": "400",
"colonia": "Centro",
"city": "Monterrey",
"state": "Nuevo León",
"postal_code": "64000",
"referencia": "Edificio azul, junto a la farmacia",
"delivery_instructions": "Entregar en recepción",
"created_at": "2026-07-13T18:41:00Z"
}
},
"requestId": "req_e7f8a9b0c1"
}Editar un destinatario
Actualización PARCIAL de un destinatario visible. Requiere scope addresses:write. sender_id es inmutable (no se re-asigna de remitente). Solo los campos presentes cambian; al menos uno. Los requeridos pueden cambiarse pero no vaciarse; los opcionales aceptan null. 404 idéntico para inexistente y no-visible.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| alias | string | null | — | |
| name | string | — | |
| phone | string | — | |
| string | null | — | ||
| street | string | — | |
| number | string | null | — | |
| colonia | string | — | |
| city | string | — | |
| state | string | — | |
| postal_code | string | — | |
| country | "MX" | — | |
| referencia | string | null | — | |
| delivery_instructions | string | null | — |
Ejemplo
{
"phone": "8187654322",
"delivery_instructions": null
}Respuestas
| Código | Descripción |
|---|---|
| 200 | El destinatario actualizado. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 409 | DUPLICATE_ADDRESS — la edición dejaría este destinatario idéntico a OTRO del MISMO remitente. La fila no se modifica. |
| 422 | VALIDATION_ERROR (details.fields) — campo desconocido, sender_id presente (inmutable), requerido vaciado, country ≠ MX, o cuerpo sin campos editables. |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| recipient | Recipient | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"recipient": {
"id": "D0067890",
"sender_id": "C0012345",
"alias": "Oficina",
"name": "María López",
"phone": "8187654322",
"email": null,
"street": "Av. Constitución",
"number": "400",
"colonia": "Centro",
"city": "Monterrey",
"state": "Nuevo León",
"postal_code": "64000",
"referencia": "Edificio azul, junto a la farmacia",
"delivery_instructions": "Entregar en recepción",
"created_at": "2026-07-13T18:41:00Z"
}
},
"requestId": "req_f8a9b0c1d2"
}Validar un código postal y listar sus colonias
Resuelve un código postal mexicano (catálogo SEPOMEX) en su estado, municipio y el conjunto de colonias/asentamientos — justo lo que se necesita para llenar una dirección antes de POST /senders o POST /shipments. Requiere scope addresses:read. Datos de referencia públicos y de solo lectura: responde IDÉNTICo con llaves ek_live_ y ek_test_ (no hay nada que simular). 422 si el CP no son 5 dígitos; 404 si no existe en el catálogo; 200 con los datos si es válido.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| postal_code | path | Sí | Código postal de 5 dígitos. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | El código postal existe. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — el código postal (bien formado) no está en el catálogo SEPOMEX. |
| 422 | VALIDATION_ERROR — el código postal debe ser exactamente 5 dígitos. |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| postal_code | string | Sí | El CP consultado (5 dígitos). |
| state | string | null | Sí | Estado (d_estado). Úsalo como state. |
| state_code | string | null | Sí | Código de estado normalizado (mismo esquema que state_code del directorio; p. ej. DF, JAL, NL). |
| municipality | string | null | Sí | Municipio/alcaldía (d_mnpio). Suele ser el mejor valor para city. |
| city | string | null | Sí | Ciudad (d_ciudad) cuando SEPOMEX la distingue del municipio; null en la mayoría de los CP. |
| colonias | Colonia[] | Sí | Asentamientos únicos del CP, ordenados por nombre. Un CP mapea a un estado/municipio pero a muchas colonias. |
Ejemplo · 200
{
"success": true,
"data": {
"postal_code": "64000",
"state": "Nuevo León",
"state_code": "NL",
"municipality": "Monterrey",
"city": "Monterrey",
"colonias": [
{
"name": "Centro",
"settlement_type": "Colonia"
}
]
},
"requestId": "req_a9b0c1d2e3"
}Importar contactos al directorio en bloque (remitentes y destinatarios)
Importación masiva del directorio — pensada para migrar contactos desde una hoja de cálculo, otra plataforma o una lista fotografiada, típicamente a través de un asistente de IA. Requiere scope addresses:write (es el mismo alcance de POST /senders / POST /recipients: una importación son esos dos creates en bloque).
Dos carriles de entrada, mutuamente excluyentes:
- contacts: arreglo de filas {type: 'sender'|'recipient', name, phone, street, number?, colonia, city?, state?, postal_code, email?, company?, rfc?} — el mismo vocabulario de campos que POST /senders / POST /recipients (los destinatarios además aceptan sender_id?, alias?, referencia?, delivery_instructions?). El cliente (p. ej. el modelo) debe mapear los datos del usuario TAL CUAL, nunca inventar valores.
- raw_csv: el contenido del CSV del usuario, VERBATIM (UTF-8, BOM opcional, máx. 256KB). El servidor lo interpreta de forma determinista: delimitador coma/punto y coma/tabulador (detectado), fila de encabezado obligatoria con las columnas de la plantilla tipo,nombre,telefono,calle,numero,colonia,ciudad,estado,codigo_postal,email,empresa,rfc (coincidencia insensible a mayúsculas y acentos, cualquier orden; columnas desconocidas → 422, nunca adivinadas). tipo acepta remitente/destinatario (o sender/recipient).
Dos fases sin estado: dry_run: true (el DEFAULT — el modo seguro) valida todo y devuelve la vista previa normalizada sin escribir nada; dry_run: false re-valida e inserta. La seguridad ante reintentos viene del DEDUPE POR CONTENIDO, no de claims: una fila idéntica (tupla normalizada nombre+teléfono+dirección+CP — el MISMO dedupe de POST /senders) a un contacto existente visible se reporta duplicate y se omite, así que repetir un commit jamás duplica el directorio.
Validación por fila (nunca aborta el lote completo): codigo_postal de 5 dígitos y existente en el catálogo SEPOMEX; si ciudad/estado vienen, deben ser coherentes con el CP (un desajuste nombra el valor esperado) y si faltan se AUTOCOMPLETAN del catálogo; telefono se normaliza a 10 dígitos (se toleran espacios, guiones y el prefijo +52/521); email con formato válido cuando venga. Cada fila responde status: ok|duplicate|invalid con issues[] en español nombrando campo y motivo.
Padre de los destinatarios: un destino siempre pertenece a un remitente. Orden de resolución: sender_id explícito de la fila → el PRIMER remitente válido del mismo lote → el remitente más reciente del directorio. Sin candidato, la fila es invalid con instrucciones.
Topes: máximo 200 filas por llamada (más → 422 pidiendo trocear el lote) y 256KB de raw_csv.
Conteos (invariantes): received = valid + invalid, valid = filas ok + duplicates; en commit además created y skipped con received = created + skipped + invalid — imposible perder filas en silencio.
Con una llave ek_test_ la importación escribe en el directorio del sandbox (mismas reglas, mismos formatos de respuesta). La visibilidad/destino de las filas es la misma de POST /senders: llaves de cliente y llaves admin sin X-PDV-ID escriben como el usuario de la llave; una llave admin con X-PDV-ID validado escribe en el directorio de ese punto de venta.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| contacts | object[] | — | Filas mapeadas tal cual de los datos del usuario. Mutuamente excluyente con raw_csv. |
| raw_csv | string | — | CSV del usuario, verbatim (UTF-8, máx. 256KB). Mutuamente excluyente con contacts. |
| dry_run | boolean | — | true (default): valida y previsualiza sin escribir. false: re-valida e inserta. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Resultado por fila + resumen. En dry_run nada se escribió; en commit, created/skipped reportan lo insertado/omitido. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 422 | VALIDATION_ERROR — error de contrato (ambos carriles a la vez, ninguno, campo desconocido, más de 200 filas → trocear el lote, CSV >256KB) o de forma del CSV (encabezado con columnas desconocidas/faltantes, no-UTF-8), con los motivos en details.fields. Los problemas POR FILA nunca son 422: viajan como status: invalid dentro de rows. |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| dry_run | boolean | Sí | |
| rows | object[] | Sí | |
| summary | object | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"dry_run": false,
"rows": [
{
"index": 0,
"line": 2,
"type": "sender",
"name": "Juan Pérez",
"status": "ok",
"issues": [],
"contact": {
"type": "sender",
"name": "Juan Pérez",
"phone": "5512345678",
"street": "Av. Insurgentes Sur",
"number": "600",
"colonia": "San Ángel",
"city": "Ciudad de México",
"state": "Ciudad de México",
"postal_code": "01000"
},
"id": "C0012399"
},
{
"index": 1,
"line": 3,
"type": "recipient",
"name": "Ana López",
"status": "duplicate",
"issues": [
"ya existe un contacto idéntico en tu directorio (se omite)"
],
"contact": null,
"id": "D0045012"
},
{
"index": 2,
"line": 4,
"type": "sender",
"name": "Mal Teléfono",
"status": "invalid",
"issues": [
"telefono: debe tener 10 dígitos después de quitar espacios, guiones y el prefijo +52 (recibido «123»)"
],
"contact": null,
"id": null
}
],
"summary": {
"received": 3,
"valid": 2,
"invalid": 1,
"duplicates": 1,
"created": 1,
"skipped": 1
}
},
"requestId": "req_f1e2d3c4b5"
}webhooks
Webhooks de eventos (envíos y recolecciones) — alternativa al polling
Listar webhooks
Todos los webhooks de la cuenta en el modo de la llave (live o sandbox), incluidos los desactivados automáticamente — status, failure_count (fallos consecutivos) y last_delivery_* explican por qué dejaron de llegar eventos. Nunca incluye secretos. Requiere scope webhooks:read.
Respuestas
| Código | Descripción |
|---|---|
| 200 | Lista + el catálogo de eventos y el tope activo. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| webhooks | Webhook[] | Sí | |
| max_active | integer | Sí | Tope de webhooks activos por cuenta y modo. |
| events | string[] | Sí | Catálogo completo de eventos suscribibles. |
Ejemplo · 200
{
"success": true,
"data": {
"webhooks": [
{
"id": "wh_0f3a9c1d2e4b5a6978c0d1e2",
"url": "https://hooks.mitienda.com/enviadores",
"events": [
"shipment.created",
"shipment.delivered"
],
"mode": "live",
"status": "active",
"failure_count": 0,
"last_delivery_at": "2026-09-04T16:05:12Z",
"last_delivery_status": 200,
"created_at": "2026-09-04T16:00:00Z",
"disabled_at": null,
"disabled_reason": null
}
],
"max_active": 5,
"events": [
"shipment.created",
"shipment.collected",
"shipment.in_transit",
"shipment.out_for_delivery",
"shipment.delivered",
"shipment.exception",
"shipment.returned",
"shipment.cancelled",
"pickup.resolved"
]
},
"requestId": "req_a7b8c9d0e1"
}Registrar un webhook
Registra un endpoint https:// público que recibirá un POST firmado por cada evento suscrito. El `secret` se devuelve UNA sola vez en esta respuesta: no se almacena (se deriva bajo una llave del servidor) y no vuelve a mostrarse; guárdalo al recibirlo. Máximo 5 webhooks activos por cuenta y modo (409 WEBHOOK_LIMIT_REACHED). La URL se valida contra SSRF: solo https://, hostname público (se rechazan localhost, .local, .internal, literales IP privadas/loopback/link-local y hosts que resuelvan a ellas) y sin credenciales embebidas; en cada entrega se vuelve a resolver el host y la conexión se fija a la IP pública verificada. Con llave ek_test_ el webhook es de sandbox (solo shipment.created/shipment.cancelled del carril de pruebas + ping). Requiere scope webhooks:write.
Cuerpo de la petición
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| url | string | Sí | Endpoint https:// público en el puerto 443 u 8443 (cualquier otro puerto se rechaza). Se rechazan http, credenciales embebidas, fragmentos, localhost/.local/.internal, literales IP privadas, hosts que resuelvan a rangos privados y hosts que apunten al propio API de Enviadores. |
| events | WebhookEvent[] | Sí | Eventos a recibir (duplicados se descartan). |
Ejemplo
{
"url": "https://hooks.mitienda.com/enviadores",
"events": [
"shipment.created",
"shipment.delivered",
"shipment.exception"
]
}Respuestas
| Código | Descripción |
|---|---|
| 201 | Webhook registrado. secret aparece SOLO aquí. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — los webhooks no están habilitados en este despliegue (respuesta idéntica a una ruta inexistente). |
| 409 | WEBHOOK_LIMIT_REACHED — ya hay 5 webhooks activos en este modo (details {max_active, mode}). Elimina uno con DELETE /webhooks/{id}. |
| 422 | VALIDATION_ERROR — details.fields nombra el problema: URL no https://, puerto distinto de 443/8443, host privado/no resoluble/propio del API, credenciales embebidas, events vacío o con un evento desconocido, campo desconocido. |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 201 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| webhook | Webhook | Sí | |
| secret | string | Sí | Secreto de firma — se muestra SOLO aquí. Guárdalo: no se almacena y no se puede recuperar; para obtener otro hay que eliminar el webhook y registrarlo de nuevo. |
Ejemplo · 201
{
"success": true,
"data": {
"webhook": {
"id": "wh_0f3a9c1d2e4b5a6978c0d1e2",
"url": "https://hooks.mitienda.com/enviadores",
"events": [
"shipment.created",
"shipment.delivered",
"shipment.exception"
],
"mode": "live",
"status": "active",
"failure_count": 0,
"last_delivery_at": null,
"last_delivery_status": null,
"created_at": "2026-09-04T16:00:00Z",
"disabled_at": null,
"disabled_reason": null
},
"secret": "whsec_9f2c…(64 hex)"
},
"requestId": "req_a7b8c9d0e1"
}Consultar un webhook
Un webhook propio (misma cuenta y mismo modo que la llave). Requiere scope webhooks:read.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | Id del webhook (wh_ + 24 hex), devuelto por POST /webhooks. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | El webhook. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — no existe, es de otra cuenta o de otro modo (respuesta idéntica). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| webhook | Webhook | Sí |
Eliminar un webhook
Elimina el webhook. Las entregas se detienen de inmediato y los eventos pendientes en cola para él se descartan. Volver a registrar la misma URL genera un id y un secret NUEVOS. Requiere scope webhooks:write.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | Id del webhook (wh_ + 24 hex), devuelto por POST /webhooks. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Eliminado. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — no existe, es de otra cuenta o de otro modo (respuesta idéntica). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| deleted | true | Sí | |
| id | string | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"deleted": true,
"id": "wh_0f3a9c1d2e4b5a6978c0d1e2"
},
"requestId": "req_a7b8c9d0e1"
}Probar un webhook (ping)
Entrega un {"event":"ping"} firmado de forma síncrona (mismos headers y misma firma que un evento real; data = {webhook_id}) y reporta el resultado. Un ping fallido NO cuenta para la desactivación automática — repítelo con libertad mientras ajustas el endpoint. Sin cuerpo. Requiere scope webhooks:write.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| id | path | Sí | Id del webhook (wh_ + 24 hex), devuelto por POST /webhooks. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Resultado del ping (200 aunque el endpoint haya fallado — mira delivered). |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — no existe, es de otra cuenta o de otro modo (respuesta idéntica). |
| 409 | WEBHOOK_DISABLED — el webhook fue desactivado automáticamente (details.disabled_reason); elimínalo y regístralo de nuevo. |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| delivered | boolean | Sí | true si el endpoint respondió 2xx dentro de 10 s. |
| response_status | integer | null | Sí | Status HTTP recibido; null si no hubo respuesta (timeout, TLS, DNS). |
| error | "timeout" | "connection failed" | "non-2xx status" | "endpoint rejected" | null | Sí | Clase del fallo, deliberadamente gruesa (el detalle de red no se expone para que el ping no sirva como sonda de puertos): timeout, connection failed (rechazo/reset/TLS/DNS), non-2xx status (ver response_status), endpoint rejected (la URL ya no pasa la política); null si se entregó. |
| event_id | string | Sí | evt_ + 24 hex — el id del cuerpo enviado y el header X-Enviadores-Delivery. |
Ejemplo · 200
{
"success": true,
"data": {
"delivered": false,
"response_status": 500,
"error": "HTTP 500",
"event_id": "evt_4a5b6c7d8e9f0a1b2c3d4e5f"
},
"requestId": "req_a7b8c9d0e1"
}account
La cuenta y la llave: identidad, capacidades y movimientos de saldo
Identificar la llave y la cuenta (whoami)
Quién eres desde el punto de vista de la API: la cuenta, si la conexión es de prueba (ek_test_) o de producción (ek_live_), los scopes que la llave realmente porta en ESTA petición, el saldo disponible y las capacidades de la superficie.
Es el endpoint de autodiagnóstico: cuando algo responde 403 INSUFFICIENT_SCOPE, compara key.scopes con el scope que nombró el error; cuando dudes si una guía es real, mira account.mode.
Cualquier llave válida puede llamarlo, sin importar sus scopes — un whoami que pudiera responder 403 por falta de permiso no serviría para su único propósito. Aun así no expone nada nuevo: balance.available es el mismo número de GET /balance (misma lectura), y el resto son hechos sobre tu propia llave. Nunca devuelve el secreto ni su hash ni su prefijo, ni la allowlist de PDVs, ni correo/teléfono/rol.
key.scopes es el conjunto EFECTIVO: una llave creada con "los permisos por omisión" (scopes_mode: default) se re-resuelve contra los scopes por omisión vigentes en cada petición, así que aquí ves lo que el gate acaba de aplicar. Una llave que alguien restringió a mano (scopes_mode: explicit) queda congelada como se creó.
account.tier_label es el escalón de verificación de las cuentas de registro propio (T1…T4); es null para cuentas creadas por nuestro equipo, que no están en esa escalera, y también si la consulta de verificación no está disponible.
capabilities describe la superficie, no la cuenta: pickups sigue el interruptor de recolecciones (siempre true en sandbox), y international/multi_package se derivan de las mismas validaciones que aplica POST /shipments, así que no pueden desincronizarse de la realidad.
En modo de prueba, balance.available es el saldo COMPUTADO del sandbox.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Identidad de la llave y la cuenta. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| account | object | Sí | |
| key | object | Sí | |
| balance | object | Sí | |
| capabilities | object | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"account": {
"id": "U0000001",
"name": "Comercializadora Ejemplo SA de CV",
"mode": "live",
"account_type": "customer",
"tier_label": "T3"
},
"key": {
"id": "AK0123456789abcdef",
"label": "Servidor de producción",
"mode": "live",
"scopes": [
"rates:read",
"shipments:write",
"shipments:read",
"tracking:read",
"cancellations:write",
"cancellations:read",
"balance:read",
"labels:read",
"addresses:read",
"addresses:write",
"pickups:write",
"pickups:read"
],
"scopes_mode": "default",
"spend_cap_daily_mxn": 5000,
"rate_limit_tier": "standard"
},
"balance": {
"available": 4675,
"currency": "MXN"
},
"capabilities": {
"pickups": true,
"international": false,
"multi_package": false,
"webhooks": false
}
},
"requestId": "req_d4e5f6a7b8"
}Listar los movimientos de saldo
El libro mayor detrás de GET /balance: cargos por guías, reembolsos por cancelación, recargas y ajustes manuales, del más reciente al más antiguo. Es la respuesta a "¿en qué se fue el saldo?".
Mismo principal que `GET /balance`, por construcción. Sin header: el saldo PERSONAL del usuario de la llave. Con X-PDV-ID validado (llaves admin, misma allowlist): el libro mayor de ese fondo de punto de venta. Las consultas cruzadas por user_id del endpoint interno NO existen aquí: una llave lee exactamente el fondo que cargarían sus envíos.
amount viene con SIGNO en MXN (negativo = salió dinero) y balance_after es el saldo tras ese movimiento. type usa un vocabulario público de cuatro palabras (charge, refund, topup, adjustment) que resume el enum interno; los detalles internos (qué reventa, qué pierna de una corrección, qué programa) no se exponen.
En modo de prueba el listado se DERIVA de los envíos sandbox — un cargo al crear, un reembolso al cancelar — de modo que la suma cuadra exactamente con el saldo computado del sandbox. Requiere scope balance:read.
Parámetros
| Nombre | En | Req. | Descripción |
|---|---|---|---|
| page | query | — | |
| limit | query | — | |
| type | query | — | Filtra por tipo público de movimiento — exactamente el mismo valor que devuelve el campo type de cada fila. charge = cargo (salió saldo), refund = devolución, topup = recarga en línea, adjustment = cualquier otro abono (alta manual, promoción, liquidación). Un valor fuera del enum es 422 VALIDATION_ERROR, nunca una lista vacía silenciosa. |
| from | query | — | Día calendario INCLUSIVO (zona horaria de negocio America/Mexico_City) desde el cual listar movimientos. Solo YYYY-MM-DD. |
| to | query | — | Día calendario INCLUSIVO (America/Mexico_City) hasta el cual listar — el día completo. from posterior a to es 422. |
| X-PDV-ID | header | — | Solo llaves de cuentas admin: el punto de venta cuyo fondo se carga/consulta. Requiere que la llave tenga una allowlist de PDVs configurada por un administrador (pdv_allowlist); sin allowlist, todo PDV real es denegado por defecto. Validado además contra los PDV activos. Cualquier violación → 403 PDV_NOT_ALLOWED. El valor personal_account equivale a omitir el header. |
Respuestas
| Código | Descripción |
|---|---|
| 200 | Página de movimientos. |
| 401 | UNAUTHORIZED — credencial Bearer ausente/mal formada o llave desconocida/revocada (respuesta idéntica en ambos casos). |
| 403 | INSUFFICIENT_SCOPE — a la llave le falta el scope requerido · PDV_NOT_ALLOWED — violación del carril X-PDV-ID. Nota de orden: el limitador por llave corre ANTES del gate de scope, así que una petición con scope incorrecto y el bucket agotado responde 429 RATE_LIMITED (el 429 tiene precedencia sobre este 403). |
| 404 | NOT_FOUND — ruta desconocida, recurso ajeno/inexistente, o la API pública no está habilitada (respuestas indistinguibles por diseño). |
| 422 | VALIDATION_ERROR — type fuera del enum, fecha mal formada o from posterior a to (campos en details.fields). |
| 429 | RATE_LIMITED — bucket de la llave agotado. Espera Retry-After s (details.retry_after_ms para el hint fino). El limitador corre antes del gate de scope, por lo que un 429 tiene precedencia sobre un 403 INSUFFICIENT_SCOPE. |
| 500 | SERVER_ERROR — falla interna (p. ej. lectura de base de datos). Es transitoria; reintenta con backoff. |
Respuesta 200 · data
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| transactions | Transaction[] | Sí | |
| pagination | Pagination | Sí |
Ejemplo · 200
{
"success": true,
"data": {
"transactions": [
{
"id": "TX00012345",
"type": "charge",
"amount": -184.5,
"balance_after": 4675,
"shipment_id": "20260830-000123",
"description": "Guía Estafeta Terrestre",
"created_at": "2026-08-30T10:00:02Z"
},
{
"id": "TX00012344",
"type": "topup",
"amount": 2000,
"balance_after": 4859.5,
"shipment_id": null,
"description": "Recarga en línea",
"created_at": "2026-08-29T18:12:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 2
}
},
"requestId": "req_b1c2d3e4f5"
}Modelos
Esquemas referenciados por las peticiones y respuestas.
SuccessEnvelope
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| success | true | Sí | |
| data | object | Sí | |
| requestId | string | Sí | Correlación de logs; también en el header X-Request-Id. |
ErrorEnvelope
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| success | false | Sí | |
| error | object | Sí | |
| requestId | string | Sí |
RatesRequest
Origen: exactamente UNO de `from` (inline) o `from_id` (remitente del directorio). Destino: exactamente UNO de `to` o `to_id`. Mandar ambos miembros de un par → 422; un id no visible para la llave → 404 (respuesta ciega). Cotizar por id usa los datos completos guardados (ciudad/estado/colonia), así que la resolución de zona es igual o mejor que con CP suelto. Bultos: exactamente UNO de `package` (una caja) o `packages` (2–10 cajas en un mismo envío).
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| from | RatesEndpointParty | — | |
| from_id | string | — | Id de un remitente del directorio (GET /senders). Mutuamente excluyente con from. |
| to | RatesEndpointParty | — | |
| to_id | string | — | Id de un destinatario del directorio (GET /recipients). Mutuamente excluyente con to. |
| package | object | — | |
| packages | PackagesList | — | |
| insurance | InsuranceRequest | — |
PackagesList
VARIOS bultos que viajan como UN solo envío (un cargo, una cotización). Mutuamente excluyente con `package`. La lista cotizada y la reservada deben ser IDÉNTICAS (mismo orden, mismas medidas) o `POST /shipments` responde 409 `RATE_PACKAGE_MISMATCH`. Solo se cotizan/reservan servicios que soportan multi-bulto.
InsuranceRequest
Cobertura para el envío. `insured_value` contrata la póliza de la PLATAFORMA (prima incluida en `pricing.total_price`) o, cuando esa póliza no está disponible, la cobertura de la propia paquetería — el precio mostrado ya la incluye; cada tarifa indica `insurance_included` / `insurance_not_supported`. NO es el valor declarado a la paquetería (`declared_value` en `POST /shipments`). *Insurance: `insured_value` buys coverage for the shipment; when the platform policy is unavailable the carrier's own coverage is used and the price shown already includes it.*
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| insured_value | number | — | Valor a asegurar (MXN). La prima queda incluida en pricing.total_price de cada tarifa. |
| declared_value | number | — | OBSOLETO — alias de insured_value (compatibilidad con integradores previos). Si mandas ambos deben coincidir. |
RatesGroupedResponse
Respuesta de `POST /rates?view=grouped`: cada servicio distinto (paquetería + nivel de servicio; para `priority` también la ventana horaria) aparece UNA vez con su rango de precios y sus opciones reservables. Mismo `unlock_notice`/`fetched_at`/`stale_after`/`meta`/`rate_id_expires_in_seconds` que la forma plana.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| services | RateGroup[] | Sí | Ordenados por price_from ascendente. |
| view | "grouped" | Sí | |
| unlock_notice | string | null | — | |
| fetched_at | string | null | Sí | |
| stale_after | string | null | Sí | |
| meta | object | Sí | |
| rate_id_expires_in_seconds | 1800 | Sí |
RateGroup
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| service_key | string | Sí | Clave estable del servicio: carrier|tier (+ |ventana para priority). |
| carrier | string | Sí | Nombre canónico de la paquetería. |
| tier | "priority" | "express" | "standard_economy" | "international" | Sí | |
| delivery_window | string | null | Sí | |
| service_label | string | Sí | Etiqueta legible, p. ej. DHL · Express, Estafeta · Prioritario 10:30 AM. |
| estimated_days | string | number | null | Sí | De la opción más barata. |
| pickup_available | boolean | Sí | true si ALGUNA opción incluye recolección. |
| booking_locked | boolean | Sí | true solo si TODAS las opciones están bloqueadas para esta cuenta. |
| price_from | number | null | Sí | |
| price_to | number | null | Sí | |
| currency | "MXN" | Sí | |
| options | object[] | Sí | Precios reservables de este servicio, de menor a mayor. Reserva options[0].rate_id salvo que el usuario pida otra. |
RatesEndpointParty
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| postal_code | string | Sí | CP de 5 dígitos (MX). |
| country | "MX" | — | Opcional. Solo MX (sin distinción de mayúsculas/minúsculas; vacío = MX) — cualquier otro valor responde 422 (v1 es doméstico MX). |
| state | string | — | |
| city | string | — | |
| neighborhood | string | — |
RatesResponse
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| services | Rate[] | Sí | |
| unlock_notice | string | null | — | Mensaje es-MX para mostrar al usuario cuando su catálogo está limitado (hay tarifas con booking_locked) o cuando se desbloqueó por recargas acumuladas y la verificación de identidad sigue pendiente. null para cuentas sin restricción. |
| fetched_at | string | null | Sí | ISO-8601. |
| stale_after | string | null | Sí | ISO-8601 (fetched_at + 5 min): re-cotiza para PRECIOS frescos; el rate_id sigue siendo enviable hasta los 30 min. |
| meta | object | Sí | |
| rate_id_expires_in_seconds | 1800 | Sí | TTL del rate_id: crea el envío dentro de esta ventana o recibirás 409 RATE_EXPIRED. |
Rate
Una tarifa enviable. `id` es el `rate_id` para `POST /shipments`. La forma es IDÉNTICA para toda llave (los costos internos de proveedor nunca se incluyen) y es una whitelist estricta: no aparecerán campos no documentados.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | string | Sí | rate_id — pásalo tal cual a POST /shipments. OPACO: su formato no es parte del contrato (difiere entre el carril de prueba y el live); no lo interpretes ni lo valides. |
| carrier | string | null | Sí | |
| service_name | string | null | Sí | Nombre comercial del servicio tal como lo publica la paquetería. |
| service_type | string | null | — | Clasificación por niveles: priority | express | standard_economy… |
| tier | string | null | — | Igual a service_type (alias de compatibilidad). |
| delivery_window | string | null | — | Ventana de entrega normalizada (p. ej. next_day, ground). |
| pickup_included | boolean | — | La tarifa incluye recolección. |
| address_delivery | boolean | — | Entrega a domicilio (false = entrega en sucursal/ocurre). |
| pricing | object | Sí | |
| delivery | object | — | |
| insurance_included | boolean | — | |
| insurance_not_supported | boolean | — | La paquetería no acepta seguro para esta tarifa. |
| zona_extendida | boolean | — | Destino en zona extendida: puede implicar cargos de reexpedición. Cuando la paquetería los cotiza por adelantado ya vienen dentro de total_price; si no, pueden facturarse después como cargo de paquetería. |
| booking_locked | boolean | — | La tarifa es VISIBLE pero aún no reservable para esta cuenta: POST /shipments responde 403 SERVICE_UNLOCK_REQUIRED. Solo afecta a cuentas de registro propio sin identidad verificada (INE) y con menos de $2,500 MXN en recargas acumuladas; los servicios sin recolección (entrega en sucursal, cargos extra en mostrador) nunca se bloquean. Las rutas de desbloqueo vienen en unlock_notice. |
ShipmentCreateRequest
Remitente: exactamente UNO de `from` (inline) o `from_id` (directorio). Destinatario: exactamente UNO de `to` o `to_id`. Ambos miembros de un par → 422; id no visible → 404 ciego. `to_id` requiere `from_id` y el destinatario debe pertenecer a ese remitente (un destinatario de otro remitente responde 404). Un lado referenciado usa la fila guardada tal cual (sin create-or-reuse). Bultos: exactamente UNO de `package` o `packages` — los MISMOS que se cotizaron.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| rate_id | string | Sí | El id de una tarifa de POST /rates cotizada con esta misma llave (y mismo X-PDV-ID, si se usó), con menos de 30 minutos. Trátalo como OPACO: cópialo tal cual; su formato no es parte del contrato (difiere entre el carril de prueba y el live) y no debe interpretarse ni validarse. |
| from | ShipmentParty | — | |
| from_id | string | — | Id de un remitente del directorio. Mutuamente excluyente con from. |
| to | ShipmentParty | — | |
| to_id | string | — | Id de un destinatario del directorio (debe pertenecer al from_id enviado). Mutuamente excluyente con to; requiere from_id. |
| package | object | — | |
| packages | PackagesList | — | |
| insurance | object | — | La cobertura que YA se cotizó (insured_value buys coverage; when the platform policy is unavailable the carrier's own coverage is used and the price shown already includes it). Omitir insurance acepta la cobertura cotizada; mandar un insured_value DISTINTO → 422 INSURANCE_MISMATCH (el total firmado nunca se recalcula al crear). Una tarifa cuya paquetería no ofrece cobertura (insurance_not_supported: true) → 422 INSURANCE_NOT_AVAILABLE. |
| declared_value | number | — | Valor de la mercancía DECLARADO A LA PAQUETERÍA (MXN). NO es seguro, pero la paquetería puede cobrarlo: debe ser EXACTAMENTE el valor con el que se cotizó (el asegurado) — un valor que la cotización no llevaba responde 422 DECLARED_VALUE_MISMATCH, salvo en servicios que lo tratan como informativo (la recuperación de DECLARED_VALUE_REQUIRED). Si se omite se declara el valor cotizado; si la cotización no llevaba ninguno, no se inventa. *declared_value must equal the value the rate was quoted with; a carrier may price it.* |
| contenido | string | — | Descripción del contenido (aparece en la guía cuando la paquetería lo soporta). |
| reference | string | — | RESERVADO — aceptado pero no persistido en v1. |
| metadata | object | — | RESERVADO en el carril live (aceptado, no persistido). En el carril SANDBOX, metadata.test_scenario fuerza una falla para rehearsal (ver x-test-mode). |
ShipmentParty
Los datos se registran en el directorio del usuario de la llave con dedupe exacto (normalizado por espacios/mayúsculas), así que repetir el mismo remitente/destinatario no crea filas nuevas. Los campos exceden-longitud se recortan al límite de columna.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| name | string | Sí | |
| apellido_paterno | string | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| apellido_materno | string | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| company | string | — | Razón social (solo remitente; ignorado en destinatario). |
| phone | string | Sí | |
| string | — | ||
| street | string | Sí | |
| number | string | — | Número exterior. |
| colonia | string | Sí | |
| city | string | Sí | |
| state | string | Sí | |
| postal_code | string | Sí | |
| country | "MX" | — | Opcional. Solo MX (sin distinción de mayúsculas/minúsculas; vacío = MX) — cualquier otro valor responde 422 (v1 es doméstico MX). |
| rfc | string | — | Solo remitente; usado por algunas paqueterías. |
ShipmentCreateResponse
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| shipment | object | Sí | |
| duplicate | boolean | — | Presente (true) cuando la paquetería devolvió una guía ya registrada y la plataforma deduplicó sin doble cargo. |
| recovered | boolean | — | Presente (true) cuando la etiqueta se recuperó tras un error transitorio del proveedor. |
Shipment
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | string | Sí | |
| guia | string | null | Sí | |
| carrier | string | null | Sí | Nombre canónico de la paquetería física (SSOT de la plataforma). |
| service | string | null | Sí | |
| status | "created" | "collected" | "in_transit" | "out_for_delivery" | "delivered" | "exception" | "returned" | "cancelled" | "unknown" | Sí | |
| status_label | string | Sí | Etiqueta en español del estatus canónico. |
| total | number | null | Sí | |
| currency | "MXN" | Sí | |
| created_at | string | null | Sí | ISO-8601 UTC (Z). |
| tracking_pending | boolean | Sí | |
| label_url | string | Sí | |
| packages | object[] | Sí | Lo que se registró al crear el envío, en el mismo vocabulario que usa POST /shipments (weight_kg, length_cm, …). Para envíos multi-paquete es la lista completa por bulto, no el agregado. Lista vacía si el envío no tiene medidas registradas.
Envíos anteriores al 2026-07-28: las dimensiones no se persistían (solo se usaban para calcular el peso volumétrico y se descartaban), así que length_cm/width_cm/height_cm llegan en null en todo envío creado antes de esa fecha. weight_kg sí está disponible en todo el histórico. Un null significa "no registrado", nunca "cero": las dimensiones no declaradas tampoco se rellenan con un valor por defecto. |
| billable_weight_kg | number | null | Sí | Peso facturable con el que se cotizó — max(peso real, peso volumétrico). Es el número contra el que hay que comparar cuando la paquetería aplica un sobrepeso; el peso real por sí solo no explica el cargo. El repeso de la paquetería NO vive en esta API (está en su reporte de facturación). |
| declared_value | number | null | Sí | Valor declarado (MXN). |
| content | string | null | Sí | Descripción del contenido capturada al crear el envío. |
| sender | object | Sí | Remitente, con la misma forma mínima que recipient: solo nombre + ciudad/estado/CP — nunca la dirección completa ni teléfonos. |
| recipient | object | Sí | Solo nombre + ciudad/estado/CP — nunca la dirección completa ni teléfonos. |
Pagination
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| page | integer | Sí | |
| limit | integer | Sí | |
| total | integer | Sí |
Tracking
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| guia | string | Sí | |
| carrier | string | null | Sí | |
| status | string | Sí | |
| status_label | string | Sí | |
| origin | object | Sí | |
| destination | object | Sí | |
| created_at | string | null | Sí | |
| events | object[] | Sí |
CancellationCreated
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | integer | Sí | |
| display_id | string | Sí | Live: formato CR%08d. Sandbox: SBX-CR-<id>. |
| status | "pending" | "cancelled" | Sí | Live devuelve pending (la solicitud entra a la cola de staff). Sandbox devuelve cancelled (cancelación síncrona y terminal — ver x-test-mode). |
| refund_status | "not_applicable" | "refunded" | Sí | Live: not_applicable (reembolso lo resuelve el staff). Sandbox: refunded (reembolso inmediato). |
| refund_amount_expected | number | null | — | |
| cancellation_deadline | string | null | — | ISO-8601 UTC (Z). Siempre null en sandbox. |
Pickup
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | integer | Sí | Id numérico de la recolección. |
| display_id | string | Sí | Live: formato PR%08d. Sandbox: SBX-PU-<id>. |
| shipment_id | string | null | Sí | |
| status | "pending" | "scheduled" | "awaiting_confirmation" | "failed" | "cancelled" | Sí | Una solicitud SIEMPRE nace en awaiting_confirmation (se está gestionando; NO la presentes como confirmada). Pasa a scheduled cuando el equipo la confirma con la paquetería — ahí llegan confirmed_window_* y confirmation_number, y se envía el correo al solicitante. failed = no se pudo agendar (entregar en sucursal). cancelled = la canceló quien la pidió. pending no se usa. El sandbox devuelve awaiting_confirmation, igual que producción. |
| carrier | string | null | — | Paquetería física que hará la recolección. |
| requested_window_start | string | null | — | ISO-8601 UTC (Z). Inicio de la ventana solicitada. |
| requested_window_end | string | null | — | ISO-8601 UTC (Z). Fin de la ventana solicitada. |
| confirmed_window_start | string | null | — | ISO-8601 UTC (Z). Ventana confirmada por la paquetería, cuando la devuelve. |
| confirmed_window_end | string | null | — | ISO-8601 UTC (Z). |
| confirmation_number | string | null | — | Folio/referencia de recolección de la paquetería. |
| created_at | string | null | — | |
| updated_at | string | null | — |
Cancellation
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | integer | Sí | |
| display_id | string | Sí | |
| shipment_id | string | null | Sí | |
| status | "pending" | "approved" | "in_progress" | "cancelled" | "rejected_by_carrier" | "not_cancellable" | "reported_to_carrier" | "rejected" | "failed" | "expired" | Sí | |
| refund_status | "not_applicable" | "refund_in_progress" | "refunded" | "refund_denied" | Sí | |
| reason_code | string | null | Sí | |
| reason_text | string | null | Sí | |
| refund_amount_expected | number | null | Sí | |
| refund_amount_actual | number | null | Sí | |
| cancellation_deadline | string | null | Sí | |
| created_at | string | null | Sí | |
| updated_at | string | null | Sí | |
| resolved_at | string | null | Sí |
Balance
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| balance | number | Sí | available + held (total del principal en el libro mayor). |
| held | number | Sí | Autorizaciones pendientes (holds). |
| available | number | Sí | Lo gastable por un envío nuevo. |
| currency | "MXN" | Sí | |
| scope | object | Sí |
SenderCreateRequest
El mismo vocabulario de campos que el `from` de `POST /shipments`, con `city`/`state` opcionales aquí (se autocompletan del catálogo SEPOMEX según el CP — la misma validación de exactitud que `POST /contacts/import`). `name` es el nombre completo; `apellido_paterno`/`apellido_materno` están deprecados (se aceptan y se integran a `name`). Campos desconocidos → 422. Los campos exceden-longitud se recortan al límite de columna.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| name | string | Sí | Nombre de pila (nombre). |
| apellido_paterno | string | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| apellido_materno | string | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| company | string | — | Razón social. |
| rfc | string | — | |
| phone | string | Sí | |
| string | — | ||
| street | string | Sí | |
| number | string | — | Número exterior. |
| colonia | string | Sí | |
| city | string | — | Opcional: si se omite se autocompleta del catálogo SEPOMEX según el CP; si viene, debe coincidir con el CP (si no → 422 nombrando la ciudad esperada). |
| state | string | — | Opcional: si se omite se autocompleta del catálogo SEPOMEX según el CP; si viene, debe coincidir con el CP (si no → 422 nombrando el estado esperado). |
| postal_code | string | Sí | 5 dígitos; debe EXISTIR en el catálogo postal (SEPOMEX). |
| country | "MX" | — | Opcional. Solo MX — cualquier otro valor responde 422. |
SenderUpdateRequest
Actualización PARCIAL: solo los campos presentes cambian. Se debe enviar al menos uno. Un campo requerido (`name`/`phone`/`street`/`colonia`/`city`/`state`/`postal_code`) puede cambiarse pero NO vaciarse. Campos opcionales aceptan `null` para limpiarse. Campos desconocidos → 422. Si la edición dejaría la dirección idéntica a OTRA visible → 409 `DUPLICATE_ADDRESS`.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| name | string | — | |
| apellido_paterno | string | null | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| apellido_materno | string | null | — | DEPRECATED (2026-09-21): el nombre completo va en name. Aún se acepta y se integra a name al guardar. |
| company | string | null | — | |
| rfc | string | null | — | |
| phone | string | — | |
| string | null | — | ||
| street | string | — | |
| number | string | null | — | |
| colonia | string | — | |
| city | string | — | |
| state | string | — | |
| postal_code | string | — | |
| country | "MX" | — |
RecipientCreateRequest
El mismo vocabulario de campos que el `to` de `POST /shipments` + `sender_id`, con `city`/`state` opcionales aquí (se autocompletan del catálogo SEPOMEX según el CP — la misma validación de exactitud que `POST /contacts/import`). `alias`/`referencia`/`delivery_instructions` solo se aplican al CREAR (una reutilización devuelve la fila existente intacta).
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| sender_id | string | Sí | Remitente dueño (visible para la llave; si no → 404 ciego). |
| alias | string | — | Nombre descriptivo ("Casa", "Oficina"). |
| name | string | Sí | |
| phone | string | Sí | |
| string | — | ||
| street | string | Sí | |
| number | string | — | |
| colonia | string | Sí | |
| city | string | — | Opcional: si se omite se autocompleta del catálogo SEPOMEX según el CP; si viene, debe coincidir con el CP (si no → 422 nombrando la ciudad esperada). |
| state | string | — | Opcional: si se omite se autocompleta del catálogo SEPOMEX según el CP; si viene, debe coincidir con el CP (si no → 422 nombrando el estado esperado). |
| postal_code | string | Sí | 5 dígitos; debe EXISTIR en el catálogo postal (SEPOMEX). |
| country | "MX" | — | |
| referencia | string | — | Referencias del domicilio. |
| delivery_instructions | string | — |
RecipientUpdateRequest
Actualización PARCIAL. `sender_id` NO puede cambiarse (los destinatarios no se re-asignan de remitente) → 422 si se envía. Misma disciplina que `SenderUpdateRequest`: al menos un campo, no vaciar requeridos, `null` limpia opcionales, unificar con OTRO destinatario del MISMO remitente → 409 `DUPLICATE_ADDRESS`.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| alias | string | null | — | |
| name | string | — | |
| phone | string | — | |
| string | null | — | ||
| street | string | — | |
| number | string | null | — | |
| colonia | string | — | |
| city | string | — | |
| state | string | — | |
| postal_code | string | — | |
| country | "MX" | — | |
| referencia | string | null | — | |
| delivery_instructions | string | null | — |
Sender
Recurso remitente. Su `id` sirve como `from_id` en `POST /rates` y `POST /shipments`.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | string | Sí | |
| name | string | null | Sí | |
| apellido_paterno | null | Sí | Siempre null: el nombre completo está en name. |
| apellido_materno | null | Sí | Siempre null: el nombre completo está en name. |
| company | string | null | Sí | |
| rfc | string | null | Sí | |
| phone | string | null | Sí | |
| string | null | Sí | ||
| street | string | null | Sí | |
| number | string | null | Sí | |
| colonia | string | null | Sí | |
| city | string | null | Sí | |
| state | string | null | Sí | |
| postal_code | string | null | Sí | |
| created_at | string | null | Sí | ISO-8601 UTC (Z). |
Recipient
Recurso destinatario. Su `id` sirve como `to_id` en `POST /rates` y `POST /shipments` (requiere `from_id` del remitente dueño).
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | string | Sí | |
| sender_id | string | Sí | |
| alias | string | null | Sí | |
| name | string | null | Sí | |
| phone | string | null | Sí | |
| string | null | Sí | ||
| street | string | null | Sí | |
| number | string | null | Sí | |
| colonia | string | null | Sí | |
| city | string | null | Sí | |
| state | string | null | Sí | |
| postal_code | string | null | Sí | |
| referencia | string | null | Sí | |
| delivery_instructions | string | null | Sí | |
| created_at | string | null | Sí | ISO-8601 UTC (Z). |
Colonia
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| name | string | Sí | Nombre del asentamiento (d_asenta de SEPOMEX). Úsalo como colonia/neighborhood. |
| settlement_type | string | null | Sí | Tipo de asentamiento SEPOMEX: Colonia, Fraccionamiento, Pueblo, Barrio, etc. |
PostalCode
Resultado de `GET /postal-codes/{postal_code}`. Datos de referencia SEPOMEX (públicos, solo lectura).
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| postal_code | string | Sí | El CP consultado (5 dígitos). |
| state | string | null | Sí | Estado (d_estado). Úsalo como state. |
| state_code | string | null | Sí | Código de estado normalizado (mismo esquema que state_code del directorio; p. ej. DF, JAL, NL). |
| municipality | string | null | Sí | Municipio/alcaldía (d_mnpio). Suele ser el mejor valor para city. |
| city | string | null | Sí | Ciudad (d_ciudad) cuando SEPOMEX la distingue del municipio; null en la mayoría de los CP. |
| colonias | Colonia[] | Sí | Asentamientos únicos del CP, ordenados por nombre. Un CP mapea a un estado/municipio pero a muchas colonias. |
LabelLink
Respuesta de `GET /labels/{envio_id}?format=json`: enlace a la etiqueta en vez de los bytes.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| shipment_id | string | Sí | |
| guia | string | — | Solo en sandbox (la vía en vivo omite el campo). |
| label_url | string | null | Sí | En vivo: enlace firmado a la copia de Enviadores (https://api.enviadores.com.mx/api/v1/label-files/{envio_id}/{exp}/{firma}.pdf). Se abre sin llave, vence en expires_at (30 días; pide uno nuevo con esta misma llamada) y nunca es la URL de la paquetería. En sandbox: enlace a la etiqueta de prueba. null cuando la etiqueta aún no está disponible; note lo explica. |
| expires_at | string | — | Solo en vivo: cuándo deja de funcionar label_url (UTC). |
| format | "pdf" | Sí | |
| sandbox | boolean | Sí | true con llaves ek_test_: etiqueta de prueba sin validez. |
| note | string | — | Presente solo cuando hay algo que aclarar (etiqueta sandbox, o sin URL directa). |
WebhookEvent
Eventos suscribibles. `shipment.created` se emite al comprar la guía; `shipment.collected`…`shipment.exception` cuando el rastreo observa una transición REAL del estatus canónico — un estatus no terminal puede repetirse legítimamente (`exception` → `in_transit` → `exception` produce dos `shipment.exception`), pero la misma transición observada dos veces se entrega una sola vez; `shipment.delivered`, `shipment.returned` y `shipment.cancelled` son terminales y se entregan a lo sumo una vez por envío (`cancelled` también cuando la cancelación queda en firme); `pickup.resolved` cuando nuestro equipo cierra una solicitud de recolección (`scheduled` o `failed`). El evento `ping` solo existe para `POST /webhooks/{id}/test` y no es suscribible.
WebhookCreateRequest
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| url | string | Sí | Endpoint https:// público en el puerto 443 u 8443 (cualquier otro puerto se rechaza). Se rechazan http, credenciales embebidas, fragmentos, localhost/.local/.internal, literales IP privadas, hosts que resuelvan a rangos privados y hosts que apunten al propio API de Enviadores. |
| events | WebhookEvent[] | Sí | Eventos a recibir (duplicados se descartan). |
Webhook
Recurso webhook. NUNCA incluye el secreto: solo existe en la respuesta de `POST /webhooks`.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | string | Sí | wh_ + 24 hex. |
| url | string | Sí | |
| events | WebhookEvent[] | Sí | |
| mode | "live" | "test" | Sí | Lane de la llave que lo registró. live recibe eventos reales; test solo shipment.created/shipment.cancelled del sandbox y el ping. |
| status | "active" | "disabled" | Sí | disabled = desactivado automáticamente: tras 20 fallos consecutivos, o porque la llave API que lo registró fue revocada (disabled_reason: key_revoked — un webhook muere con su llave). Al desactivarse, sus eventos pendientes se descartan. No cuenta para el tope; elimínalo y regístralo de nuevo. |
| failure_count | integer | Sí | Fallos CONSECUTIVOS de entrega (cualquier respuesta no 2xx o error de transporte). Vuelve a 0 con cada 2xx. Los pings de prueba no lo afectan. |
| last_delivery_at | string | null | — | ISO-8601 UTC (Z). Último intento (éxito o fallo), pings incluidos. |
| last_delivery_status | integer | null | — | Status HTTP del último intento; 0 = fallo de transporte (timeout/TLS/DNS). |
| created_at | string | null | — | ISO-8601 UTC (Z). |
| disabled_at | string | null | — | ISO-8601 UTC (Z). |
| disabled_reason | string | null | — | key_revoked cuando la llave que lo registró fue revocada; texto explicativo cuando fue por fallos consecutivos. |
WebhookCreated
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| webhook | Webhook | Sí | |
| secret | string | Sí | Secreto de firma — se muestra SOLO aquí. Guárdalo: no se almacena y no se puede recuperar; para obtener otro hay que eliminar el webhook y registrarlo de nuevo. |
WebhookTestResult
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| delivered | boolean | Sí | true si el endpoint respondió 2xx dentro de 10 s. |
| response_status | integer | null | Sí | Status HTTP recibido; null si no hubo respuesta (timeout, TLS, DNS). |
| error | "timeout" | "connection failed" | "non-2xx status" | "endpoint rejected" | null | Sí | Clase del fallo, deliberadamente gruesa (el detalle de red no se expone para que el ping no sirva como sonda de puertos): timeout, connection failed (rechazo/reset/TLS/DNS), non-2xx status (ver response_status), endpoint rejected (la URL ya no pasa la política); null si se entregó. |
| event_id | string | Sí | evt_ + 24 hex — el id del cuerpo enviado y el header X-Enviadores-Delivery. |
WebhookPayload
Cuerpo de cada `POST` a tu endpoint. Headers: `Content-Type: application/json`, `User-Agent: enviadores-webhooks/1.0`, `X-Enviadores-Signature: t=<unix>,v1=<hex>` (`v1 = HMAC-SHA256(secret, "<t>.<cuerpo crudo>")`), `X-Enviadores-Event`, `X-Enviadores-Delivery` (= `id`). No se siguen redirecciones; responde 2xx en <10 s.
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | string | Sí | evt_ + 24 hex. Estable entre reintentos de la misma entrega — deduplica con él. |
| event | string | Sí | Nombre del evento (WebhookEvent, o ping). |
| mode | "live" | "test" | Sí | |
| created_at | string | Sí | ISO-8601 UTC (Z). Momento en que se emitió el evento. |
| data | object | Sí | {shipment: Shipment} para shipment.* (misma forma que GET /shipments/{id}), {pickup: Pickup} para pickup.resolved (misma forma que GET /pickups/{id}), {webhook_id} para ping. |
Me
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| account | object | Sí | |
| key | object | Sí | |
| balance | object | Sí | |
| capabilities | object | Sí |
Transaction
| Campo | Tipo | Req. | Descripción |
|---|---|---|---|
| id | string | Sí | Id del movimiento (estable; el que conviene citar a soporte). |
| type | "charge" | "refund" | "topup" | "adjustment" | Sí | Vocabulario público: cargo, devolución, recarga en línea, o cualquier otro abono. |
| amount | number | Sí | Importe en MXN CON SIGNO: negativo si salió saldo, positivo si entró. |
| balance_after | number | null | Sí | Saldo del principal después de este movimiento. |
| shipment_id | string | null | Sí | Envío relacionado, cuando el movimiento nació de uno. |
| description | string | null | Sí | Texto del movimiento. Las correcciones de cargo (cuando reasignamos un cobro al fondo correcto) se reportan siempre como Ajuste: el detalle interno describe nuestra propia contabilidad y nombra al operador que la hizo, y nada de eso viaja a esta superficie. |
| created_at | string | null | Sí | ISO-8601 en UTC (Z). |