Envíos
El objeto shipment, su ciclo de vida y cómo crear, consultar y cancelar envíos.
El envío (shipment) es la entidad central del API: describe un paquete que viaja de un origen a un destino. Se crea en estado DRAFT, se convierte en guía al comprar una tarifa y avanza por su ciclo de vida hasta la entrega.
El ciclo de vida
DRAFT → LABEL_PURCHASED → READY_FOR_PICKUP → PICKED_UP → IN_TRANSIT → OUT_FOR_DELIVERY → DELIVERED
| Estado | Significado |
|---|---|
DRAFT |
Recién creado; con cotizaciones, editable y cancelable sin costo |
PENDING |
Enviado, en espera de compra de guía |
LABEL_PURCHASED |
Guía comprada, en espera de recolección |
READY_FOR_PICKUP |
Paquetería notificada para recolectar |
PICKED_UP |
La paquetería recogió el paquete |
IN_TRANSIT |
En camino |
OUT_FOR_DELIVERY |
En reparto de última milla |
DELIVERED |
Entregado |
RETURNED |
Devuelto al remitente |
FAILED |
Entrega fallida |
CANCELLED |
Cancelado |
Dos maneras de comprar una guía
POST /v1/shipments sirve a dos flujos. Elige el que corresponda a cada envío:
1. En dos pasos (compara y elige). Crea el envío; la respuesta trae rates[] con las cotizaciones de las paqueterías disponibles. Comparas precio y velocidad, eliges una y compras con su rateId en POST /v1/shipments/:id/label. Ideal cuando el costo o la velocidad deciden en cada envío. Si ya sabes con quién enviar, pasa carrierCode y serviceLevel al crear para acotar las cotizaciones a una categoría.
2. En una llamada. Si ya sabes con qué paquetería y servicio enviar, o solo quieres la más barata, agrega un objeto purchase al crear el envío. La guía llega en la misma respuesta, sin segundo paso. Sirve para automatización y volumen predecible. Ver Compra en una llamada.
| En dos pasos | En una llamada | |
|---|---|---|
| Cuándo usarla | El precio o la velocidad deciden en cada envío | Ya sabes el servicio, o quieres la tarifa más barata |
| Peticiones | Crear → comprar con rateId |
Una sola: POST /v1/shipments con purchase |
| Qué regresa | rates[] para comparar |
La guía (label + purchasedRate) lista |
| Ideal para | Tiendas que optimizan por costo | Automatización y volumen predecible |
Endpoints
| Método | Ruta | Alcance | Descripción |
|---|---|---|---|
POST |
/v1/shipments |
shipments:write |
Crear un envío (devuelve rates[] inline; con purchase, compra la guía en la misma llamada) |
GET |
/v1/shipments |
shipments:read |
Listar envíos (paginado y filtrable) |
GET |
/v1/shipments/stats |
shipments:read |
Conteo de envíos por estado |
GET |
/v1/shipments/:id |
shipments:read |
Detalle con snapshots, guía y eventos |
PUT |
/v1/shipments/:id |
shipments:write |
Actualizar un envío en DRAFT |
DELETE |
/v1/shipments/:id |
shipments:write |
Cancelar un envío |
POST |
/v1/shipments/:id/return |
shipments:write |
Crear un envío de retorno (ruta invertida) |
Crea un envío
Para cada rol de dirección (from, to, return) envía exactamente una de dos variantes: el ID de una dirección guardada (fromAddressId) o un objeto inline (fromAddress). Las direcciones inline pueden guardarse en tu directorio con saveToAddressBook: true.
Parámetros del cuerpo
parcelobjeto
El paquete: length, width y height (cm) y weight (kg). Acepta dimensionUnit (cm | in), weightUnit (kg | g | lb | oz) y description.
objetofromAddressId?string
Origen guardado. Envía exactamente uno de fromAddressId o fromAddress.
stringfromAddress?objeto
Origen inline (ver el objeto dirección). Puede guardarse con saveToAddressBook: true.
objetotoAddressId?string
Destino guardado. Envía exactamente uno de toAddressId o toAddress.
stringtoAddress?objeto
Destino inline (ver el objeto dirección).
objetoreturnAddressId?string
Retorno guardado (máximo uno; por defecto se usa el origen).
stringreturnAddress?objeto
Retorno inline.
objetopurchase?objeto
Compra la guía en la misma llamada: carrierCode + serviceCode (servicio exacto) o strategy: cheapest. Ver Compra en una llamada.
objetoexternalId?string
Tu propia referencia, como el número de orden o folio. Búscala después con ?externalId=.
stringcarrierCode?string
Opcional: acota las cotizaciones a esta paquetería desde la creación (no compra por sí solo).
stringserviceLevel?string
Opcional: acota el servicio (p. ej. standard, express).
stringrequestInsurance?boolean
Solicitar seguro sobre el valor declarado. Al comprar una tarifa asegurada se crea una póliza real (ver Seguro y reclamaciones).
booleanfalsedeclaredValue?number
Valor declarado en MXN (cobertura del seguro).
numbermetadata?objeto
Pares llave-valor tuyos; se devuelven tal cual.
objetocurl -X POST https://api.sendit.mx/v1/shipments \
-H "X-API-Key: sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"externalId": "ORD-2026-001",
"fromAddressId": "clx_direccion_origen",
"toAddress": {
"contactName": "María López",
"contactPhone": "+5213312345678",
"street": "Av. López Mateos",
"exteriorNumber": "45",
"neighborhood": "Jardines del Sol",
"city": "Zapopan",
"state": "JAL",
"postalCode": "45050",
"country": "MX",
"saveToAddressBook": false
},
"parcel": {
"length": 30, "width": 20, "height": 15, "weight": 2.5,
"description": "Electrónicos"
},
"requestInsurance": true,
"declaredValue": 5000,
"metadata": { "orderId": "shopify-12345" }
}'
La respuesta 201 trae el envío en DRAFT con sus snapshots de dirección, el historial de eventos y las cotizaciones inline (rates[], ver Cotizaciones):
{
"success": true,
"data": {
"id": "clxq1w2e3r4t5y6u7i8o9p0a",
"status": "DRAFT",
"externalId": "ORD-2026-001",
"fromAddressSnapshot": {
"contactName": "Bruno Sánchez",
"city": "Ciudad de México",
"postalCode": "03100",
"savedAddressId": "clx_direccion_origen"
},
"toAddressSnapshot": {
"contactName": "María López",
"city": "Zapopan",
"postalCode": "45050",
"savedAddressId": null
},
"parcel": { "length": 30, "width": 20, "height": 15, "weight": 2.5 },
"rates": [
{
"id": "DHL_standard_a1b2c3",
"carrierCode": "DHL",
"serviceCode": "EXPRESS_WORLDWIDE",
"serviceLevel": "standard",
"totalPrice": 326.82,
"currency": "MXN"
}
],
"events": [
{ "status": "DRAFT", "description": "Shipment created", "occurredAt": "2026-07-17T12:00:00.000Z" }
],
"createdAt": "2026-07-17T12:00:00.000Z"
}
}
Compra en una llamada
Agrega un objeto purchase a POST /v1/shipments para crear el envío y comprar la guía en una sola petición. Elige una forma de seleccionar el servicio: carrierCode + serviceCode para el producto exacto, o strategy: "cheapest" para la tarifa más barata. Nunca mandes las dos ni omitas ambas. Con una llave de alcance acotado necesitas labels:write además de shipments:write.
Parámetros de purchase
carrierCode?string
Paquetería exacta; se usa junto con serviceCode. Excluyente con strategy.
stringserviceCode?string
Código nativo del servicio; se usa junto con carrierCode.
stringstrategy?string
cheapest: selecciona automáticamente la tarifa más barata. Excluyente con carrierCode + serviceCode.
stringlabelFormat?string
Formato de la guía generada: PDF | ZPL.
stringPDFasync?boolean
true devuelve labelPurchaseAttempt dentro de la respuesta 201. Consulta su statusUrl.
booleanfalsecurl -X POST https://api.sendit.mx/v1/shipments \
-H "X-API-Key: sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"fromAddressId": "clx_direccion_origen",
"toAddress": {
"contactName": "María López",
"contactPhone": "+5213312345678",
"street": "Av. López Mateos",
"exteriorNumber": "45",
"neighborhood": "Jardines del Sol",
"city": "Zapopan",
"state": "JAL",
"postalCode": "45050",
"country": "MX"
},
"parcel": { "length": 30, "width": 20, "height": 15, "weight": 2.5 },
"purchase": { "strategy": "cheapest", "labelFormat": "PDF" }
}'
En éxito, la respuesta 201 incluye además label (con trackingNumber, labelUrl, charged) y purchasedRate:
{
"success": true,
"data": {
"id": "clxq1w2e3r4t5y6u7i8o9p0a",
"status": "LABEL_PURCHASED",
"label": {
"trackingNumber": "1234567890",
"labelUrl": "https://labels.sendit.mx/clxq1w2e3r4t5y6u7i8o9p0a/1234567890.pdf",
"charged": "326.82",
"currency": "MXN"
},
"purchasedRate": {
"id": "DHL_standard_a1b2c3",
"carrierCode": "DHL",
"serviceCode": "EXPRESS_WORLDWIDE",
"serviceLevel": "standard",
"totalPrice": 326.82
}
}
}
Con purchase.async: true, la respuesta externa sigue siendo 201. Recibes labelPurchaseAttempt en lugar de label. Consulta su statusUrl o usa label.purchase.completed. Ver Operaciones asíncronas.
El envío siempre se crea primero: si la compra falla, te queda un DRAFT que puedes terminar por el flujo de dos pasos.
| Código | Cuándo | Cómo resolverlo |
|---|---|---|
402 INSUFFICIENT_BALANCE |
El monedero no cubre la guía | El envío queda en DRAFT; fondea el monedero y compra con POST /v1/shipments/:id/label |
422 ONE_CALL_BUY_RATES_PENDING |
Las paqueterías tardaron (>8 s) en cotizar | Sondea ratesPollUrl y compra con el rateId |
422 ONE_CALL_BUY_NO_MATCHING_RATE |
Ninguna tarifa coincidió con tu selección | Elige una de details.availableRates |
400 INVALID_INPUT |
purchase traía ambas formas de selección, o ninguna |
Envía carrierCode + serviceCode o strategy, no las dos |
Manda varias cajas juntas
Un envío lleva un parcel. Para despachar varias cajas en la misma operación, crea un envío por caja y agrúpalos en un lote. Compras todas las guías en una llamada y generas un solo manifiesto para la paquetería.
Las direcciones se congelan en snapshots
Al crear el envío, cada dirección se copia a un snapshot inmutable (fromAddressSnapshot, toAddressSnapshot, returnAddressSnapshot). Editar o borrar después la dirección guardada jamás altera los envíos históricos.
Siempre lee la dirección desde el snapshot, no desde el ID:
// Correcto: el snapshot es el valor canónico
const origin = shipment.fromAddressSnapshot;
// Incorrecto: la dirección guardada pudo cambiar o borrarse
const origin = await getAddress(shipment.fromAddressId);
Dentro del snapshot, savedAddressId indica de qué entrada del directorio provino (null si fue una dirección de un solo uso).
Lista y filtra
GET /v1/shipments?status=IN_TRANSIT&carrierCode=DHL&limit=50
| Filtro | Coincidencia |
|---|---|
status |
Exacta (DRAFT, IN_TRANSIT, …) |
statuses |
Varios estados a la vez (repite el parámetro o sepáralos por coma) |
carrierCode |
Exacta (DHL, FEDEX, ESTAFETA, …) |
trackingNumber |
Parcial (contiene) |
externalId |
Exacta |
search |
Texto libre sobre número de rastreo y externalId |
createdFrom / createdTo |
Rango de fechas de creación (ISO; from inclusivo, to exclusivo) |
La paginación por cursor y los operadores avanzados están en Paginación y filtros. Para tableros, GET /v1/shipments/stats devuelve el conteo por estado en una sola llamada.
Consulta el detalle
GET /v1/shipments/:id devuelve el envío completo: snapshots, parcel, resumen de la guía (si existe) y el historial de eventos:
{
"success": true,
"data": {
"id": "clxq1w2e3r4t5y6u7i8o9p0a",
"status": "IN_TRANSIT",
"trackingNumber": "TEST-DHL-A1B2C3D4",
"publicTrackingToken": "2f68c611-30af-4a86-9cde-793413af5f65",
"trackingUrl": "https://app.sendit.mx/track/2f68c611-30af-4a86-9cde-793413af5f65",
"events": [
{ "status": "IN_TRANSIT", "description": "Package in transit", "occurredAt": "2026-07-17T09:12:00.000Z" },
{ "status": "PICKED_UP", "description": "Package picked up", "occurredAt": "2026-07-17T08:03:00.000Z" }
]
}
}
Actualiza un borrador
Solo los envíos en DRAFT se pueden editar; después quedan bloqueados:
curl -X PUT https://api.sendit.mx/v1/shipments/{id} \
-H "X-API-Key: sk_test_..." \
-H "Content-Type: application/json" \
-d '{ "externalId": "ORD-2026-001-v2" }'
Un envío que ya avanzó devuelve 409 SHIPMENT_ALREADY_PROCESSED.
Cancela un envío
curl -X DELETE https://api.sendit.mx/v1/shipments/{id} \
-H "X-API-Key: sk_test_..."
No se puede cancelar en DELIVERED, RETURNED, FAILED ni si ya está CANCELLED. Si el envío ya tiene guía comprada y quieres el reembolso, cancela primero la guía. Ver Cancelaciones y reembolsos.
Crea un envío de retorno
POST /v1/shipments/:id/return crea un nuevo envío en DRAFT con la ruta invertida, ligado al original vía returnForShipmentId:
- from = el destino original (donde está el paquete ahora)
- to = la dirección de retorno original, o en su defecto el origen
- parcel = copiado del original (puedes sobreescribirlo si se reempaca)
La respuesta es idéntica a la de POST /v1/shipments, con rates[] inline. La guía de retorno se compra por el flujo normal. Reglas:
- El original debe tener guía (los
DRAFT,PENDING,CANCELLEDyRETURNEDse rechazan). - Un retorno activo por envío. Cancelar el retorno libera el cupo.
- No hay retornos de retornos.
- En modo de prueba funciona de punta a punta.