Overview Flow Guide
The end-to-end path for shipping with the Easyship API: compare rates, create a shipment, buy the label, hand the parcel to the courier, and track it to delivery. Most integrations need just three calls.
public-api.easyship.com2024-09 (in the path)Authorization: Bearer <token>The shipment lifecycle
Steps 2 and 3 can be a single call: create the shipment with buy_label: true and the label comes back in the same response.
Before you start
Credentials
| Item | Value |
|---|---|
| Token | Create an API integration in the Easyship dashboard. Sandbox tokens start with sand_, production tokens with prod_. See Authentication |
| Header | Authorization: Bearer <token> and Content-Type: application/json |
| Scopes | public.rate:read, public.shipment:write, public.label:write, public.pickup:write, public.track:read. See Scopes |
| Testing | Use a sandbox token to test without real labels or charges. See Sandbox |
Data you need for each shipment
| Data | Notes |
|---|---|
| Origin and destination addresses | Include city and state for the US, CA, MX and AU. Validate them first with Address Validation |
| Parcel | Total weight plus box dimensions, or weight and dimensions per item |
| Items | Description, quantity and value. For international shipments, add hs_code and origin_country_alpha2 |
| Delivery terms | incoterms: DDU (buyer pays import tax) or DDP (you pay). See Taxes and Duties |
Choose your integration path
POST /shipments with courier_service_id and buy_label: true. Add buy_label_synchronous: true to get the label in the response.
Best for: checkout flows where the courier is already chosen.
Create the shipment, review its rates, then call POST /shipments/{id}/label.
Best for: back-office tools where someone confirms the courier.
POST /batches/shipments and POST /batches/labels run in the background. Results arrive by webhook.
Best for: hundreds of orders at once.
Step by step
- Get rates
Compare every courier available for the parcel. Each rate has a
courier_service.id, atotal_charge, delivery days and ranking fields. Full details: How to Get Shipping Rates.cURLcurl -X POST https://public-api.easyship.com/2024-09/rates \ -H "Authorization: Bearer $EASYSHIP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "origin_address": { "line_1": "1 Market St", "city": "San Francisco", "state": "CA", "postal_code": "94105", "country_alpha2": "US" }, "destination_address": { "line_1": "10 Downing Street", "city": "London", "postal_code": "SW1A 2AA", "country_alpha2": "GB" }, "incoterms": "DDU", "parcels": [{ "total_actual_weight": 1.2, "box": { "length": 30, "width": 20, "height": 10 }, "items": [{ "description": "T-shirt", "category": "fashion", "hs_code": "61091000", "quantity": 2, "declared_currency": "USD", "declared_customs_value": 25 }] }] }'Pick a rate and keep its
courier_service.id.Optional. If you skip this step,POST /shipmentsstill returns all rates and assigns the best-value courier. - Create the shipment
Send the same addresses and parcels, plus the chosen courier in
courier_settings. Add contact details (contact_name,contact_phone,contact_email) to both addresses; couriers need them on the label.cURLcurl -X POST https://public-api.easyship.com/2024-09/shipments \ -H "Authorization: Bearer $EASYSHIP_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1001" \ -d '{ "origin_address": { "...": "as in step 1, plus contact details" }, "destination_address": { "...": "as in step 1, plus contact details" }, "incoterms": "DDU", "courier_settings": { "courier_service_id": "01563646-58c1-4607-8fe0-cae3e33c0001", "allow_fallback": false }, "shipping_settings": { "buy_label": false }, "parcels": [{ "...": "as in step 1" }] }'Response{ "shipment": { "easyship_shipment_id": "ESUS10006002", "shipment_state": "created", "label_state": "not_created", "courier_service": { "id": "01563646-58c1-4607-8fe0-cae3e33c0001", "name": "DHL Express Worldwide" }, "rates": [ ... ], "tracking_page_url": "https://www.trackmyshipment.co/shipment-tracking/ESUS10006002" } }Option Effect courier_settings.allow_fallbackIf the chosen courier can't be used, let Easyship pick another instead of failing courier_settings.apply_shipping_rulesApply the shipping rules set up on your account shipping_settings.buy_labelBuy the label in the same call (skips step 3) shipping_settings.buy_label_synchronousWith buy_label: wait and return the label in the response. Not available for batchesIdempotency-KeyheaderRetry safely: a repeated request with the same key returns the first successful response instead of creating a second shipment To change the shipment before buying the label, use Update a Shipment.
- Buy the label
This books the courier, charges your balance and returns the label and other shipping documents. You can pass
courier_service_idhere to set or change the courier; if none is set, Easyship assigns the best-value courier.cURLcurl -X POST https://public-api.easyship.com/2024-09/shipments/ESUS10006002/label \ -H "Authorization: Bearer $EASYSHIP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "courier_service_id": "01563646-58c1-4607-8fe0-cae3e33c0001" }'Response{ "shipment": { "easyship_shipment_id": "ESUS10006002", "label_state": "generated", "label_paid_at": "2026-10-15T10:21:00Z", "shipping_documents": [ { "category": "label", "format": "pdf", "page_size": "4x6", "base64_encoded_strings": ["JVBERi0xLjUK..."] } ], "trackings": [ ... ] } }Decode
base64_encoded_stringsto get the file, or fetch it later withGET /shipments/{id}/documents. Need a different format or page size? Passprinting_options.Buying the label commits the shipment. From here the courier is booked. To stop it, use Cancel a Shipment. - Hand the parcel to the courier
Drop it off, or book a collection with
POST /pickupsif the courier offers pickups. Send thecourier_service_id, aselected_date, a time window (or atime_slot_id) and the shipment IDs.cURLcurl -X POST https://public-api.easyship.com/2024-09/pickups \ -H "Authorization: Bearer $EASYSHIP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "courier_service_id": "01563646-58c1-4607-8fe0-cae3e33c0001", "selected_date": "2026-10-16", "selected_from_time": "12:00", "selected_to_time": "16:00", "easyship_shipment_ids": ["ESUS10006002"] }' - Track the shipment
Tracking starts automatically once the label exists. Subscribe to the tracking webhooks below, or look it up:
cURLcurl "https://public-api.easyship.com/2024-09/shipments/trackings?easyship_shipment_id=ESUS10006002&include_checkpoints=true" \ -H "Authorization: Bearer $EASYSHIP_TOKEN"You can also share
tracking_page_urlwith your buyer. Full details: How to Track a Shipment.
Webhooks to subscribe to
Create a webhook with the Webhooks API and list these names in event_types. In each delivery, the payload's event_type uses the dotted form shown under each name.
| Event | Fires when |
|---|---|
shipment_label_createdarrives as shipment.label.created | A label is ready (useful for batches and async labels) |
shipment_label_failedarrives as shipment.label.failed | Label generation failed |
shipment_tracking_status_changedarrives as shipment.tracking.status.changed | The delivery status changes, e.g. in transit → delivered |
shipment_tracking_checkpoints_createdarrives as shipment.tracking.checkpoints.created | The courier reports a new tracking event |
shipment_cancelledarrives as shipment.cancelled | A shipment is cancelled |
Shipment states
Every shipment response carries four state fields. These are the values you'll see most:
| Field | Tracks | Common values |
|---|---|---|
shipment_state | The shipment record | created, cancelled |
label_state | The label | not_created → generating → generated / printed, or failed |
pickup_state | Hand-over to the courier | not_requested until a pickup is booked |
delivery_state | Courier to buyer | in_transit_to_customer, out_for_delivery, delivered, exception, returned_to_shipper |
FAQ and common errors
POST /shipments returned 202 instead of 201.buy_label was set. meta.status is partial_success and meta.errors explains why. Fix the cause, then call POST /shipments/{id}/label.hs_code, item values) for international shipments. Check label_state and last_failure_http_response_messages on the shipment.allow_fallback: false to get an error instead of a substitute, or use GET /shipments/{id}/unavailable_couriers to see why.Updated 15 days ago
