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

FieldTypeDescription
easypostIdstringRate ID (prefixed rate_)
carrierstringCarrier name (e.g. "USPS", "UPS", "FedEx")
servicestringService level (e.g. "Priority Mail", "Ground")
ratenumberPrice in USD
deliveryDaysnumber | nullEstimated transit days
deliveryDatestring | nullEstimated delivery date (ISO 8601)
purchaseTokenstringSigned token for purchasing this rate

Aggregated Rate Object (Multi-Box)

FieldTypeDescription
carrierstringCarrier name
servicestringService level
totalRatenumberSummed cost across all boxes
deliveryDaysnumber | nullEstimated transit days
is_regionalbooleanWhether the rate is from a regional carrier
perBoxShipmentIdsstring[]EasyPost shipment IDs for each box
perBoxRateIdsstring[]EasyPost rate IDs aligned to each box
purchaseTokenstringSigned token authorizing this aggregated purchase

Per-Box Shipment Object (Multi-Box)

FieldTypeDescription
packageIndexnumberZero-based package index
easypostShipmentIdstringEasyPost shipment ID (shp_...)
weightLbsnumberEstimated package weight in pounds
dimensionsobjectEstimated package dimensions
ratesarrayPer-box raw rates from EasyPost
itemIdsstring[]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

StatusCodeCause
400VALIDATION_ERROROrder has no warehouse assigned
403FORBIDDENOrder exists but belongs to another user
404NOT_FOUNDOrder not found
409CONFLICTOrder already has active shipments

Purchase Label (Single-Box)

POST /api/v1/orders/:id/labels

Purchases a shipping label for the selected rate.

Request Body

FieldTypeRequiredDescription
purchaseTokenstringyesSigned token from the rates response
easypostShipmentIdstringnoEasyPost shipment ID (shp_...)
rateIdstringnoEasyPost rate ID (rate_...)

Note: The purchaseToken encodes the shipment and rate IDs. You can optionally pass easypostShipmentId and rateId explicitly, 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

FieldTypeRequiredDescription
purchaseTokenstringyesSigned token from the aggregated rates response
purchasesarraynoOptional per-box array used to attach itemIds; if omitted, token shipment/rate pairs are used as-is

Per-box purchase object:

FieldTypeRequiredDescription
easypostShipmentIdstringyesEasyPost shipment ID for this box
rateIdstringyesRate ID for this box
itemIdsstring[]noOrder 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

StatusCodeCause
400VALIDATION_ERRORInvalid/expired purchase token, malformed IDs, or invalid itemIds
403FORBIDDENOrder or token does not belong to the calling API key/user
404NOT_FOUNDOrder not found
409CONFLICTOrder already has an active shipment