Load the truck in as many calls as you need, check the load, send it once, and check the parcels in when they arrive. The transfer closes by itself when the last parcel is accepted.
A parcel has to be recorded as sitting in the source depot before it can go on a truck. Skip this and the parcel comes back as needs_repair. The route also needs a configured driver, unless you name one of your drivers with driverId on the first add.
Same source, destination and type every time. Every call lands on the same open transfer ("reused": true), so a large truck is simply several calls. Leave dispatch out while you are still loading, and keep the transferId: every later step takes it.
{ "action": "add",
"sourceDepotId": 3, "destinationDepotId": 7,
"type": "delivery",
"orderIds": [1234, 1235, 1236] }add.reason says why; for wrong_destination_depot, correctDestinationDepot names the right depot.parcels[] is exactly what will leave. This is the last point where you can change the load.
{ "action": "status", "transferId": "4f1c8a02-…" }remove with their orderIds, then check again. Their leg is closed, so they can go on a later truck with a new add.cancel the transfer. Every parcel on it is released.Send the transferId. Every parcel on the transfer leaves together and goes inTransit, however many there are.
{ "action": "dispatch", "transferId": "4f1c8a02-…" }"dispatched": 0 and changes nothing.One per parcel, as it comes off the truck at the destination depot.
When the last parcel is accepted, the transfer becomes completed. If one stays in_progress for a long time, a parcel on it was never accepted; status shows which.
One flowchart per action, with each condition and the exact response it produces. The first failing check answers, and nothing after it runs.
flowchart TD
classDef ok fill:#E2F2E8,stroke:#2B7148,color:#173d27
classDef err fill:#F8E6E6,stroke:#A23A3A,color:#5c1b1b
classDef retry fill:#E3EDF8,stroke:#1D5FA8,color:#12345c
R(["POST /partner_transfer"]) --> H{"Headers present,<br/>api key valid,<br/>signature matches the raw body?"}
H -- "no headers" --> E1["401 Missing authentication headers"]:::err
H -- "unknown key" --> E2["401 Invalid api key"]:::err
H -- "bad signature" --> E3["401 Invalid signature"]:::err
H -- yes --> P{"Key enabled for<br/>partner routes?"}
P -- no --> E4["403 partner_scope_not_configured"]:::err
P -- yes --> J{"Body is a JSON object?"}
J -- no --> E5["400 Invalid payload"]:::err
J -- yes --> A{"action is add, dispatch,<br/>status, list, remove or cancel?"}
A -- no --> E6["400 Missing or invalid field: action"]:::err
A -- yes --> EM{"employeeId a positive integer,<br/>employeeName a string?<br/>(only checked when sent)"}
EM -- no --> E7["400 Invalid field: employeeId / employeeName"]:::err
EM -- yes --> RUN["Run the action<br/>(flowcharts below)"]
RUN --> OUT{"How it ended"}
OUT -- "done" --> OK["200 ok: true + the action's body"]:::ok
OUT -- "a field is wrong" --> V["400 · category request · the message names the field"]:::err
OUT -- "business rule" --> T["400 or 403 · error code + category + retryable"]:::err
OUT -- "another call is working on the same transfer" --> C["409 transfer_in_progress · retryable: true"]:::retry
OUT -- "unexpected" --> I["500 internal_error · quote x-request-id"]:::err
x-request-id header. Error bodies are { ok: false, error, category, retryable }, plus reason and orderIds where they apply. Unknown top-level fields are ignored, not rejected.add
flowchart TD
classDef ok fill:#E2F2E8,stroke:#2B7148,color:#173d27
classDef err fill:#F8E6E6,stroke:#A23A3A,color:#5c1b1b
classDef retry fill:#E3EDF8,stroke:#1D5FA8,color:#12345c
S(["add"]) --> V1{"sourceDepotId and<br/>destinationDepotId<br/>positive integers?"}
V1 -- no --> X1["400 Missing or invalid field"]:::err
V1 -- yes --> V2{"Same depot?"}
V2 -- yes --> X2["400 same_depot"]:::err
V2 -- no --> V3{"type is delivery or returned,<br/>orderIds 1 to 200 positive integers,<br/>dispatch and blockWrongDestination booleans,<br/>driverId a positive integer?"}
V3 -- no --> X3["400 Missing or invalid field"]:::err
V3 -- yes --> O1{"Any orderId belongs<br/>to another partner?"}
O1 -- yes --> X4["403 unauthorizedPartnerForRequest<br/>+ orderIds · nothing created or loaded"]:::err
O1 -- no --> L{"Route free?<br/>(no other add opening it)"}
L -- no --> X5["409 transfer_in_progress"]:::retry
L -- yes --> D{"Both depots are yours?"}
D -- no --> X6["400 depot_not_found"]:::err
D -- yes --> DR{"driverId sent?"}
DR -- yes --> DR2{"Driver exists and<br/>is one of yours?"}
DR2 -- no --> X7["400 driver_not_found"]:::err
DR2 -- yes --> EX
DR -- no --> EX{"A ready transfer already<br/>open on this route and type?"}
EX -- yes --> Z{"Everything on it<br/>already handled?"}
Z -- no --> RE["Reuse it · reused: true<br/>(its driver is kept)"]
Z -- yes --> SET["It closes;<br/>a new one is opened"]
EX -- no --> NEW
SET --> NEW{"Driver = driverId,<br/>else the route's configured driver"}
NEW -- none --> X8["400 no_route_driver · nothing created"]:::err
NEW -- found --> CR["Create transfer · status ready · reused: false"]
RE --> LOOP["Each orderId in order, one at a time<br/>(per-parcel chart below)"]
CR --> LOOP
LOOP -- "transfer dispatched or cancelled meanwhile" --> X9["400 transfer_closed<br/>(parcels already done stay loaded)"]:::err
LOOP -- "lock busy mid-batch" --> X10["409 transfer_in_progress<br/>(re-send the same call: done ones answer already_added)"]:::retry
LOOP --> DP{"dispatch: true?"}
DP -- yes --> DIS["Dispatch the whole transfer<br/>(dispatch chart)"]
DP -- no --> RESP["200 · transferId, status, reused,<br/>added, alreadyAdded, needsRepair, refused,<br/>wrongDestination, dispatched, parcels[]"]:::ok
DIS --> RESP
ready. That's why repeating add with the same route loads more parcels onto the same truck, and why a timed-out add is safe to repeat. add has no transferId field; the route picks the transfer.parcels[]
flowchart TD
classDef ok fill:#E2F2E8,stroke:#2B7148,color:#173d27
classDef err fill:#F8E6E6,stroke:#A23A3A,color:#5c1b1b
classDef warn fill:#FBF1DF,stroke:#9A6412,color:#4d320a
S(["one orderId"]) --> F1{"Order exists?"}
F1 -- no --> R1["refused · order_not_found"]:::err
F1 -- yes --> F2{"Yours?"}
F2 -- no --> R2["refused · unauthorizedPartnerForRequest"]:::err
F2 -- yes --> F3{"Already delivered<br/>or returned?"}
F3 -- yes --> R3["refused · already_delivered_or_returned"]:::err
F3 -- no --> F4{"type returned and<br/>an open QC ticket?"}
F4 -- yes --> R4["refused · unresolved_qc_ticket<br/>repairable: close the ticket, re-send"]:::warn
F4 -- no --> F5{"Out for final delivery?"}
F5 -- yes --> R5["refused · active_last_mile_mission"]:::err
F5 -- no --> F6{"Already on this transfer?"}
F6 -- yes --> A1["already_added"]:::ok
F6 -- no --> F7{"Heading the way this type goes?<br/>delivery: to the customer<br/>returned: to the merchant"}
F7 -- no --> R6["refused · wrong_final_destination"]:::err
F7 -- yes --> F8{"Zone routing sends it<br/>to another depot?"}
F8 -- "yes, blockWrongDestination true (default)" --> R7["refused · wrong_destination_depot<br/>+ correctDestinationDepot"]:::err
F8 -- "yes, blockWrongDestination false" --> F9
F8 -- "no, or no routing configured" --> F9{"On a last-mile runsheet<br/>still being loaded?"}
F9 -- yes --> R8["refused · active_last_mile_runsheet"]:::err
F9 -- no --> F10{"Record out of step?<br/>not_in_depot · wrong_depot<br/>unresolved_missions · active_ready_transfer"}
F10 -- "any of them" --> N1["needs_repair + repairs[]<br/>nothing loaded"]:::warn
F10 -- none --> A2["added · new leg, missionStatus ready<br/>+ warning wrong_destination_depot<br/>when loaded despite routing"]:::ok
error, category, retryable and a reason you can show the operator. A parcel still on another transfer that hasn't left, or with any other open leg, comes back needs_repair. Take it off the other transfer with remove, then add it again.dispatch
flowchart TD
classDef ok fill:#E2F2E8,stroke:#2B7148,color:#173d27
classDef err fill:#F8E6E6,stroke:#A23A3A,color:#5c1b1b
classDef retry fill:#E3EDF8,stroke:#1D5FA8,color:#12345c
S(["dispatch"]) --> V{"transferId is a uuid?"}
V -- no --> X1["400 Missing or invalid field: transferId"]:::err
V -- yes --> O{"orderIds in the body?"}
O -- yes --> X2["400 Invalid field: orderIds"]:::err
O -- no --> L{"Transfer free?"}
L -- no --> X3["409 transfer_in_progress"]:::retry
L -- "no such transfer" --> X4["400 transfer_not_found"]:::err
L -- yes --> OW{"Yours?"}
OW -- "another partner's" --> X5["403 unauthorizedPartnerForRequest"]:::err
OW -- yes --> C{"completed or cancelled?"}
C -- yes --> X6["400 transfer_closed"]:::err
C -- no --> DRV{"Transfer has a driver?"}
DRV -- no --> X7["400 no_driver_assigned"]:::err
DRV -- yes --> RT{"Destination depot<br/>still resolves?"}
RT -- no --> X8["400 destination_depot_missing"]:::err
RT -- yes --> RD{"Any legs still ready?"}
RD -- no --> Z["200 dispatched: 0<br/>(a repeat, or an empty truck)"]:::ok
RD -- yes --> MV["Every waiting parcel → inTransit,<br/>handed to the transfer's driver<br/>Transfer → in_progress<br/>Each parcel's history shows the pickup"]
MV --> OK["200 dispatched: n · orderIds · status in_progress"]:::ok
inTransit are left alone. Calling dispatch again on an in_progress transfer only sends legs that are still ready, which is dispatched: 0 after a normal dispatch.statuslist
flowchart TD
classDef ok fill:#E2F2E8,stroke:#2B7148,color:#173d27
classDef err fill:#F8E6E6,stroke:#A23A3A,color:#5c1b1b
classDef retry fill:#E3EDF8,stroke:#1D5FA8,color:#12345c
S(["status"]) --> V{"transferId is a uuid?"}
V -- no --> X1["400 Missing or invalid field: transferId"]:::err
V -- yes --> L{"Transfer free?"}
L -- no --> X2["409 transfer_in_progress"]:::retry
L -- "no such transfer" --> X3["400 transfer_not_found"]:::err
L -- yes --> OW{"Yours?"}
OW -- "another partner's" --> X4["403 unauthorizedPartnerForRequest"]:::err
OW -- yes --> RT{"Destination depot<br/>still resolves?"}
RT -- no --> X5["400 destination_depot_missing"]:::err
RT -- yes --> PR{"Transfer still ready and<br/>a leg on it moved on elsewhere?"}
PR -- yes --> P1["Drop those lines · prunedLines: n"]
PR -- no --> ST
P1 --> ST{"Every leg now closed?"}
ST -- yes --> DONE["Transfer → completed"]
ST -- no --> OK
DONE --> OK["200 · transfer header, prunedLines, parcels[]"]:::ok
status takes transferId only. It is the read that completes a transfer once its last parcel has been accepted at the destination. list never changes anything.
flowchart LR
classDef ok fill:#E2F2E8,stroke:#2B7148,color:#173d27
classDef err fill:#F8E6E6,stroke:#A23A3A,color:#5c1b1b
S(["list"]) --> V{"type delivery or returned?<br/>sourceDepotId, limit positive integers?<br/>status ready, in_progress,<br/>completed or cancelled?"}
V -- no --> X1["400 Missing or invalid field"]:::err
V -- yes --> OK["200 transfers[] newest first<br/>limit default 50, max 200<br/>each with parcelCount"]:::ok
list shows the stored status. A transfer whose parcels were all accepted can still show in_progress there until a status call on it completes it.removecancel
flowchart TD
classDef ok fill:#E2F2E8,stroke:#2B7148,color:#173d27
classDef err fill:#F8E6E6,stroke:#A23A3A,color:#5c1b1b
classDef retry fill:#E3EDF8,stroke:#1D5FA8,color:#12345c
S(["remove"]) --> V{"transferId a uuid and<br/>orderIds 1 to 200 positive integers?"}
V -- no --> X1["400 Missing or invalid field"]:::err
V -- yes --> L{"Transfer free?"}
L -- no --> X2["409 transfer_in_progress"]:::retry
L -- "no such transfer" --> X3["400 transfer_not_found"]:::err
L -- yes --> OW{"Yours?"}
OW -- "another partner's" --> X4["403 unauthorizedPartnerForRequest"]:::err
OW -- yes --> RD{"Transfer status ready?"}
RD -- no --> X5["400 transfer_closed<br/>(already left, completed or cancelled)"]:::err
RD -- yes --> OO{"Any orderId belongs<br/>to another partner?"}
OO -- yes --> X6["403 unauthorizedPartnerForRequest + orderIds"]:::err
OO -- no --> F{"Named parcels with a<br/>ready leg on this transfer?"}
F -- none --> Z["200 removed: 0"]:::ok
F -- some --> MV["Each parcel is taken off, its leg closed (fail)<br/>and the change recorded in its history"]
MV --> OK["200 removed: n · orderIds<br/>(they can be added to another transfer)"]:::ok
flowchart TD
classDef ok fill:#E2F2E8,stroke:#2B7148,color:#173d27
classDef err fill:#F8E6E6,stroke:#A23A3A,color:#5c1b1b
classDef retry fill:#E3EDF8,stroke:#1D5FA8,color:#12345c
S(["cancel"]) --> V{"transferId is a uuid?"}
V -- no --> X1["400 Missing or invalid field: transferId"]:::err
V -- yes --> L{"Transfer free?"}
L -- no --> X2["409 transfer_in_progress"]:::retry
L -- "no such transfer" --> X3["400 transfer_not_found"]:::err
L -- yes --> OW{"Yours?"}
OW -- "another partner's" --> X4["403 unauthorizedPartnerForRequest"]:::err
OW -- yes --> AC{"Already cancelled?"}
AC -- yes --> Z["200 released: 0"]:::ok
AC -- no --> RD{"Status ready?"}
RD -- no --> X5["400 transfer_closed"]:::err
RD -- yes --> MV["Each waiting parcel is released, its leg closed (fail)<br/>Transfer → cancelled"]
MV --> OK["200 status cancelled · released: n"]:::ok
stateDiagram-v2 direction LR [*] --> ready: first add on a route ready --> ready: add · remove ready --> in_progress: dispatch, or add with dispatch true ready --> cancelled: cancel ready --> completed: every leg closed elsewhere, seen by add or status in_progress --> completed: last parcel accepted at destination, seen by status completed --> [*] cancelled --> [*]
ready accepts remove and cancel; add and dispatch are refused once it is completed or cancelled.stateDiagram-v2 direction LR [*] --> ready: add ready --> inTransit: dispatch ready --> fail: remove · cancel inTransit --> success: partner_accept_in_depot at the destination success --> [*] fail --> [*]
missionStatus in parcels[]). A parcel whose leg failed has no open leg left, so it can be added to another transfer.| HTTP | error | category | retry | Raised by | What to do |
|---|---|---|---|---|---|
| 400 | Missing or invalid field: … | request | no | any action | The message names the field. Fix the request. |
| 400 | same_depot | request | no | add | Source and destination must differ. |
| 400 | depot_not_found | request | no | add | A depot id isn't one of yours. Check your mapping. |
| 400 | driver_not_found | request | no | add | The driverId doesn't exist or isn't yours. Omit it to use the route driver. |
| 400 | no_route_driver | configuration | no | add | No driver configured for the route, and none named. Nothing was created. |
| 400 | no_driver_assigned | configuration | no | dispatch | The transfer has no driver. Contact us. |
| 400 | transfer_not_found | request | no | dispatch, status, remove, cancel, add | Check the transferId. |
| 400 | transfer_closed | terminal | no | add, dispatch, remove, cancel | Already left, completed or cancelled. Start a new transfer. |
| 400 | destination_depot_missing | configuration | no | dispatch, status | Misconfigured on our side. Contact us with the x-request-id. |
| 401 | Missing authentication headers · Invalid api key · Invalid signature | — | no | any action | Check the headers and sign the exact bytes you send. |
| 403 | partner_scope_not_configured | — | no | any action | Your key isn't enabled for partner routes. Contact us. |
| 403 | unauthorizedPartnerForRequest | auth | no | add, remove (orders) · dispatch, status, remove, cancel (transfer) | Another partner's transfer or orders. reason says which; drop the foreign orderIds. |
| 409 | transfer_in_progress | conflict | yes | add, dispatch, status, remove, cancel | Another call holds the transfer or the route. Retry after a short pause. |
| 500 | internal_error | configuration | no | any action | Retry once, then contact us with the x-request-id. |
| status | error / repairs | category | retry | What to do |
|---|---|---|---|---|
added | —, or warning: wrong_destination_depot | — | — | On the truck. With the warning, it was loaded although routing sends it to correctDestinationDepot. |
already_added | — | — | — | Already on this truck. Success. |
refused | order_not_found | terminal | no | Check the id. |
refused | unauthorizedPartnerForRequest | terminal | no | Not your order. Normally the whole call is a 403 before this. |
refused | already_delivered_or_returned | terminal | no | The journey is finished. |
refused | unresolved_qc_ticket | repairable | yes | Returns only. Close the QC ticket, then send it again. |
refused | active_last_mile_mission | terminal | no | Out for delivery. Recall that mission first. |
refused | wrong_final_destination | terminal | no | Use the other type. |
refused | wrong_destination_depot | terminal | no | Put it on a transfer to correctDestinationDepot, or send blockWrongDestination: false. |
refused | active_last_mile_runsheet | terminal | no | Take it off that last-mile runsheet first. |
needs_repair | not_in_depot | — | yes | Check it in at the source depot with partner_accept_in_depot. |
needs_repair | wrong_depot | — | yes | It's recorded at another depot. Check it in at this source depot. |
needs_repair | unresolved_missions | — | yes | An older leg is still open. Close it (for example with remove on the other transfer). |
needs_repair | active_ready_transfer | — | yes | It's on another transfer that hasn't left. remove it there first. |
ok: true with added: 0ok describes the call, not the parcels. Always read added, refused and parcels[].
already_added, dispatched: 0, released: 0A repeated call. The first one landed, and nothing changed.
409 transfer_in_progressAnother call is working on the same transfer. Retry after a short pause. This is the only failure worth retrying.