Skip to main content

Hub Shipments (Hub Transit)

Route a shipment through one of Stream's consolidation hubs instead of shipping directly from origin to destination.

What is hub transit?​

By default a quote ships directly from origin to destination. With hub transit, Stream routes the shipment through an intermediate hub and splits it into two legs:

  • Clearance leg — the international leg from your origin into the hub region. This is the leg that clears customs.
  • Local leg — the domestic / final-mile leg from the hub on to the destination.

You never choose the hub yourself. When you ask for hub transit, Stream selects an eligible hub for your origin → destination lane and builds both legs for you. Hub routing can unlock better rates and consolidated handling on lanes where it's supported.

Requesting a hub quote​

Set hub_transit: true in the Create Quote request body. Everything else is identical to a direct quote.

{
"hub_transit": true,
"origin_address": {
"name": "John Doe",
"company": "Tech Corp",
"address_1": "123 Tech Street",
"city": "Johannesburg",
"province": "Gauteng",
"postal_code": "2196",
"country_id": 226,
"email": "john@techcorp.com",
"phone": "+27123456789",
"classification": "business"
},
"destination_address": {
"name": "Jane Smith",
"company": "UK Business",
"address_1": "456 London Road",
"city": "London",
"province": "London",
"postal_code": "SW1A 2AA",
"country_id": 70,
"email": "jane@ukbusiness.com",
"phone": "+442012345678",
"classification": "business"
},
"purpose_of_shipment_id": 8,
"value_of_goods": "600.00",
"value_of_goods_currency_id": "ZAR",
"mass_unit": "kg",
"distance_unit": "cm",
"packages": [
{
"weight": 5.5,
"width": 30.0,
"height": 20.0,
"length": 40.0
}
]
}

If no hub serves the route, the request fails with 422 Unprocessable Entity:

{
"success": false,
"error": "[\"Hub transit No hub is available for this route.\"]",
"errors": []
}

When that happens, fall back to a direct quote (omit hub_transit, or send false).

What a hub quote returns​

A hub quote collapses the two legs into a single estimate per service, so you compare hub options exactly like direct ones — you don't see or pick legs individually.

  • hub_transit on the quote is true.
  • Each entry in estimates is anchored on the clearance leg: estimate_id is the clearance-leg estimate id (this is the id you pass when creating the order).
  • line_items are the combined costs of both legs, grouped by description and summed in your account currency — so the price you see is the all-in hub price, not just one leg.
{
"quote": {
"id": 1234,
"dirty": false,
"hub_transit": true,
"errors": [],
"estimates": [
{
"estimate_id": 55788,
"shipping_company": "FedEx",
"shipping_service": "Express Priority",
"quote_expired": false,
"line_items": [
{ "description": "Base Rate", "cost": 1450.00, "currency_id": "ZAR" },
{ "description": "Fuel Surcharge", "cost": 217.50, "currency_id": "ZAR" }
],
"billable_weights": { "...": "same breakdown as a standard estimate" }
}
]
}
}

Each cost above is the sum across both legs (clearance + local) for that line item — a hub estimate is one combined price, not two.

Placing a hub order​

Create the order exactly as you would for a direct quote — pass the hub estimate's estimate_id as quote_estimate_id to Create Order. There is nothing hub-specific to send. The resulting order spans both hub legs.

The hub lane is fixed at quote time​

The route — origin → hub → destination — is locked when the hub quote is created. The origin_address and destination_address you send when placing the order do not re-route a hub order: they are ignored for routing (only origin_address.export_code is read, for the export licence). This keeps the order on the exact lane the rate was quoted for.

To ship a different origin or destination, request a new hub quote (hub_transit: true) for that lane and order from it — you can't re-point an existing hub order at order time.

Waybills & documents for both legs​

A hub order has two legs, each with its own waybill. Generating waybills (Create Waybill) produces one for both legs.

To retrieve them, read the order's shipments array, returned by Create Order, Get Orders and Create Waybill.

The array is ordered by route: the first entry is the leg leaving your origin (origin → hub), and the last is the onward leg into your destination (hub → destination). A shipment carries no leg-role field, so use this ordering to tell the legs apart — shipments[0] is always the first leg of the journey, shipments[1] the next. Each entry carries that leg's own:

  • waybill — the waybill / tracking number
  • waybill_document_link — the waybill PDF for that leg
  • tracking_link and status
"shipments": [
{ "shipping_service": "...", "waybill": "WB-ORIGIN-TO-HUB", "waybill_document_link": "https://.../leg-1-waybill.pdf", "tracking_link": "...", "status": "..." },
{ "shipping_service": "...", "waybill": "WB-HUB-TO-DESTINATION", "waybill_document_link": "https://.../leg-2-waybill.pdf", "tracking_link": "...", "status": "..." }
]

On a hub order the order's top-level waybill, waybill_document_link, tracking_link and status describe a single leg, so always read those per leg from shipments[], in route order, to get both waybills.

Documents​

Documents you upload with Create Order attach once, to the leg that clears customs. On EU hub lanes the local leg is domestic or intra-EU and needs no commercial invoice, so one upload covers the order.

Pickups​

Both pickup endpoints (Schedule Pickup) work on the first leg, the one leaving your origin. There is nothing for you to schedule on the hub → destination leg.

Cancelling​

Cancel Order cancels both legs together.

End-to-end flow​

  1. Quote — POST /quotes with hub_transit: true → a hub quote whose estimates are collapsed and clearance-anchored.
  2. Choose — pick an estimate and read its estimate_id.
  3. Order — POST /orders with that quote_estimate_id → a two-leg hub order; both legs' estimates are accepted automatically.
  4. Waybills — POST /orders/{id}/waybills readies both legs; read each leg's waybill from shipments[].
  5. Pickup — GET /orders/{id}/pickup_availabilities then POST /orders/{id}/schedule_pickups, both for the first leg.
  6. Track — poll GET /orders/{id} and read shipments[], or register a webhook; each leg fires its own events with the same order id.

Important notes​

  • Hub selection is automatic — you request hub transit; Stream picks the hub. You can't specify one.
  • One estimate, both legs — a hub estimate's estimate_id and line_items represent the clearance leg with the local leg folded in. You never accept the local leg yourself.
  • Availability is lane-dependent — not every origin → destination has a serviceable hub; handle the 422 "No hub is available for this route." response.
  • Same order endpoint — hub vs direct is decided entirely at quote time. Ordering is identical; you just use a hub estimate's id.