Labels
Buy a label (guía) from a rate: the quoted price is the charged price, no surprises.
Buying a label turns a rate into a print-ready shipping label (guía). Your wallet is debited at the quoted price, never more and never less. There are two ways to buy. Two-step compares rates and buys with a rateId, as shown below. One-call adds purchase when you create the shipment. See One-call buy.
Buy a label
You need a shipment with valid rates and the chosen rateId. The Idempotency-Key header is optional but recommended: retrying with the same key replays the result and avoids a double charge (see Idempotency).
Body parameters
rateIdstring
The chosen rate's id, taken from rates[]. Valid for 24 hours.
stringlabelFormat?string
PDF or ZPL. If omitted, PDF is used.
stringPDFexternalReference?string
Your own reference for this purchase. Up to 255 characters.
stringasync?boolean
true returns 202 and an attempt you poll later. See Asynchronous purchase.
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, // generated and persisted by you
"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"
}
}
}
The shipment moves to LABEL_PURCHASED, tracking goes live, and labelUrl points at the print-ready PDF.
One-call buy
If you already know the carrier and service, or just want the cheapest, skip the second step. Add a purchase object when you create the shipment. Choose carrierCode + serviceCode for the exact product, or 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_origin_address",
"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_origin_address",
toAddress: { /* ...destination... */ },
parcel: { length: 30, width: 20, height: 15, weight: 2.5 },
purchase: { strategy: "cheapest", labelFormat: "PDF" },
}),
}).then((r) => r.json());
// The label arrives in the same response
console.log(shipment.label.trackingNumber, shipment.label.labelUrl);The response includes purchasedRate and label, with trackingNumber, labelUrl, and charged. The shipment is always created first, so a failed purchase leaves you a recoverable DRAFT. The full failure semantics are in Shipments. With a scoped API key you need labels:write in addition to shipments:write.
The price contract
A purchase gives you three guarantees:
- If the balance is short, nothing happens. The purchase fails with
402 INSUFFICIENT_BALANCE. There is no charge and no label. - You are charged the quoted price. The chosen rate’s
totalPriceis debited exactly, IVA included. The amount is not recalculated at purchase. - A proven failure returns the charge. The attempt reaches
failedafter the refund is credited.
An inconclusive outcome reaches action_required. That status confirms neither a label nor a refund. Do not start another purchase for the shipment.
Retrying with the same Idempotency-Key replays the original result. It never creates a second charge. Details in Idempotency.
Asynchronous purchase
A purchase is synchronous by default: you wait and the label comes back in the response. With async: true the request returns at once and you read the outcome later. Use it for volume without one open connection per label. See Asynchronous operations.
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 }'
The response is 202 Accepted. The Location header points at the attempt, and Retry-After: 2 is a polling hint:
{
"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
}
}
Poll the attempt until it reaches a terminal status:
GET /v1/label-purchase-attempts/lat_550e8400-e29b-41d4-a716-446655440000
status |
Meaning |
|---|---|
pending |
Accepted, not started |
processing |
In progress |
succeeded |
Done. The attempt carries the label |
failed |
The failure ended and the charge was refunded. No label exists |
action_required |
The outcome is inconclusive. No label or refund is implied |
Three things worth knowing:
- Funds are checked before the attempt is accepted. If the balance is short you get
402 INSUFFICIENT_BALANCEimmediately, and no half-finished attempt is left behind. - Repeating the same purchase returns the same attempt, as long as
rateId, format, external reference, and theasyncpreference all match. Change any of them and you get409 SHIPMENT_LABEL_IN_PROGRESS. - To hear about it without polling, subscribe to the
label.purchase.completedwebhook: it fires on all three terminal outcomes.
Label formats
labelFormat |
Use |
|---|---|
PDF |
Default. Letter size, print-ready |
ZPL |
Zebra thermal printers (raw) |
If you omit labelFormat, PDF is used.
Purchase errors
| Code | When | How to resolve it |
|---|---|---|
402 INSUFFICIENT_BALANCE |
Balance can’t cover totalPrice |
Fund your wallet; details includes the shortfall |
410 RATES_EXPIRED |
The rate expired (24 h) | GET /v1/shipments/:id/rates and buy with the fresh rateId |
409 SHIPMENT_ALREADY_PROCESSED |
Shipment already has a label or isn’t DRAFT |
Check the shipment; for another label, create another shipment |
409 SHIPMENT_LABEL_IN_PROGRESS |
An active attempt already owns the shipment | Retrieve the existing attempt; do not start another purchase |
502 CARRIER_ERROR |
The carrier conclusively rejected the purchase | Confirm that the attempt reached failed before buying again |
After the purchase
- Tracking: the
trackingNumberstarts producing tracking events and webhooks. - Pickup: schedule the carrier to collect the package. See Pickups.
- If you made a mistake: void an unused label for a full refund. See Refunds & voids.