REST API · Guide
Labels & tracking
The usual flow is four calls: quote → buy → download the label → track. All paths are under https://api.parcelhub.ca; see Get started for the token.
1. Get rates
Send the addresses and one entry per box. You get every carrier's options, cheapest first, and a quote_id.
Nothing is charged.
curl -s https://api.parcelhub.ca/v1/rates -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
"from": { "name": "Warehouse", "street": "120 Adelaide St W", "city": "Toronto",
"state": "ON", "postal_code": "M5H 1T1", "country": "CA", "phone": "4165550100" },
"to": { "name": "Sam Lee", "street": "1 Yonge St", "city": "Toronto", "state": "ON",
"postal_code": "M5E 1W7", "country": "CA", "is_residential": true },
"packages": [
{ "weight": 2, "weight_unit": "lb", "length": 10, "width": 8, "height": 4, "dimension_unit": "in" },
{ "weight": 0.5, "weight_unit": "lb", "length": 6, "width": 6, "height": 2, "dimension_unit": "in" }
],
"signature_option": "required",
"ship_date": "2026-11-03"
}' amount_centsis the price before tax.published_cents, when present, is the carrier's own list price for the same service.pickup_supported: falsemeans the carrier won't collect this parcel: it must be dropped off.tracking_available: falsemarks a service with no tracking.carriers_with_no_optionslists carriers that returned nothing for this parcel, with a reason.ship_datecan be up to 7 days ahead (leave it out for today). Use the same date when you buy.signature_optionisnone,requiredoradult; the quote includes the carrier's charge for it.- Set
is_residential: truefor home addresses: some carriers charge more, and quoting it up front avoids a later adjustment. - Want one carrier only? Add
"carriers": ["canada_post"](or several, e.g.["gls", "canpar"]). Only those carriers are quoted, so the call is faster too. Codes are thecarriervalues on each rate:canada_post,ups,fedex,purolator,canpar,gls,uniuni. See the carrier guides.
Need a number before you have full details? POST /v1/rates/quick is a lighter estimate. Always quote with POST /v1/rates before buying.
2. Show the total (optional)
For the exact charge with sales tax, any discount and prepaid duties, before buying:
curl -s https://api.parcelhub.ca/v1/shipments/price-preview -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
"amount_cents": 1288, "from_country": "CA", "to_country": "CA", "to_state": "ON",
"discount_code": "WELCOME10"
}'
# → subtotal_cents, discount_cents, tax_cents, tax_lines, duties_cents, total_cents 3. Buy the label
Send the same addresses and packages you quoted, plus the chosen rate's carrier, service_code and service_name exactly as returned, and the quote_id. The wallet is charged.
curl -s https://api.parcelhub.ca/v1/shipments -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: order-1042' -d '{
"carrier": "<carrier from the rate>", "service_code": "<service_code>", "service_name": "<service_name>",
"quote_id": "<quote_id from /v1/rates>",
"from": { … the same as the quote … }, "to": { … }, "packages": [ { …, "items": [] } ],
"ship_date": "2026-11-03", "signature_option": "required",
"reference_number": "Order 1042", "email_notifications": true, "label_size": "thermal"
}' Idempotency-Key. Your order number works well. If the connection drops and you retry
with the same key, you get the first shipment back instead of a second label.- 409 with
insufficient_funds: the wallet doesn't cover it. Top up and retry with the same key. - 400: something is wrong or the rate is no longer offered; the message says which. Re-quote if needed.
- 503: the carrier didn't answer. Nothing was bought; retry with the same key in a minute.
- Shipments that cross a border also need
customs_declaration_id: see Cross-border.
4. Label, tracking and search
# Label PDF link (valid ~15 min). Add ?box=2 for one box of a multi-box shipment. curl -s https://api.parcelhub.ca/v1/shipments/$ID/label -H "Authorization: Bearer $TOKEN" # Tracking events, newest first curl -s https://api.parcelhub.ca/v1/shipments/$ID/tracking -H "Authorization: Bearer $TOKEN" # Find shipments: by status, dates, or a search (order no., tracking no., recipient) curl -s "https://api.parcelhub.ca/v1/shipments?search=1042&limit=20" -H "Authorization: Bearer $TOKEN"
Labels come as PDF: thermal (4×6 in) or standard (8.5×11 in), from label_size or the
account's shipping preferences. Download links expire after about 15 minutes, so fetch a fresh
one each time you need it rather than storing it.
Shipment status
| status | Meaning |
|---|---|
paid | Label bought; the carrier has not scanned it yet. |
processing | The label is being created with the carrier. |
shipped | The carrier has the parcel (first scan) and it is moving. |
delivered | Delivered. |
exception | The carrier reported a problem: failed attempt, held, address issue or returned. |
cancelled | The label was voided. |
GET /v1/shipments/{id}/timeline shows everything that happened on ParcelHub (purchase, refunds, charges,
pickups) alongside the carrier's scans.
Void a label
An unused label can be voided and refunded to the wallet. Some carriers refund only once they confirm it wasn't used.
curl -s -X POST https://api.parcelhub.ca/v1/shipments/$ID/cancel -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"reason":"Order cancelled"}' Return labels
Makes a label from the original recipient back to you, charged to the wallet. Any field you send (from, to, packages, carrier…) replaces the original's. Quote it first with POST /v1/rates and the addresses swapped if you want to show the price.
# Same packages, recipient → sender. Send only what should differ.
curl -s https://api.parcelhub.ca/v1/shipments/$ID/return -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-H 'Idempotency-Key: return-1042' -d '{"reason": "wrong_item"}' Drafts: save now, buy later
Drafts hold a shipment without buying it. Orders imported from stores arrive as drafts too.
List them with GET /v1/shipments/drafts.
# 1. Save without buying
curl -s https://api.parcelhub.ca/v1/shipments/draft -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{ "from": { … }, "to": { … }, "packages": [ … ] }'
# 2. Later: quote, then record the chosen rate on the draft
curl -s -X PATCH https://api.parcelhub.ca/v1/shipments/draft/$DRAFT -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{
"selected_carrier": "…", "selected_service_code": "…", "selected_service_name": "…",
"selected_amount_cents": 1288, "selected_currency": "CAD", "quote_id": "…"
}'
# 3. Buy it (charges the wallet; the draft becomes a shipment)
curl -s -X POST https://api.parcelhub.ca/v1/shipments/draft/$DRAFT/process -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{}' Batches
Group purchased shipments (for example a day's orders) and print every label as one PDF.
curl -s https://api.parcelhub.ca/v1/batches -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"name":"Monday orders"}'
curl -s https://api.parcelhub.ca/v1/batches/$BATCH/shipments -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' -d '{"shipment_ids":["…","…"]}'
# One merged PDF of every label in the batch:
curl -s https://api.parcelhub.ca/v1/batches/$BATCH/labels -H "Authorization: Bearer $TOKEN" A batch holds purchased shipments only. Drafts tagged for a batch join it when their label is bought; see them with GET /v1/batches/{id}/drafts. Close a batch with PATCH {"status":"closed"}.