Relay — returns & shipping coordination
Relay is Last Mile Solutions' returns & shipping coordination network. Quote a return, ingest the order with its marketplace context, hand the customer a print-at-home label or a QR code, and follow the parcel through possession, sort, carrier injection, and delivery — over one unified, versioned API.
Returns are the primary flow. Service is configured per market. Money is integer cents; timestamps are ISO-8601 UTC; every POST accepts an Idempotency-Key. A request body that is absent, malformed, or not a JSON object is 422 validation_error with details.field: "body" — a client-side mistake is never reported as a 5xx.
Your key may be scoped. Most keys carry * and reach everything below. A narrower key is confined to grants of the form <resource>:<action> — the resource is the first path segment after /v2/ (locations, shipments, rate-cards; the /v2/partners/… family is all partners), the action is read for GET and write for everything else. <resource>:write grants <resource>:read as well, not the reverse. An endpoint outside your grants answers 403 insufficient_scope, naming the scope you would have needed in details.required_scope — a different refusal from 403 forbidden, which means your source address is outside the allowlist pinned to your key. GET /v2/ and GET /v2/health answer on any key regardless of scope.
You see your own endpoints, not everyone's. Most of this reference is the shared API and is the same for every partner. A few endpoints under /v2/partners/… are written around one partner's own payload dialect, and each is listed only for the partner it belongs to — so this page never says who else we onboard, or what their requests look like. If yours is missing, open the reference from the link in your welcome email, or sign in on /docs/flow with your API key.
Three programs, one API
Relay runs three drop-off programs across the Package Hub national mail-store network. They differ in who owns the parcel and where it is headed — not in the calls you make. One credential, one base path, one set of objects serves all three.
| Program | direction | What happens |
|---|---|---|
| Returns | return | A consumer drops a return at a Package Hub store and Relay carries it back to the shipper. This is the primary flow, and the one the rest of this reference uses for its examples. |
| Platform drop-off | outbound | A seller on an online platform uses Package Hub as the first mile for an outbound customer shipment. Same ingest, same lifecycle, opposite direction of travel. |
| Retail | outbound | A walk-in customer presents a package at the counter: price it with GET /service-options, ingest it with POST /shipments, and take possession with POST /scan-events/accept. The store collects payment; Relay records the shipment. |
Nothing about the program is baked into the credential or the path. Set direction per shipment, and use the free-form metadata object to carry your own program, channel, or campaign tags — Relay echoes it back verbatim on every read.
The five carrier scenarios
Every shipment resolves to a service level, and each service level names exactly one operating scenario: who selects the final-mile carrier, what class of carrier that is, and how the parcel leaves the LMS facility. Four metadata fields — carrier_selection, carrier_class, handoff, scenario — travel with every rate, service option, rate card, and shipment, so you never have to infer the model from the service-level name.
| Scenario | service_level | carrier_selection | carrier_class | handoff |
|---|---|---|---|---|
| 1a | reach_alternative | lms | alternative | inject |
| 1b | reach_coverage | lms | coverage | inject |
| 1c | core | lms | core | inject |
| 2 | client_carrier_inject | client | client_selected | inject |
| 3 | client_carrier_collect | client | client_selected | collect |
- 1a · reach_alternative — LMS sweeps, sorts, and injects to an alternative-network carrier. The most aggressive rates, with partial ZIP coverage.
- 1b · reach_coverage — the same flow to a coverage carrier with 100% US ZIP coverage.
- 1c · core — the same flow into the LMS Core network, for regional, ZIP-defined lanes.
- 2 · client_carrier_inject — you select the carrier; LMS picks up, sorts, and injects at that carrier's facility.
- 3 · client_carrier_collect — you select the carrier; LMS picks up, sorts, and makes the parcels ready, and the carrier collects them at the LMS facility.
Each launched market runs exactly one of these — GET /markets tells you which, and whether that market's carrier is chosen by LMS or by the marketplace. GET /service-options prices every scenario for a lane in a single call, including the local same-day drop-off cutoff.
Service-level rename
Responses always emit the current IDs below. The prior ID is echoed alongside them as legacy_service_level for one release, and legacy IDs remain accepted on input anywhere you send a service_level — rate-card and transit-time filters, and inline shipment creation — so nothing in flight breaks.
| Legacy ID | Current ID |
|---|---|
| relay_select_saver | reach_alternative |
| relay_select_national | reach_coverage |
| relay_core | core |
| pickup_sort_inject | client_carrier_inject |
| pickup_sort_collect | client_carrier_collect |
The parcel lifecycle
Relay reports a parcel's whole journey, not just its endpoints. Every step below is a shipment status, a webhook event of the same name, and a tracking event on GET /tracking/{tracking}.
created→label_ready→ready_for_pickup→picked_up→at_sort_center→in_possession→sorted→injected|made_ready→in_transit→out_for_delivery→delivered
undeliverable|unclaimed→return_to_sender|disposed|out_for_delivery
cancelledexpiredrejectedexception
| Status | What it means |
|---|---|
| created | Ingested and priced. Push-ahead orders carry an expires_at and auto-expire if never possessed. |
| label_ready | The printable label is minted, or the QR is staged with Package Hub. |
| ready_for_pickup | Scanned at a Package Hub counter — either a pre-applied label accepted at the counter, or a QR scanned, printed, and applied on site. |
| picked_up | Collected from the store by an LMS driver. |
| at_sort_center | Arrived at the LMS sort facility. Stamps milestones.sortScanned. |
| in_possession | Possession scan — at the store for a QR walk-in, or at the sort facility on the standard ladder. Stamps milestones.possessionScanned. |
| sorted | Sorted and segmented to the carrier staging area. |
| injected | Handed into the receiving carrier's network (handoff: inject). |
| made_ready | Staged and signed for at the LMS facility for carrier collection (handoff: collect). |
| in_transit | Moving with the carrier. |
| out_for_delivery | On the delivery vehicle. Stamps milestones.driverScanned. |
| delivered | Delivered. Stamps milestones.deliveryScanned and publishes pod / podGrade. |
| rejected | Refused at the counter, with a reason code from the closed reject set. |
| expired | A push-ahead label or QR was never presented and its expires_at window elapsed. Stamps expired_at and emits shipment.expired. Terminal. |
| undeliverable | Delivery failed and the parcel is off the delivery leg, awaiting its onward instruction. |
| return_to_sender | The return leg has started — the parcel is on its way back to the sender. Terminal. |
| unclaimed | Nobody collected it. Stamps unclaimed_since, and an advisory dispose_after 30 days later. |
| disposed | Destroyed or liquidated. Terminal. |
held is a flag, not a status: POST /shipments/{id}/hold and /release pause and resume a pre-injection parcel without moving it off the ladder.
The last four are the sad paths off the ladder, and they are reached by one call — POST /shipments/{id}/disposition, carrying rts, unclaimed, dispose, or redeliver plus a reason. A redeliver rejoins the ladder at out_for_delivery and completes normally; rts takes two calls, because a return leg cannot begin while the parcel is still out with the delivery carrier. See the endpoint for the full rule.
Drop-off, tender, and acceptance
- Quote and ingest. Price the lane with
GET /service-optionsorPOST /rates, then ingest withPOST /shipments—fulfillment: print_labelfor a print-at-home label, orqr_codefor a code the customer shows at the counter. - Amend before drop-off.
PATCH /shipments/{id}re-rates a parcel that hasn't been scanned yet; a price or routing change mints a fresh label and supersedes the old one.POST /shipments/{id}/label/reprintandPOST /shipments/{id}/qr/reissuecover a lost printout or a lost code. Once the parcel is scanned at a store, the shipment is locked. - At the counter. A QR is presented to
POST /scan-events/walk-in, which validates it, produces the label on site, and takes possession. A pre-applied label is accepted withPOST /scan-events/accept. A package the store cannot take is refused withPOST /scan-events/reject. If the weight or dimensions differ from what was declared,POST /discrepanciesrecords declared-vs-observed up front and links the resulting price adjustment. - Collection and sort. The driver collects the store's parcels; the day's drop-offs close onto a manifest (
GET /manifests,POST /manifests/{id}/close). The parcels arrive at the sort facility, are possession-scanned, and are sorted to the carrier staging area. - Handoff.
POST /tendersstages a batch of parcels at a facility for a named receiving carrier.POST /tenders/{id}/acceptis that carrier's proof of receipt — who accepted, when, and how many pieces — and advances every member shipment toinjectedormade_ready. A piece-count mismatch records a short/over discrepancy on the tender for reconciliation without blocking the handoff. - Delivery. The parcel moves to
in_transit,out_for_delivery, anddelivered. Proof of delivery is atGET /shipments/{id}/pod;GET /tracking/{tracking}is the customer-facing view and needs only an API key. Reweigh adjustments are disputable, and lost, damaged, or misdelivered parcels are claimable.
In sandbox, POST /test/shipments/{id}/advance walks a shipment one step down this ladder at a time so you can exercise every status and every webhook without waiting on a real parcel. POST /test/shipments/{id}/rewind is its inverse — it puts the shipment back to a rung you name so you can run the same steps again, and emits nothing while it does, so what your endpoints see is the ladder replayed rather than a parcel travelling backwards.