Rates & Labels
The shipping flow is: get rates for an order, then purchase a label using the rate you want. The API supports both single-box and multi-box shipments.
Get Shipping Rates
POST /api/v1/orders/:id/rates
Fetches available shipping rates for an order. The response includes a signed purchaseToken that must be used when purchasing a label.
Prerequisites
- The order must have a warehouse assigned
- The order must not have active (non-voided) shipments
Example
curl -X POST https://shipwave.app/api/v1/orders/clx1abc2d3efg/rates \
-H "Authorization: Bearer sw_live_abc123..."
const res = await fetch(
"https://shipwave.app/api/v1/orders/clx1abc2d3efg/rates",
{
method: "POST",
headers: { Authorization: "Bearer sw_live_abc123..." },
}
);
const { data } = await res.json();
Response (Single-Box)
When packing resolves to a single package, you get one shipment with direct rate options:
{
"data": {
"orderId": "clx1abc2d3efg",
"easypostShipmentId": "shp_abc123",
"isMultiBox": false,
"packageCount": 1,
"rates": [
{
"easypostId": "rate_xyz789",
"carrier": "USPS",
"service": "Priority Mail",
"rate": 12.50,
"deliveryDays": 2,
"deliveryDate": "2026-02-21T00:00:00.000Z",
"purchaseToken": "eyJhbGciOi..."
},
{
"easypostId": "rate_abc456",
"carrier": "UPS",
"service": "Ground",
"rate": 9.75,
"deliveryDays": 5,
"deliveryDate": "2026-02-24T00:00:00.000Z",
"purchaseToken": "eyJhbGciOi..."
}
],
"purchaseTokenTtlSeconds": 900
}
}
Response (Multi-Box)
When packing resolves to multiple packages, rates are aggregated by carrier/service across all boxes:
{
"data": {
"orderId": "clx1abc2d3efg",
"isMultiBox": true,
"packageCount": 3,
"aggregatedRates": [
{
"carrier": "UPS",
"service": "Ground",
"totalRate": 45.00,
"deliveryDays": 4,
"is_regional": false,
"perBoxShipmentIds": ["shp_box1", "shp_box2", "shp_box3"],
"perBoxRateIds": ["rate_a1", "rate_a2", "rate_a3"],
"purchaseToken": "eyJhbGciOi..."
},
{
"carrier": "USPS",
"service": "Priority Mail",
"totalRate": 52.25,
"deliveryDays": 3,
"is_regional": false,
"perBoxShipmentIds": ["shp_box1", "shp_box2", "shp_box3"],
"perBoxRateIds": ["rate_b1", "rate_b2", "rate_b3"],
"purchaseToken": "eyJhbGciOi..."
}
],
"perBoxShipments": [
{
"packageIndex": 0,
"easypostShipmentId": "shp_box1",
"weightLbs": 3.0,
"dimensions": { "length": 12, "width": 8, "height": 6 },
"rates": [
{
"easypostId": "rate_a1",
"carrier": "UPS",
"service": "Ground",
"rate": 15.0,
"deliveryDays": 4,
"is_regional": false
}
],
"itemIds": ["clx1item1", "clx1item2"]
},
{
"packageIndex": 1,
"easypostShipmentId": "shp_box2",
"weightLbs": 2.8,
"dimensions": { "length": 12, "width": 8, "height": 6 },
"rates": [
{
"easypostId": "rate_a2",
"carrier": "UPS",
"service": "Ground",
"rate": 15.0,
"deliveryDays": 4,
"is_regional": false
}
],
"itemIds": ["clx1item3"]
},
{
"packageIndex": 2,
"easypostShipmentId": "shp_box3",
"weightLbs": 1.9,
"dimensions": { "length": 10, "width": 8, "height": 4 },
"rates": [
{
"easypostId": "rate_a3",
"carrier": "UPS",
"service": "Ground",
"rate": 15.0,
"deliveryDays": 4,
"is_regional": false
}
],
"itemIds": ["clx1item4"]
}
],
"purchaseTokenTtlSeconds": 900
}
}
Single-Box Rate Object
| Field | Type | Description |
|---|---|---|
easypostId | string | Rate ID (prefixed rate_) |
carrier | string | Carrier name (e.g. "USPS", "UPS", "FedEx") |
service | string | Service level (e.g. "Priority Mail", "Ground") |
rate | number | Price in USD |
deliveryDays | number | null | Estimated transit days |
deliveryDate | string | null | Estimated delivery date (ISO 8601) |
purchaseToken | string | Signed token for purchasing this rate |
Aggregated Rate Object (Multi-Box)
| Field | Type | Description |
|---|---|---|
carrier | string | Carrier name |
service | string | Service level |
totalRate | number | Summed cost across all boxes |
deliveryDays | number | null | Estimated transit days |
is_regional | boolean | Whether the rate is from a regional carrier |
perBoxShipmentIds | string[] | EasyPost shipment IDs for each box |
perBoxRateIds | string[] | EasyPost rate IDs aligned to each box |
purchaseToken | string | Signed token authorizing this aggregated purchase |
Per-Box Shipment Object (Multi-Box)
| Field | Type | Description |
|---|---|---|
packageIndex | number | Zero-based package index |
easypostShipmentId | string | EasyPost shipment ID (shp_...) |
weightLbs | number | Estimated package weight in pounds |
dimensions | object | Estimated package dimensions |
rates | array | Per-box raw rates from EasyPost |
itemIds | string[] | Order line item IDs allocated to the box |
Purchase Token
The purchaseToken is a signed, time-limited token that authorizes a label purchase. It expires after 15 minutes (purchaseTokenTtlSeconds: 900). After expiration, request new rates.
Tokens are bound to:
- the order ID
- the owning user
- the specific API key used to fetch rates
- the approved shipment/rate pairs (single or multi-box)
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | Order has no warehouse assigned |
| 403 | FORBIDDEN | Order exists but belongs to another user |
| 404 | NOT_FOUND | Order not found |
| 409 | CONFLICT | Order already has active shipments |
Purchase Label (Single-Box)
POST /api/v1/orders/:id/labels
Purchases a shipping label for the selected rate.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
purchaseToken | string | yes | Signed token from the rates response |
easypostShipmentId | string | no | EasyPost shipment ID (shp_...) |
rateId | string | no | EasyPost rate ID (rate_...) |
Note: The
purchaseTokenencodes the shipment and rate IDs. You can optionally passeasypostShipmentIdandrateIdexplicitly, but the token is always validated.
Example
curl -X POST https://shipwave.app/api/v1/orders/clx1abc2d3efg/labels \
-H "Authorization: Bearer sw_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"purchaseToken": "eyJhbGciOi...",
"easypostShipmentId": "shp_abc123",
"rateId": "rate_xyz789"
}'
const res = await fetch(
"https://shipwave.app/api/v1/orders/clx1abc2d3efg/labels",
{
method: "POST",
headers: {
Authorization: "Bearer sw_live_abc123...",
"Content-Type": "application/json",
},
body: JSON.stringify({
purchaseToken: "eyJhbGciOi...",
easypostShipmentId: "shp_abc123",
rateId: "rate_xyz789",
}),
}
);
const { data } = await res.json(); // 201 Created
Response (201 Created)
{
"data": {
"shipment": {
"id": "clx2ship1",
"trackingCode": "9400111899223100012345",
"labelUrl": "https://easypost-files.s3.amazonaws.com/files/postage_label/label.png",
"carrier": "USPS",
"service": "Priority Mail",
"rate": 12.50
}
}
}
Idempotency
If a label has already been purchased for this order, the existing shipment is returned with a 200 OK instead of creating a duplicate.
Purchase Labels (Multi-Box)
POST /api/v1/orders/:id/labels
For multi-box shipments, you can pass a purchases array to attach itemIds per package. All boxes are purchased atomically — if any box fails, all previously purchased labels in the batch are voided automatically.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
purchaseToken | string | yes | Signed token from the aggregated rates response |
purchases | array | no | Optional per-box array used to attach itemIds; if omitted, token shipment/rate pairs are used as-is |
Per-box purchase object:
| Field | Type | Required | Description |
|---|---|---|---|
easypostShipmentId | string | yes | EasyPost shipment ID for this box |
rateId | string | yes | Rate ID for this box |
itemIds | string[] | no | Order item IDs in this box (for scoped fulfillment) |
Example
curl -X POST https://shipwave.app/api/v1/orders/clx1abc2d3efg/labels \
-H "Authorization: Bearer sw_live_abc123..." \
-H "Content-Type: application/json" \
-d '{
"purchaseToken": "eyJhbGciOi...",
"purchases": [
{
"easypostShipmentId": "shp_box1",
"rateId": "rate_a1",
"itemIds": ["clx1item1", "clx1item2"]
},
{
"easypostShipmentId": "shp_box2",
"rateId": "rate_a2",
"itemIds": ["clx1item3"]
},
{
"easypostShipmentId": "shp_box3",
"rateId": "rate_a3",
"itemIds": ["clx1item4"]
}
]
}'
Response (201 Created)
{
"data": {
"isMultiBox": true,
"shipments": [
{
"id": "clx2ship1",
"trackingCode": "1Z999AA10123456784",
"labelUrl": "https://easypost-files.s3.amazonaws.com/label_box1.png",
"carrier": "UPS",
"service": "Ground",
"rate": 15.00,
"boxIndex": 0
},
{
"id": "clx2ship2",
"trackingCode": "1Z999AA10123456785",
"labelUrl": "https://easypost-files.s3.amazonaws.com/label_box2.png",
"carrier": "UPS",
"service": "Ground",
"rate": 15.00,
"boxIndex": 1
},
{
"id": "clx2ship3",
"trackingCode": "1Z999AA10123456786",
"labelUrl": "https://easypost-files.s3.amazonaws.com/label_box3.png",
"carrier": "UPS",
"service": "Ground",
"rate": 15.00,
"boxIndex": 2
}
]
}
}
Rollback on Failure
If purchasing any box fails, all previously purchased labels in the batch are automatically voided via the EasyPost refund API. The error response indicates which box failed:
{
"error": {
"code": "INTERNAL_ERROR",
"message": "Failed on box 3 of 5: <reason>. All previously purchased labels have been voided.",
"details": {
"rolledBack": true,
"voidedShipmentCount": 2
}
}
}
End-to-End Example
const API = "https://shipwave.app/api/v1";
const headers = {
Authorization: "Bearer sw_live_abc123...",
"Content-Type": "application/json",
};
// 1. Get rates
const ratesRes = await fetch(`${API}/orders/${orderId}/rates`, {
method: "POST",
headers,
});
const { data: rateData } = await ratesRes.json();
if (rateData.isMultiBox) {
// 2a. Multi-box: pick an aggregated rate and build purchases
const chosen = rateData.aggregatedRates[0]; // cheapest
const purchases = chosen.perBoxShipmentIds.map((shipId, i) => ({
easypostShipmentId: shipId,
rateId: chosen.perBoxRateIds[i],
itemIds: rateData.perBoxShipments[i]?.itemIds,
}));
const labelRes = await fetch(`${API}/orders/${orderId}/labels`, {
method: "POST",
headers,
body: JSON.stringify({ purchaseToken: chosen.purchaseToken, purchases }),
});
const { data: labelData } = await labelRes.json();
console.log(`Purchased ${labelData.shipments.length} labels`);
} else {
// 2b. Single-box: pick a rate directly
const chosen = rateData.rates[0]; // cheapest
const labelRes = await fetch(`${API}/orders/${orderId}/labels`, {
method: "POST",
headers,
body: JSON.stringify({
purchaseToken: chosen.purchaseToken,
easypostShipmentId: rateData.easypostShipmentId,
rateId: chosen.easypostId,
}),
});
const { data: labelData } = await labelRes.json();
console.log(`Tracking: ${labelData.shipment.trackingCode}`);
}
Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid/expired purchase token, malformed IDs, or invalid itemIds |
| 403 | FORBIDDEN | Order or token does not belong to the calling API key/user |
| 404 | NOT_FOUND | Order not found |
| 409 | CONFLICT | Order already has an active shipment |