Guías
Compra una guía a partir de una cotización: el precio cotizado es el precio cobrado, sin sorpresas.
Comprar una guía convierte una cotización en una etiqueta lista para imprimir. El monedero se debita al precio cotizado, nunca más y nunca menos. Hay dos maneras de comprar. En dos pasos comparas cotizaciones y compras con un rateId, como se muestra aquí abajo. En una llamada agregas purchase al crear el envío. Ver Compra en una llamada.
Compra una guía
Necesitas un envío con cotizaciones vigentes y el rateId elegido. El encabezado Idempotency-Key es opcional pero recomendado: reintentar con la misma llave reproduce el resultado y evita un doble cargo (ver Idempotencia).
Parámetros del cuerpo
rateIdstring
El id de la tarifa elegida, tomado de rates[]. Vigencia: 24 horas.
stringlabelFormat?string
PDF o ZPL. Si lo omites, se usa PDF.
stringPDFexternalReference?string
Tu propia referencia para esta compra. Máximo 255 caracteres.
stringasync?boolean
true devuelve 202 y un intento que consultas después. Ver Compra asíncrona.
booleanfalsecurl -X POST https://api.sendit.mx/v1/shipments/clxq1w2e3r4t5y6u7i8o9p0a/label \
-H "X-API-Key: sk_test_..." \
-H "Idempotency-Key: 1f0e6f0e-59a4-4a6f-9d2e-9b1a7b2c3d4e" \
-H "Content-Type: application/json" \
-d '{ "rateId": "DHL_standard_a1b2c3", "labelFormat": "PDF" }'const res = await fetch(
`https://api.sendit.mx/v1/shipments/${shipmentId}/label`,
{
method: "POST",
headers: {
"X-API-Key": process.env.SENDIT_API_KEY,
"Idempotency-Key": idempotencyKey, // generada y persistida por ti
"Content-Type": "application/json",
},
body: JSON.stringify({ rateId, labelFormat: "PDF" }),
}
);
const { data: label } = await res.json();{
"success": true,
"data": {
"shipmentId": "clxq1w2e3r4t5y6u7i8o9p0a",
"attemptId": "lat_550e8400-e29b-41d4-a716-446655440000",
"labelId": "clxlbl456abc789def012ghi",
"trackingNumber": "1234567890",
"labelUrl": "https://labels.sendit.mx/clxq1w2e3r4t5y6u7i8o9p0a/1234567890.pdf",
"carrierCode": "DHL",
"serviceCode": "EXPRESS_WORLDWIDE",
"serviceName": "DHL Express Nacional",
"charged": "326.82",
"currency": "MXN",
"walletBalanceAfter": "12158.43",
"breakdown": {
"quotedTotal": "326.82",
"ivaAmount": "45.08",
"overageCharge": "0.00",
"total": "326.82"
}
}
}
El envío pasa a LABEL_PURCHASED, el rastreo queda activo y labelUrl apunta al PDF listo para imprimir.
Compra en una llamada
Si ya sabes con qué paquetería y servicio enviarás, o solo quieres la más barata, sáltate el segundo paso. Agrega un objeto purchase al crear el envío y recibe la guía en la misma respuesta. Elige carrierCode + serviceCode para el producto exacto, o strategy: "cheapest".
curl -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": { "carrierCode": "DHL", "serviceCode": "EXPRESS_WORLDWIDE", "labelFormat": "PDF" }
}'const { data: shipment } = await fetch("https://api.sendit.mx/v1/shipments", {
method: "POST",
headers: {
"X-API-Key": process.env.SENDIT_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
fromAddressId: "clx_direccion_origen",
toAddress: { /* ...destino... */ },
parcel: { length: 30, width: 20, height: 15, weight: 2.5 },
purchase: { strategy: "cheapest", labelFormat: "PDF" },
}),
}).then((r) => r.json());
// La guía llega en la misma respuesta
console.log(shipment.label.trackingNumber, shipment.label.labelUrl);La respuesta incluye purchasedRate y label, con trackingNumber, labelUrl y charged. El envío siempre se crea primero, así que una compra fallida te deja un DRAFT recuperable. La semántica de fallos completa está en Envíos. Con una llave de alcance acotado necesitas labels:write además de shipments:write.
El contrato de precio
La compra te da tres garantías:
- Si el saldo no alcanza, no pasa nada. La compra falla con
402 INSUFFICIENT_BALANCE. No hay cargo, no hay guía. - Se cobra el precio cotizado. Se debita exactamente el
totalPricede la tarifa elegida, con IVA incluido. El monto no se recalcula al comprar. - Una falla comprobada devuelve el cargo. El intento termina en
faileddespués de acreditar el reembolso.
Un resultado no concluyente termina en action_required. Ese estado no confirma una guía ni un reembolso. No inicies otra compra para el mismo envío.
Reintentar con la misma Idempotency-Key reproduce el resultado original. Nunca genera un segundo cargo. Detalles en Idempotencia.
Compra asíncrona
Por defecto la compra es síncrona: esperas y recibes la guía en la respuesta. Con async: true la petición regresa de inmediato y tú consultas el resultado después. Sirve cuando compras en volumen y no quieres mantener una conexión abierta por cada guía. Ver Operaciones asíncronas.
curl -X POST https://api.sendit.mx/v1/shipments/clxq1w2e3r4t5y6u7i8o9p0a/label \
-H "X-API-Key: sk_test_..." \
-H "Content-Type: application/json" \
-d '{ "rateId": "DHL_standard_a1b2c3", "async": true }'
La respuesta es 202 Accepted. El encabezado Location apunta al intento y Retry-After: 2 te sugiere cada cuánto consultar:
{
"success": true,
"data": {
"id": "lat_550e8400-e29b-41d4-a716-446655440000",
"object": "label_purchase_attempt",
"shipmentId": "clxq1w2e3r4t5y6u7i8o9p0a",
"externalReference": null,
"status": "pending",
"livemode": false,
"async": true,
"pricing": {
"quotedTotal": "326.82",
"ivaAmount": "45.08",
"overageCharge": "0.00",
"netWalletCost": "326.82"
},
"charged": "326.82",
"currency": "MXN",
"walletBalanceAfter": "12158.43",
"refundedAmount": null,
"label": null,
"error": null,
"statusUrl": "/v1/label-purchase-attempts/lat_550e8400-e29b-41d4-a716-446655440000",
"createdAt": "2026-08-01T10:00:00.000Z",
"updatedAt": "2026-08-01T10:00:00.000Z",
"completedAt": null
}
}
Consulta el intento hasta que llegue a un estado terminal:
GET /v1/label-purchase-attempts/lat_550e8400-e29b-41d4-a716-446655440000
status |
Significado |
|---|---|
pending |
Aceptado, aún no empieza |
processing |
En curso |
succeeded |
Listo. El intento trae la guía |
failed |
La falla terminó y el cargo se reembolsó. No hay guía |
action_required |
El resultado no es concluyente. No hay guía ni reembolso implícito |
Tres cosas que conviene saber:
- El saldo se valida antes de aceptar. Si no alcanza, recibes
402 INSUFFICIENT_BALANCEde inmediato y no queda ningún intento a medias. - Repetir la misma compra te devuelve el mismo intento, siempre que coincidan
rateId, formato, referencia externa y preferencia deasync. Si cambia algo, recibes409 SHIPMENT_LABEL_IN_PROGRESS. - Para enterarte sin sondear, suscríbete al webhook
label.purchase.completed: se dispara en los tres desenlaces terminales.
Formatos de guía
labelFormat |
Uso |
|---|---|
PDF |
Por defecto. Tamaño carta, listo para imprimir |
ZPL |
Impresoras térmicas Zebra (raw) |
Si no envías labelFormat, se usa PDF.
Errores de compra
| Código | Cuándo | Cómo resolverlo |
|---|---|---|
402 INSUFFICIENT_BALANCE |
El saldo no cubre el totalPrice |
Fondea tu monedero; details incluye el faltante |
410 RATES_EXPIRED |
La cotización venció (24 h) | GET /v1/shipments/:id/rates y compra con el nuevo rateId |
409 SHIPMENT_ALREADY_PROCESSED |
El envío ya tiene guía o no está en DRAFT |
Consulta el envío; si necesitas otra guía, crea otro envío |
409 SHIPMENT_LABEL_IN_PROGRESS |
Ya existe un intento activo para el envío | Consulta el intento existente; no inicies otra compra |
502 CARRIER_ERROR |
La paquetería rechazó la compra de forma concluyente | Confirma que el intento terminó en failed antes de volver a comprar |
Después de la compra
- Rastreo: el
trackingNumberempieza a generar eventos de rastreo y webhooks. - Recolección: programa que la paquetería pase por el paquete. Ver Recolecciones.
- Si te equivocaste: una guía sin usar se cancela con reembolso completo. Ver Cancelaciones y reembolsos.