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_cents is the price before tax. published_cents, when present, is the carrier's own list price for the same service.
  • pickup_supported: false means the carrier won't collect this parcel: it must be dropped off.
  • tracking_available: false marks a service with no tracking.
  • carriers_with_no_options lists carriers that returned nothing for this parcel, with a reason.
  • ship_date can be up to 7 days ahead (leave it out for today). Use the same date when you buy.
  • signature_option is none, required or adult; the quote includes the carrier's charge for it.
  • Set is_residential: true for 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 the carrier values 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"
}'
Always send an 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

statusMeaning
paidLabel bought; the carrier has not scanned it yet.
processingThe label is being created with the carrier.
shippedThe carrier has the parcel (first scan) and it is moving.
deliveredDelivered.
exceptionThe carrier reported a problem: failed attempt, held, address issue or returned.
cancelledThe 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"}.