Rates
Compare rates from every carrier in a single call; the quoted total is exactly what you pay.
Rates come to you: when you create a shipment, the response already includes rates[] with quotes from every carrier enabled for that route. There is no separate quoting call to manage.
If you already know which carrier and service you want, or just want the cheapest, add the purchase object when you create the shipment. See One-call buy.
The rate object
{
"id": "DHL_standard_a1b2c3",
"carrierCode": "DHL",
"serviceCode": "EXPRESS_WORLDWIDE",
"carrierName": "DHL Express",
"serviceName": "DHL Express Nacional",
"serviceLevel": "standard",
"totalPrice": 326.82,
"currency": "MXN",
"isInsured": false,
"estimatedDays": { "min": 1, "max": 2 },
"expiresAt": "2026-07-18T14:30:00.000Z",
"breakdown": {
"baseRate": 264.50,
"fuelSurcharge": 17.24,
"insuranceCost": 0.00,
"subtotal": 281.74,
"ivaRate": 0.16,
"ivaAmount": 45.08,
"total": 326.82
}
}
| Field | Description |
|---|---|
id |
The rateId — pass it to the label purchase |
serviceCode |
The carrier-native exact product code |
serviceLevel |
Normalized across carriers (express, standard, economy, …) |
totalPrice |
The total to pay, IVA included — matches breakdown.total |
isInsured |
true if the rate includes insurance (you created the shipment with requestInsurance) |
estimatedDays |
Estimated business-day delivery range |
expiresAt |
Rate validity (24 hours) |
breakdown |
Transparent split: base rate, surcharges, insurance, subtotal, and IVA as a separate line |
If you created the shipment with requestInsurance: true and a declaredValue, rates come back with the premium in breakdown.insuranceCost and isInsured: true. Buying an insured rate creates a real policy you can claim against. See Insurance & claims.
A rate’s lifecycle
POST /v1/shipments
→ creates the shipment (DRAFT)
→ quotes every enabled carrier
→ returns the shipment + rates[] (valid: 24 h)
↓
GET /v1/shipments/:id/rates ← fetch or refresh any time
↓
POST /v1/shipments/:id/label { rateId } ← buy at the quoted price
If carriers are slow
Quoting has an 8-second budget. If a carrier lags, the shipment returns immediately with rates on the way:
{
"success": true,
"data": {
"id": "clxq1w2e3r4t5y6u7i8o9p0a",
"status": "DRAFT",
"rates": [],
"ratesStatus": "pending",
"ratesExpiresAt": null,
"ratesPollUrl": "/v1/shipments/clxq1w2e3r4t5y6u7i8o9p0a/rates"
}
}
Poll ratesPollUrl (e.g. every 2 seconds) until ratesStatus is "ready".
Fetch or refresh rates
GET /v1/shipments/:id/rates ← returns the current rates
GET /v1/shipments/:id/rates?refresh=true ← re-quotes the carriers
Without ?refresh=true you get the cached rates while their 24-hour validity lasts. With ?refresh=true everything is re-quoted and the validity resets.
When a rate expires
Buying with an expired rateId returns 410 RATES_EXPIRED:
{
"success": false,
"error": {
"code": "RATES_EXPIRED",
"message": "Shipping rates have expired. Please refresh rates and select again.",
"details": {
"shipmentId": "clxq1w2e3r4t5y6u7i8o9p0a",
"hint": "GET /v1/shipments/clxq1w2e3r4t5y6u7i8o9p0a/rates"
}
}
}
Refresh, pick a new rateId, and buy again. You’ll also see this error if you use a rateId belonging to a different shipment.
Quote without creating a shipment
For checkout widgets or pre-purchase estimates, use the standalone endpoint:
Body parameters
originobject
The origin. It needs at least { postalCode }.
objectdestinationobject
The destination. It needs at least { postalCode }.
objectparcelobject
The package: length, width, height (cm) and weight (kg).
objectcurl -X POST https://api.sendit.mx/v1/rates \
-H "X-API-Key: sk_test_..." \
-H "Content-Type: application/json" \
-d '{
"origin": { "postalCode": "03940" },
"destination": { "postalCode": "45050" },
"parcel": { "weight": 1.5, "length": 20, "width": 15, "height": 10 }
}'
The response returns rates[] (the same rate object above) and its validity:
{
"success": true,
"data": {
"rates": [
{ "id": "DHL_standard_a1b2c3", "carrierCode": "DHL", "serviceCode": "EXPRESS_WORLDWIDE", "serviceLevel": "standard", "totalPrice": 88.40, "currency": "MXN", "isInsured": false }
],
"expiresAt": "2026-07-18T14:30:00.000Z"
}
}
These rates are display-only: they can’t be used to buy a label. To buy, create the shipment first. You can limit to one carrier (POST /v1/rates/carrier/DHL) or sort with ?sortBy=price or ?sortBy=speed.
Sort rates
const byPrice = [...rates].sort((a, b) => a.totalPrice - b.totalPrice);
const bySpeed = [...rates].sort(
(a, b) => a.estimatedDays.min - b.estimatedDays.min || a.totalPrice - b.totalPrice
);