Skip to content
Last updated

Common Workflows

End-to-end flows that chain V5 endpoints together. Each workflow lists the exact endpoint sequence and what to do with the response between steps.


Courier shipment lifecycle

Create, rate, label, and ship a single courier package. The most common V5 flow — covers ~80% of carrier integrations.

#StepEndpointWhat you do
1Create the shipmentPOST /shipments with type: courierSend to_address + packages. Returns data.id (the V5 shipment_id).
2List ratesGET /rates/{shipment_id}Pick a carrier.service_code (e.g. canada_post.expedited).
3Buy the labelPOST /labels/{shipment_id} with the chosen serviceCharges credits. Returns data.label_url (PDF) + data.tracking_code.
4Schedule a pickup (optional)POST /pickups with { shipment_ids: [...] }Only valid for courier shipments (FedEx/UPS/Purolator via Freightcom).

Use an Idempotency-Key header on steps 1, 3, and 4 so network retries can't double-charge or double-book.


LTL shipment booking

Less-than-truckload freight has its own endpoints, all under the ltl:read / ltl:write scopes. Quoting is stateless: the booking call repeats the quote body and adds the chosen service.

#StepEndpointWhat you do
1Get LTL quotesPOST /ltl/quoteSend addresses, pallets, accessorials, pickup_date and the dock hours in pickup (ready_at / close_at, local HH:MM). Pick a service.
2Book with the carrierPOST /ltl/shipments with the same body plus serviceCharges credits. Returns data.id, bol_number, pickup_confirmation_number, a signed bol_url, and pro_number if the carrier assigned one already. Add pickup.instructions for the driver (dock number, gate code).
3Follow the shipmentGET /ltl/shipments/{shipment_id}status moves booked → in_transit → delivered (or exception). pro_number appears once the carrier assigns it; charges.adjustments lists reweighs, reclasses and accessorials invoiced after pickup. Fetch again for a fresh bol_url: each link expires after 30 minutes.
4Cancel a booking (optional)DELETE /ltl/shipments/{shipment_id}Voids the booking with the carrier while it is still booked.

Carrier adjustments also appear on GET /billing/transactions (needs billing:read): match them to the shipment with shipment_id and read the cause from adjustment.reason.

Use an Idempotency-Key header on step 2 so a network retry can't book twice.


Batch processing

Group many shipments into a single batch, generate labels for all of them, and drain per-shipment failures.

#StepEndpointWhat you do
1Create the batchPOST /batchesSend { name, shipment_ids }. Returns data.id.
2Trigger processingPOST /batches/{batch_id}/processReturns 202. Label generation runs async.
3Poll statusGET /batches/{batch_id}Repeat until data.status != "processing". Realistic clients back off (1s → 2s → 4s).
4Drain errorsGET /batches/{batch_id}/errorsPer-shipment failures with the validation problem.

Top up credits, then buy a label

Labels are paid from the account's credit balance. If the balance is low, top up first.

#StepEndpointWhat you do
1Charge the payment methodPOST /credits/top-up with { amount, payment_method_id? }payment_method_id is optional — defaults to the account's default card. Returns data.id (the new transaction).
2Confirm the new balanceGET /credits/balanceSanity-check that the top-up landed and covers the upcoming label cost.
3Buy the labelPOST /labels/{shipment_id} with the chosen serviceDebits the credit balance.

Always send an Idempotency-Key header on step 1. Top-up retries without a key double-charge.


Classify and approve a product

Customs declarations need a per-product HS code. V5 wraps the TRU classification API so you can ask for a suggested code, optionally verify the manufacturer, then persist the product.

#StepEndpointWhat you do
1Suggest an HS codePOST /products/classify with { title, description }Returns data.hs_code.
2Verify the manufacturer (optional)POST /products/verify-manufacturer with { manufacturer, country_of_origin }CUSMA / country-of-origin compliance check.
3Create the productPOST /products with the verified hs_code and source fieldsPersists the row that store imports and later shipment-creation calls reference.

If an existing product enters Manufacturer Failed after import, you can run just step 2 on it and re-call POST /products/{sku}/approve-classification.


Compare retail and enterprise tier rates (opt-in)

Some reseller and partner integrations need to show what a shipment would cost at Stallion's standard retail (small business) and enterprise published margins, alongside the account's own quoted rate.

#StepWhat you do
1Enable the account settingAdmin enables Show Retail & Enterprise Rates via API (show_tier_rates_api). Uses standard published margins — ignores the account's custom pricing rules.
2Quote ratesGET /rates/{shipment_id} or POST /rates as usual.
3Read tier_pricingWhen enabled, each rate object may include a tier_pricing object with retail and enterprise sub-objects.

Each tier entry has the same fields as the primary rate (service, subtotal, tax, total, currency, add_ons, etc.), so clients can display them without a separate parser. When the setting is off or the rate cannot be re-margined (no captured postage cost), the tier_pricing key is omitted.

The same opt-in field is available on v4 get-rates responses (rates[].tier_pricing).

Constraints

  • Tier totals are derived from the already-fetched carrier cost — no extra carrier API calls.
  • Stallion Protection add-ons are recomputed per tier because insurance pricing depends on the rate total.
  • Booking always uses the account's primary quoted rate, not a tier comparison.

Tell a carrier timeout apart from lane non-coverage

A service can be missing from a rates response for two very different reasons: the carrier didn't answer in time (transient — retry and it may come back), or the carrier answered and genuinely doesn't serve the lane. Every rates response (GET /rates/{shipment_id}, POST /rates, POST /rates/estimate) carries a meta object that makes the difference explicit:

{
  "data": [ ...rates... ],
  "meta": {
    "timeout_seconds": 7,
    "excluded_services": [
      {
        "postage_type_id": 141,
        "service": "intelcom.standard",
        "carrier": "Intelcom",
        "service_name": "Intelcom Standard",
        "reason": "carrier_timeout",
        "message": "The carrier did not respond within the 7-second rate timeout. ..."
      }
    ]
  }
}
#StepWhat you do
1Quote ratesAny rates endpoint. Optionally pass timeout (1–25 seconds; body field on the POST endpoints, query param on the GET) to override the default rate-fetch timeout.
2Read meta.excluded_servicesEach entry names an omitted service and a machine-readable reason (see the ExcludedService schema for the full enum).
3Branch on reasoncarrier_timeout / carrier_error → transient; retry (optionally with a higher timeout) before concluding anything about the lane. no_rate_for_lane → the carrier answered and offered nothing; safe to treat as non-coverage for this shipment.

Choosing a timeout

  • A higher value (e.g. 15–25) trades latency for completeness — fewer carrier_timeout omissions, useful for batch quoting or building a lane-coverage dataset.
  • A lower value (e.g. 3–5) favors a fast checkout: slow carriers are dropped and reported in meta.excluded_services instead of holding up the response.
  • The cap is 25 seconds so the HTTP request itself can still complete; omitting the parameter uses the platform default (meta.timeout_seconds always tells you what was applied).

Other endpoint-level flows

These don't need a multi-step description — they're single calls or small glue logic.

  • Quote rates without creating a shipment — POST /rates with shipment details. Returns rates. No shipment row is created.
  • Validate addresses — POST /addresses/validate before POST /shipments to surface bad zip/postal codes early.
  • Manage imported orders — POST /orders, GET /orders, PUT /orders/{order_id}. Delete only before linking to a non-voided shipment.
  • Find drop-off locations — GET /locations. Render the returned branch and partner-site addresses; refresh periodically because the catalog changes.

Idempotency & retry rules

All mutating endpoints (POST, PUT, DELETE) accept an Idempotency-Key header. A retried request with the same key inside 24h returns the original response byte-for-byte. Reusing a key with a different body yields 409 idempotency_conflict. See Errors & Idempotency.