Deliveries
19 API calls in this section.
Endpoints for managing deliveries. These APIs provide functionality to create, retrieve, update, and delete deliveries, both individually and in bulk. Driver-action HTTP response messages are localized plain-language sentences. For the picked_up to in_transit move, the English response message is The package is now on the way to its destination; clients must display the returned message and must not build visible copy from raw status values. A P&D parent exposes its open exception through the existing has_exception and exception fields. A re-staged failed P&D outbound leg resolved as Return to Sender is returned as machine status failed; it is read-only and the parent continues through its separate return lifecycle. Starting or progressing its outbound child returns HTTP 409 {success:false,code:'parent_exception_open',message:'Resolve the parent order exception before starting this delivery'}; cancellation remains available.
List Dispatcher Contacts
/delivery/dispatcher-contactsReturns the active dispatchers the authenticated driver may contact.
Authentication and business rules
Driver JWT authentication is required.
No delivery/order ID is required for this list.
The driver and dispatchers must belong to the authenticated tenant.
Only active users with the exact
dispatcherrole are returned.When both the driver and dispatcher have explicit team mappings, they must share at least one team. Existing all-team behavior is preserved: no driver mapping means the driver may see all dispatchers; no dispatcher mapping means that dispatcher is available to all teams.
Team IDs and names are authorization-only and are never returned.
Success (HTTP 200)
returns success and dispatchers. Each dispatcher contains only user_id, full_name, avatar_url, and phone. An empty eligible set returns HTTP 200 with dispatchers: [].
Important errors
HTTP 403 when the authenticated user is not a driver or the driver record is unavailable. The response
messageis localized; English for a non-driver isOnly drivers can perform this action.
Frontend integration contract
Load this endpoint when the driver opens the dispatcher contact picker. If one result is returned, the UI may use it directly; if multiple are returned, let the driver choose. Open tel:<phone> for calls. Disable or hide Call when phone is null. Show No dispatcher available for an empty array. To message a dispatcher from inside an order, call POST /messaging/dispatcher-conversations with the current delivery_id and selected dispatcher_user_id. Display the returned plain-language message.
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/dispatcher-contacts" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "dispatchers": [4 {5 "user_id": "dispatcher-user-uuid",6 "full_name": "Shivan Ahmed",7 "avatar_url": "https://example.com/avatar.png",8 "phone": "+9647500000000"9 }10 ]11}List Deliveries
/delivery/deliveriesGet a list of deliveries with filtering and pagination. Access is restricted by role.
Search
searchmatches the delivery search fields,barcode, andexternal_reference_id.Delivery order codes match with or without the
DELprefix. Examples:DEL1911,DEL19,1911, and19can all matchdelivery_code1911.A non-empty
searchreturns matching orders in ANY status: the defaultdraft/deliveredexclusions are skipped and anystatusorcolumn_filters.statusvalue is ignored for that request. Clearingsearchrestores normal status filtering.
Status filtering
When no
search, explicit delivery-status selector, return selector, or assignment-status filter is sent, the list keeps the existing default behavior and excludesdraftanddeliveredrows.A non-empty standalone
assignment_statusorcolumn_filters.assignment_statusskips only those default exclusions, allowing matching assignments on terminal deliveries to remain visible. It does not override explicitstatus,column_filters.status, oris_completedconstraints. Both assignment-filter forms are exact matches and are AND-combined when supplied together.Frontend integration: send the selected assignment state as
column_filters={"assignment_status":"<status>"}(preferred for the grid) or standaloneassignment_status=<status>; omit it when clearing the filter. No response fields or navigation behavior change.When
searchis non-empty, status filtering is bypassed and matching orders in any status (pending,in_transit,returning,delivered,cancelled,draft, etc.) are returned.When
column_filters.statusis a real status value (and no search), the list applies exact delivery-status matching.returningis stored on the canonical original/P&D parent only after its linkedoperational_returnreachespicked_uporin_transit. Pending, Assigned, and En route to pickup return work leaves the parent Staged at warehouse. The failed outbound child remains Failed.When
column_filters.statusis the literal stringAll(and no search), the list applies no delivery-status equality filter and returns every status, includingdraft,delivered,cancelled, and other statuses. Pagination, sorting, search, and all other filters still apply.
Zones
Each row includes pickup_zone_id and dropoff_zone_id (from deliveries, exposed on delivery_list_view). Query zone_ids matches if either column is in the list (same as driver zone scoping).
Carrier fields
carrier_type, direction,pickup_warehouse_id,dropoff_warehouse_id,sequence_plan(hybrid only).
Parent assignment projection
For staged/P&D parent rows, the existing
assignmentobject is built from child delivery assignments instead of a parent assignment row.assignment.source = "child_rollup"identifies this response-only projection.assignment.delivery_id,assignment.delivery_code,assignment.direction, andassignment.leg_sequenceidentify the active/source child leg.assignment.status_updatesis a merged child-assignment timeline. Each event includessource = "child_assignment",assignment_id,delivery_id,delivery_code,direction, andleg_sequence.Cancelled/returned child assignments remain visible for history/audit, so terminal staged parents still expose the source child driver instead of
Not Assigned.Frontend integration: keep reading
delivery.driver,delivery.assignment.status, anddelivery.assignment.status_updates; no separate parent-specific field is required.
Exceptions
Each delivery row includes
has_exceptionandexception, plus compatibility fieldshas_open_exceptionandopen_exception.has_exception=truemeansexceptioncontains the compact snapshot of the latest opendelivery_exceptionsrow.column_filters.has_open_exceptionaccepts booleantrue/falseor the strings"true"/"false".truereturns only orders with a current open exception,falsereturns only orders without one, and omitting the key returns both. The filter is applied before pagination and counting.exceptionincludesid,exception_type,exception_label,option,status,reported_at, andupdated_at.optionis the tenant/system exception option snapshot matched byexception_type; use it for list rendering when present.Frontend integration: continue sending
column_filters={"has_open_exception":"true"}for the Has Exception selection and omithas_open_exceptionwhen the filter is cleared. Show the exception indicator only whenhas_exceptionis true. Useexception.exception_labelorexception.option.labelfor the list label. Fetch full details fromGET /delivery/exceptions?delivery_id=<delivery_id>&status=openonly when the user opens the exception context.
Query parameters
| Parameter | Type | Description |
|---|---|---|
page | string | Page number for pagination Example: |
limit | string | Number of items per page Example: |
status | string | Filter by delivery status. |
search | string | Case-insensitive partial search across delivery search fields, |
customer_id | string | Filter by customer ID |
driver_id | string | Filter by driver ID |
assignment_statusOptional | string | Optional effective assignment-status filter. Direct deliveries use their own current assignment. Parent/P&D orders use the same child-leg assignment projected in the returned row. Use |
bulk_group | string | Filter by bulk group identifier |
delivery_type | string | Filter by delivery type (Delivery, Pickup, OnDemand, etc.) |
tenant_idOptional | string | Tenant ID (optional if in auth token) |
column_filtersOptional | string | Optional JSON string for column-level filters. Keys are AND-combined and ANDed with existing standalone filters. Supported keys: |
column_filters.is_parentOptional | string | Documentation-only example for the nested Example: |
column_filters.delivery_service_idOptional | string | Documentation-only example for the nested Example: |
column_filters.source_transfer_manifest_idOptional | string | Documentation-only example for the nested Example: |
sort_byOptional | string | Optional column to sort by. Allowed values: |
sort_orderOptional | string | Optional sort order: asc or desc. Default: desc. |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/deliveries?page=1&limit=10&status=&search=&customer_id=&driver_id=&assignment_status=&bulk_group=&delivery_type=&tenant_id=&column_filters=&sort_by=&sort_order=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Deliveries retrieved successfully",4 "deliveries": [5 {6 "id": "original-delivery-uuid",7 "delivery_code": "2501",8 "return_delivery_id": "return-delivery-uuid",9 "return_delivery_code": "2601",10 "original_delivery_id": null11 }12 ],13 "page": 1,14 "limit": 10,15 "total": 116}Get Driver Deliveries
/delivery/driver-deliveriesDriver delivery cards expose the existing parcel contract plus returnStatus. returnStatus=null means normal actions. return_approved or return_in_transit is the persistent instruction to hide Deliver, show Mark Returned, and use the card's existing pickup location as the return destination. This field is read directly from the delivery; the driver app never needs to query a resolved exception. After a failed operational return is received back at its pickup warehouse, the delivery becomes Pending and is discoverable in status=pending when the driver is eligible to self-assign. Its previous returned assignment remains audit history and is not returned as current ownership: the card has assignmentId=null and assignmentStatus=null until a fresh assignment is created. The frontend must show Assign, not Change Driver or Unassign, while those current-assignment fields are null. Every driver delivery row includes nullable sourceTransferManifestId; a non-null value identifies the originating Transfer Manifest. For a staged child, the value is inherited from its parent order. pickupLocation and deliveryLocation preserve their stored title value when present. Frontend integration: when sourceTransferManifestId is non-null and the relevant location title exists, show that title before the zone pill.
G2G origin display
each delivery includes nullable g2gOrigin with { "branchName": string|null, "branchAddress": string|null }. It is populated only for Transfer Manifest orders from a valid stored manifest_origin_v1 snapshot, with the staged parent snapshot used when the child has no usable origin and its manifest ID is absent or matches the parent. Normal orders and older orders without usable saved origin return null. When sourceTransferManifestId is non-null, show a read-only Original G2G location line separately from pickup, using the available branch name and address; if g2gOrigin is null, show Origin unavailable. Hide the line only when the manifest ID is null. Keep pickupLocation, deliveryLocation, maps, navigation, route calculations, and driver actions unchanged. Origin metadata is returned by the existing batched list read or single detail read; do not request one detail per card.
Driver deliveries list (driver-only).
Pagination order
Rows keep the existing business priority and
updatedAtdescending order. Rows with the sameupdatedAtare ordered bydeliveryIddescending, so offset pages do not overlap because of timestamp ties.
Default behavior (no status filter)
Returns normal deliveries assigned to the driver
Excludes delivered deliveries
Includes a cancelled delivery only when its latest assignment for this driver is still
picked_uporen_route_to_delivery; this represents physical parcel custody, not permission to continue deliveryExcludes child deliveries whose Waybill is active (
drafted,sealed,assigned,picked_up,in_transit, orawaiting_receipt). Those packages are represented by the Waybill card fromGET /delivery/driver-waybillsand must not be independently startable from the normal delivery list.
Exception decision contract
Every row includes
returnStatus:null,return_approved,return_in_transit, orreturn_completed.While
status=exception, an open exception blocks driver actions.After dispatcher Retry,
returnStatusis null and the driver continues the normal delivery action.After dispatcher Return,
returnStatus=return_approved; hide Delivered and show one whole-delivery Mark Returned action usingPOST /delivery/deliveries/:deliveryId/status.
Cancelled custody contract
Every row exposes
assignmentIdandassignmentStatusA cancelled custody row includes
returnInstructionwithaction=complete_return_handoff, a localizedmessage, anddestination.warehouseIdplus the existing location snapshotFor an inbound warehouse leg, the destination is the drop-off warehouse; for an outbound warehouse leg, it is the pickup warehouse; a non-warehouse delivery returns to its pickup location
Cancelled custody rows are ordered before normal active work and count in the
alltotal until handoff completion
Filtering
status=delivered: returns only delivered deliveriesstatus=pending: returns pending deliveries. If tenantdriver_config.driver_can_self_assignis true, it also includes unassigned pending deliveries. Pending deliveries that are already assigned to this driver (pending acceptance) are returned first and includependingAcceptance: true.status=en_route_to_pickup: returns deliveries whose delivery status has moved beyond assigned and the driver is travelling to pickup.Active-Waybill children are excluded from every status and search filter. Children of
received,closed, orcancelledWaybills follow normal delivery filtering.
Proof Requirements
Each delivery card includes
proofRequirements.pickupandproofRequirements.deliverywhen a delivery service policy can be resolved.If the pickup endpoint is a warehouse, pickup uses
proof_settings.warehouse_pickup.If the pickup endpoint is not a warehouse, pickup uses
proof_settings.driver_pickup.If the delivery endpoint is a warehouse, delivery uses
proof_settings.warehouse_delivery.If the delivery endpoint is not a warehouse, delivery uses
proof_settings.driver_delivery.
Frontend contract
When
status=cancelledandreturnInstructionis present, show the instruction and destination, hide every normal delivery action, and offer only the literalreturnInstruction.action.Submit the row's child
deliveryIdtoPOST /delivery/action/:deliveryId; do not use a staged parent ID.Render status tabs in the backend-provided order:
all,pending(labelAvailable),assigned,en_route_to_pickup,picked_up,in_transit,delivered,cancelled,returned.Render and navigate active Waybill work from
/delivery/driver-waybills, not from a child delivery card.The backend also suppresses
delivery_realtime_assignmentsrows for active-Waybill children; usewaybill_realtimeto refresh Waybill cards.Use each card's
proofRequirementsto decide whether to prompt for pickup/delivery photo or signature.If a card has
deliveryFlow = "replacement_forward", show the replacement badge and the replacement completion flow. For staged replacements, usereplacementParentDeliveryIdonly as the parent linkage id; continue sending the childdeliveryIdfor driver actions.
Replacement staging
For the final outbound child of a warehouse-staged replacement parent, each delivery row exposes
deliveryFlow: "replacement_forward"even though the child leg itself remains an operational child.That row also includes
replacementParentDeliveryIdwith the staged parent id.
Response includes
deliveries: simplified UI shapestatuses: list of status counts for the driver UI tabs/filters, ordered asall,pending,assigned,en_route_to_pickup,picked_up,in_transit,delivered,cancelled
G2G origin display
each delivery includes nullable g2gOrigin with { "branchName": string|null, "branchAddress": string|null }. It is populated only for Transfer Manifest orders from a valid stored manifest_origin_v1 snapshot, with the staged parent snapshot used when the child has no usable origin and its manifest ID is absent or matches the parent. Normal orders and older orders without usable saved origin return null. When sourceTransferManifestId is non-null, show a read-only Original G2G location line separately from pickup, using the available branch name and address; if g2gOrigin is null, show Origin unavailable. Hide the line only when the manifest ID is null. Keep pickupLocation, deliveryLocation, maps, navigation, route calculations, and driver actions unchanged. Origin metadata is returned by the existing batched list read or single detail read; do not request one detail per card.
Query parameters
| Parameter | Type | Description |
|---|---|---|
page | string | Page number for pagination (default: 1) Example: |
limit | string | Number of items per page (default: 10) Example: |
statusOptional | string | Filter by delivery status (optional) |
searchOptional | string | Search term for filtering deliveries (optional) |
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/driver-deliveries?page=1&limit=10&status=&search=&tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "deliveries": [4 {5 "deliveryId": "outbound-child-uuid",6 "deliveryCode": "1500B",7 "sourceTransferManifestId": "manifest-uuid",8 "status": "in_transit",9 "returnStatus": "return_approved",10 "pickupLocation": {11 "title": {12 "en": "Processing point"13 },14 "full_address": "Warehouse origin"15 },16 "quantity": 3,17 "parcelCount": 2,18 "packageDetails": {19 "total_items": 2,20 "total_quantity": 2,21 "items": [22 {23 "id": "parcel-1",24 "status": "pending",25 "parent_status": "pending",26 "actionable": true27 },28 {29 "id": "parcel-2",30 "status": "pending",31 "parent_status": "pending",32 "actionable": true33 }34 ]35 },36 "g2gOrigin": {37 "branchName": "Original branch",38 "branchAddress": "Original branch address"39 }40 }41 ],42 "total": 1,43 "page": 1,44 "limit": 1045}Get Driver Delivery Detail V1
/delivery/driver/deliveries/{{delivery_id}}delivery.sourceTransferManifestId contains the originating Transfer Manifest ID or null. For a staged child, the value is inherited from its parent order. pickupLocation.title and deliveryLocation.title preserve the stored multilingual title value or return null. Frontend integration: when the manifest ID is non-null and the relevant title exists, show the localized title before the zone pill.
G2G origin display
each delivery includes nullable g2gOrigin with { "branchName": string|null, "branchAddress": string|null }. It is populated only for Transfer Manifest orders from a valid stored manifest_origin_v1 snapshot, with the staged parent snapshot used when the child has no usable origin and its manifest ID is absent or matches the parent. Normal orders and older orders without usable saved origin return null. When sourceTransferManifestId is non-null, show a read-only Original G2G location line separately from pickup, using the available branch name and address; if g2gOrigin is null, show Origin unavailable. Hide the line only when the manifest ID is null. Keep pickupLocation, deliveryLocation, maps, navigation, route calculations, and driver actions unchanged. Origin metadata is returned by the existing batched list read or single detail read; do not request one detail per card.
Fast driver-only delivery detail projection. A valid user Bearer JWT is required; API keys are not accepted. The shared Delivery Edge middleware is the only authentication owner: it validates the credential and resolves the active user's tenant, roles, permissions, and team context. The handler rejects non-JWT and non-driver actors and passes only the middleware-owned tenant and user IDs to one service-role-only RPC. Any authenticated user with a current driver record may read an internal, non-parent delivery in the same tenant when delivery-service and customer team restrictions permit it, even when that delivery is unassigned or assigned to another driver. Assignment is returned only when it belongs to the authenticated driver; otherwise assignment is null and pendingAcceptance is false. Scanner and action APIs keep their existing assignment, route, Waybill, zone, and action-state authorization. A client-supplied tenant or driver ID is never accepted. Missing, cross-tenant, team-restricted, and unsupported delivery identifiers return HTTP 404 delivery_not_found.
The response is authoritative and uses camelCase with explicit nulls, numeric zeroes, and empty arrays. It contains current assignment/route data for the authenticated driver, customer contact data, pickup and delivery locations, endpoint warehouses, service/type, fees and collection amounts, proof requirements, leg-aware parcel state (including pickupStatus and actionable), current/parent exception history with labels, and compact relationships. quantity and packageDetails.totalQuantity preserve the legacy product/package count from package_details.total_quantity, defaulting to 1 when it is absent or invalid. unitSize remains the separate vehicle-capacity value and is never used as product quantity. The response intentionally excludes driver/vehicle objects, carrier/Prime data, shipment labels, proof history, order/status timelines, parent/child trees, search strings, and generic admin enrichment.
For a staged outbound child, packageDetails.items excludes source packages whose return_status is return_approved, return_in_transit, or return_completed. packageDetails.totalItems, its item-status summary, parcelCount, and actionableParcelCount describe the remaining projected packages. This matches the existing generic detail and driver-list package projection. The exclusion does not apply to inbound history or the separate return delivery. A missing pickup status does not hide an otherwise eligible package; only explicit not_received does. Clients use the returned items and actionable flags for package actions; no new request field or mobile adapter change is required.
relationships.original, relationships.return, and relationships.replacementParent contain only deliveryId, deliveryCode, status, and deliveryFlow. They are display/navigation context, never mutation authorization. Detail visibility also never authorizes a mutation; every scanner and action endpoint independently applies its existing stricter rules.
Important errors
HTTP 400 invalid_filter_type or invalid_delivery_identifier; HTTP 401 unauthorized when authentication is missing or the authenticated mechanism is not JWT; HTTP 403 unauthorized for an invalid/expired credential from the shared middleware, or insufficient_permissions / user_account_is_inactive; HTTP 404 delivery_not_found; HTTP 500 internal_server_error. Every handler response is private, no-store and includes X-Request-ID.
Frontend integration contract
add one mobile service adapter from this camelCase V1 object to the existing DeliveryItem shape. Use quantity for product/package count and unitSize only for capacity. Use this endpoint for driver detail reads only; warehouse detail screens stay on the generic endpoint. Do not infer action eligibility from successful detail access; keep using the existing scanner and action responses. Keep the old endpoint behind a temporary rollout flag until staging field-parity and latency comparison pass.
G2G origin display
each delivery includes nullable g2gOrigin with { "branchName": string|null, "branchAddress": string|null }. It is populated only for Transfer Manifest orders from a valid stored manifest_origin_v1 snapshot, with the staged parent snapshot used when the child has no usable origin and its manifest ID is absent or matches the parent. Normal orders and older orders without usable saved origin return null. When sourceTransferManifestId is non-null, show a read-only Original G2G location line separately from pickup, using the available branch name and address; if g2gOrigin is null, show Origin unavailable. Hide the line only when the manifest ID is null. Keep pickupLocation, deliveryLocation, maps, navigation, route calculations, and driver actions unchanged. Origin metadata is returned by the existing batched list read or single detail read; do not request one detail per card.
Path parameters
| Parameter | Type | Description |
|---|---|---|
delivery_idRequired | string | Variable used inside the request path. |
Query parameters
| Parameter | Type | Description |
|---|---|---|
filter_typeRequired | string | Required lookup mode. Allowed values are exactly Example: |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/driver/deliveries/${DELIVERY_ID}?filter_type=id&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Delivery retrieved successfully",4 "delivery": {5 "deliveryId": "delivery-uuid",6 "deliveryCode": "3501",7 "barcode": "DEL3501",8 "externalReferenceId": null,9 "sourceTransferManifestId": "manifest-uuid",10 "status": "assigned",11 "returnStatus": null,12 "deliveryFlow": null,13 "pendingAcceptance": false,14 "assignment": {15 "id": "assignment-uuid",16 "status": "assigned",17 "sequence": 1,18 "routeSessionId": null,19 "routeDetails": null20 },21 "customer": {22 "id": "customer-uuid",23 "fullName": "Test Customer",24 "avatarUrl": null,25 "phone": null,26 "email": null27 },28 "pickupLocation": {29 "id": null,30 "title": {31 "en": "Processing point"32 },33 "fullAddress": "Pickup address",34 "latitude": 33.3,35 "longitude": 44.4,36 "city": null,37 "state": null,38 "country": null,39 "zoneName": "Pickup Zone",40 "zoneCode": "PZ",41 "contactInfo": null42 },43 "deliveryLocation": {44 "id": null,45 "title": {46 "en": "Destination branch"47 },48 "fullAddress": "Delivery address",49 "latitude": 33.4,50 "longitude": 44.5,51 "city": null,52 "state": null,53 "country": null,54 "zoneName": "Delivery Zone",55 "zoneCode": "DZ",56 "contactInfo": null57 },58 "pickupWarehouse": null,59 "dropoffWarehouse": null,60 "deliveryService": null,61 "priority": 1,62 "unitSize": 1,63 "quantity": 1,64 "notes": null,65 "scheduledPickupTime": null,66 "scheduledDeliveryTime": null,67 "paymentMethod": "wallet",68 "codAmount": 0,69 "payerType": "sender",70 "deliveryFee": 0,71 "feeToCollectAtPickup": 0,72 "feeToCollectAtDelivery": 0,73 "proofRequirements": {74 "pickup": {75 "photoRequired": false,76 "signatureRequired": false77 },78 "delivery": {79 "photoRequired": true,80 "signatureRequired": true81 }82 },83 "packageDetails": {84 "totalItems": 1,85 "totalQuantity": 1,86 "itemStatusSummary": {87 "pending": 1,88 "delivered": 0,89 "returned": 0,90 "isPartiallyDelivered": false,91 "isPartiallyReturned": false92 },93 "pickupStatusSummary": null,94 "items": []95 },96 "parcelCount": 1,97 "actionableParcelCount": 0,98 "packageOutcomeStage": "final_delivery",99 "packageOutcomeActionsAllowed": true,100 "exceptions": [],101 "relationships": {102 "original": null,103 "return": null,104 "replacementParent": null105 },106 "createdAt": "2026-08-30T00:00:00Z",107 "updatedAt": "2026-08-30T00:00:00Z",108 "g2gOrigin": {109 "branchName": "Original branch",110 "branchAddress": "Original branch address"111 }112 }113}Resolve Driver Delivery Barcode V1
/delivery/driver/deliveries/scan/{{barcode}}Lightweight driver-only barcode resolver. A valid user Bearer JWT is required; API keys are not accepted. The shared Delivery Edge middleware is the only authentication owner and resolves the active user's trusted tenant and role context. The handler rejects non-JWT and non-driver actors, then one service-role-only RPC applies driver visibility and performs the indexed (tenant_id, barcode) lookup. Missing and unauthorized barcodes both return HTTP 404.
A normal successful scan returns the one actionable deliveryId. When an original or P&D barcode has no active forward leg because its outbound delivery failed, but it has an authorized active linked return, the same response also contains compact delivery and return_delivery records. delivery is the failed outbound with is_active=false and next_action=null. return_delivery is the linked return with its current is_active and driver next_action. Top-level deliveryId is the actionable return delivery ID. The active return prevents no_actionable_delivery_leg. Direct return-barcode scans and orders without active returns keep the normal response.
If more than one independently actionable authorized delivery exists, the endpoint returns HTTP 409 ambiguous_delivery_barcode. A recognized parent with no actionable child and no active linked return returns HTTP 409 no_actionable_delivery_leg. Important authentication errors are HTTP 401 unauthorized when authentication is missing or the authenticated mechanism is not JWT; HTTP 403 unauthorized for an invalid/expired credential from the shared middleware, or insufficient_permissions / user_account_is_inactive.
Frontend integration contract
call this endpoint first. For a normal response, navigate using deliveryId and fetch detail only when the destination screen needs it. When return_delivery is present, show the existing active-return presentation, render delivery only as inactive context, and use top-level deliveryId for the actionable return navigation. Display localized messages for HTTP 409 conflicts. Do not use this route for the warehouse scanner. Keep a temporary feature flag to the legacy barcode detail until staging scanner parity passes.
Path parameters
| Parameter | Type | Description |
|---|---|---|
barcodeRequired | string | Variable used inside the request path. |
Query parameters
| Parameter | Type | Description |
|---|---|---|
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/driver/deliveries/scan/${BARCODE}?language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Delivery barcode resolved successfully",4 "scannedBarcode": "DEL3501",5 "deliveryId": "actionable-delivery-uuid"6}Get Driver Delivery Status Summary
/delivery/driver-deliveries/status-summaryThe Available count uses the same current-assignment rule as the Pending driver list. A Pending operational-return retry with only a previous returned assignment counts as Available when self-assignment, zone, team, and service eligibility pass.
Get driver delivery status summary with counts and labels (driver-only).
Purpose
Lightweight endpoint that returns only status counts and labels
Useful for displaying status tabs/cards in the driver UI
Does not include delivery list data
Uses the same visibility rule as
GET /delivery/driver-deliveries: children of active Waybills (drafted,sealed,assigned,picked_up,in_transit,awaiting_receipt) are excluded from all counts.
Response includes
statuses: Array of status objects withstatus,label, andcountfieldsStatus order:
all,pending(labelAvailable),assigned,en_route_to_pickup,picked_up,in_transit,delivered,cancelled
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/driver-deliveries/status-summary?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Status summary retrieved successfully",4 "statuses": [5 {6 "status": "all",7 "label": "All",8 "count": 59 },10 {11 "status": "pending",12 "label": "Available",13 "count": 514 },15 {16 "status": "assigned",17 "label": "Assigned",18 "count": 219 },20 {21 "status": "en_route_to_pickup",22 "label": "En Route To Pickup",23 "count": 124 },25 {26 "status": "picked_up",27 "label": "Picked Up",28 "count": 029 },30 {31 "status": "in_transit",32 "label": "In Transit",33 "count": 234 },35 {36 "status": "delivered",37 "label": "Delivered",38 "count": 1039 },40 {41 "status": "cancelled",42 "label": "Cancelled",43 "count": 044 }45 ]46}Get Delivery
/delivery/deliveries/:deliveryIdDelivery detail includes delivery.return_status directly from the delivery read model. Its value is null | return_approved | return_in_transit | return_completed. For a linked operational return, the canonical original/P&D parent remains status=staged_at_warehouse while the return is Pending, Assigned, or En route to pickup; physical picked_up or in_transit changes it to status=returning and return_status=return_in_transit; return delivery completion changes it to status=returned and return_status=return_completed. The failed outbound leg remains Failed. This is stored database state, not response-only mapping. For tenant admin/dispatcher detail, canonical order_events records return lifecycle changes as return_to_sender_approved, returning_to_sender, and returned_to_sender. Each return event contains timestamp, optional actor/reason/notes, previous_return_status, new_return_status, and metadata with source=delivery_return_status, delivery_id, and delivery_codeplus available return-operation IDs. Operational P&D child custody changes are not projected as parent sender-return events. When a primary status changes toreturningorreturnedtogether with its return status, the primary status event remains authoritative and a duplicate return-status event is not appended. Historical parents affected by the former hybrid RTS cancellation repair retain their original audit entries and receive one additivefailed_delivery_status_correctedevent. Its metadata hassource=hybrid_outbound_rts_parent_event_correction, the corrected child and return IDs, and parent_status_unchanged=true; it corrects the audit meaning and does not represent parcel movement or a parent status transition. Canonical order_eventsalso includeswarehouse_transfer_completedas soon as a present/damaged member of a warehouse-to-warehouse Waybill is marked received. This is a custody/location event independent of delivery status: a staged P&D parent can remainstaged_at_warehousewhile the transfer event is appended. The transfer event contains the receipttimestamp, actor_id/actor_name/actor_role, a system-generated notes value (Transfer received from {origin} at {destination}), and metadata with waybill_id, waybill_number, driver_id, origin/destination warehouse IDs and names, received_at, and verification counts. Missing members and location-origin/destination Waybills do not add this event. A later Waybill close does not duplicate it. For tenant admin/dispatcher detail, order_eventsalways belongs to the requested delivery row. A parent request returns the parent's aggregate history; a P&D child request returns only that inbound/outbound leg's status, driver/handoff, exception, and custody events. No separatedelivery_eventsfield is returned. The history is loaded by one primary-key query. Customer and driverstatus_updates` remain unchanged.
G2G origin display
detail includes nullable source_transfer_manifest_id, source_transfer_manifest_item_id, and source_transfer_manifest_meta, consistent with the delivery list. For a Transfer Manifest order whose metadata has snapshot_version=manifest_origin_v1, show the non-empty source_branch_name and source_branch_address as a separate read-only Original G2G location line. For a known manifest-linked order with no usable saved origin, show Origin unavailable. Hide the line only when source_transfer_manifest_id is null; display any genuine partial name or address that is available. The operational pickup_location and delivery_location remain unchanged; use those for maps, navigation, and routing. These origin fields come from the existing saved order snapshot; no branch lookup is required.
Get delivery details. Includes carrier fields (carrier_type, direction, pickup_warehouse_id, dropoff_warehouse_id). If staged/hybrid parent, also includes sequence_plan, children_summary, and full children; if prime, includes prime object.
Internal event history (tenant admin/dispatcher)
delivery.order_eventsbelongs to the requested delivery row.For a P&D parent request, it contains the parent's complete aggregate journey. For a P&D inbound/outbound child request, it contains only that operational leg's status, driver/handoff, exception, and Waybill custody events.
No separate
delivery_eventsfield is returned. The requested history is loaded with one bounded primary-key database query. Customer and driver tracking continues to usestatus_updates.
Children
children_summaryis a compact operational-leg list withid,type,direction,status, and optionalprime.childrenis included for staged/hybrid parent detail responses when child rows are available fromdelivery_list_view.Each child includes the normal delivery list view fields, including
delivery_flowwhen present. Replacement-return children are excluded from this parent operational children list.
Parent assignment projection
For staged/P&D parent details, the existing
assignmentobject is built from child delivery assignments instead of a parent assignment row.assignment.source = "child_rollup"identifies this response-only projection.assignment.delivery_id,assignment.delivery_code,assignment.direction, andassignment.leg_sequenceidentify the active/source child leg.assignment.status_updatesis a merged child-assignment timeline. Each event includessource = "child_assignment",assignment_id,delivery_id,delivery_code,direction, andleg_sequence.driverandvehiclefollow the same source child leg as the projected assignment.Cancelled/returned child assignments remain visible for history/audit, so terminal staged parents still expose the source child driver instead of
Not Assigned.Frontend integration: keep reading
delivery.driver,delivery.assignment.status, anddelivery.assignment.status_updates; no separate parent-specific field is required.
Proofs
The optional
proofsarray contains one row per submitted asset. Each row hasactor_type,actor_id,stage(pickupordelivery),proof_type(photoorsignature), andstorage_url.proof_of_pickupandproof_of_deliveryare delivery-level aggregate proof summaries when captured, shaped as{ signature, photos }.
Exceptions
delivery.has_exceptionanddelivery.exception(with compatibility aliaseshas_open_exceptionandopen_exception) expose the compact current-open-exception snapshot. During the warehouse decision stage the exception is stored directly on the P&D parent; once an outbound is assigned or moving, an exception is stored on that outbound and the existing parent projection remains compatible.While the parent exception is open, the nested outbound child response remains unchanged. Frontend derives its blocked display from the parent
has_exceptionandexceptionfields. Starting or progressing that outbound returns HTTP 409parent_exception_open.The top-level
exceptionsarray contains the delivery's exception history.Each item includes the canonical
exception_typeand the user-facingexception_labelfrom the tenantexception_typessetting. Renderexception_label; do not send it when creating or updating an exception.
Replacement orders
delivery_flowindicates if this is areplacement_forwardorreplacement_return(null for normal deliveries).If
delivery_flow = 'replacement_forward'and the return delivery has been auto-created, response includesreplacement_deliverywith the full return delivery details and its current status.If
delivery_flow = 'replacement_return', response includesreplacement_deliverywith the forward delivery details.replacement_deliveryis absent if not a replacement, or if the return has not yet been created (forward not yet completed).For the final outbound child of a warehouse-staged replacement parent, the detail response exposes effective
delivery_flow = "replacement_forward"andreplacement_parent_delivery_idwith the parent id so driver UI can render the replacement stop while still acting on the child delivery id. Once created, its linked return is also exposed throughreplacement_delivery.
Actor ID contract for new driver-performed delivery and assignment status events: actor_role=driver means actor_id is the tenant-scoped drivers.id, resolvable with GET /delivery/drivers/:driverId. A dispatcher-performed event uses the dispatcher user ID. Historical order events may contain a driver user ID instead; resolve a legacy value with GET /users/:userId when it is not a driver ID. Existing event JSON is not rewritten.
G2G origin display
detail includes nullable source_transfer_manifest_id, source_transfer_manifest_item_id, and source_transfer_manifest_meta, consistent with the delivery list. For a Transfer Manifest order whose metadata has snapshot_version=manifest_origin_v1, show the non-empty source_branch_name and source_branch_address as a separate read-only Original G2G location line. For a known manifest-linked order with no usable saved origin, show Origin unavailable. Hide the line only when source_transfer_manifest_id is null; display any genuine partial name or address that is available. The operational pickup_location and delivery_location remain unchanged; use those for maps, navigation, and routing. These origin fields come from the existing saved order snapshot; no branch lookup is required.
Path parameters
| Parameter | Type | Description |
|---|---|---|
deliveryIdRequired | string | ID of the delivery to retrieve Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/deliveries/${DELIVERYID}?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "delivery": {4 "id": "delivery-uuid",5 "source_transfer_manifest_id": "manifest-uuid",6 "pickup_location": {7 "title": {8 "en": "Processing point"9 },10 "full_address": "100 Meter Road, Erbil"11 },12 "delivery_location": {13 "title": {14 "en": "Destination branch"15 },16 "full_address": "Main Street, Erbil"17 },18 "source_transfer_manifest_item_id": "manifest-item-uuid",19 "source_transfer_manifest_meta": {20 "snapshot_version": "manifest_origin_v1",21 "source_branch_name": "Original branch",22 "source_branch_address": "Original branch address"23 }24 }25}Scan Delivery Barcode With Linked Return
/delivery/deliveries/{{delivery_barcode}}Read-only mobile scan contract for the existing Get Delivery handler. The backend preserves an eligible active linked return before resolving a P&D original child and returns the scanned delivery and linked return as separate choices. Every displayed delivery and return_delivery has its own non-null navigation target with {action,entity_id,reason}. Driver targets use action=open_delivery; entity_id is the ID of that exact displayed delivery and the fallback reason is delivery_details_available. Linked summaries include id, delivery_code, barcode, status, delivery_flow, is_active, and the actor's operational next_action. If a P&D original has no active forward child but has an active linked return, the scan still succeeds with the original context and return choice. If no eligible active return exists, return_delivery and return_target are null and the normal scan flow is unchanged. A direct return-barcode scan keeps the return in delivery, uses delivery_target, and includes original_delivery. Mobile shows Delivery and Return choices when return_delivery is present; selecting a choice uses only that choice's delivery ID and corresponding target. Dismissing the choice does not navigate or process either order. No assignment, receipt, handoff, or status change occurs during this GET. The return remains a separate order with its own stored barcode and shipment label.
Path parameters
| Parameter | Type | Description |
|---|---|---|
delivery_barcodeRequired | string | Variable used inside the request path. |
Query parameters
| Parameter | Type | Description |
|---|---|---|
filter_type | string | Example: |
tenant_id | string | Example: |
language | string | Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/deliveries/${DELIVERY_BARCODE}?filter_type=barcode&tenant_id=${TENANT_ID}&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Delivery retrieved successfully",4 "scanned_barcode": "DEL1001",5 "delivery": {6 "id": "original-delivery-uuid",7 "delivery_code": "1001",8 "barcode": "DEL1001",9 "status": "cancelled",10 "delivery_flow": "standard",11 "is_active": false,12 "next_action": null13 },14 "delivery_target": {15 "action": "open_delivery",16 "entity_id": "original-delivery-uuid",17 "reason": "delivery_details_available"18 },19 "return_delivery": {20 "id": "return-delivery-uuid",21 "delivery_code": "1050",22 "barcode": "DEL1050",23 "status": "pending",24 "delivery_flow": "operational_return",25 "is_active": true,26 "next_action": "en_route_to_pickup"27 },28 "return_target": {29 "action": "open_delivery",30 "entity_id": "return-delivery-uuid",31 "reason": "delivery_details_available"32 },33 "exceptions": []34}Create Delivery
/delivery/deliveriesUnit pricing contract
send both the user's unit_size input and current package_details. Fully measurable package details require weight, length, width, and height values above zero for every item, and the backend-calculated unit overrides unit_size. If any item has a zero, missing, or empty measurement, the package is treated as unmeasurable and the backend uses the submitted unit_size. Negative or non-numeric measurements remain invalid. If neither usable package measurements nor a submitted unit is present, creation uses general.unit_size.settings.missing_unit_size_fallback: service minimum, half of service maximum, service maximum, or HTTP 400 unit_size_or_dimensions_required. The calculated/selected unit is validated against the service unit_min/unit_max before routing, pricing, wallet handling, or persistence. An out-of-range unit returns HTTP 400 with code can_not_use_this_delivery_for_this_unit_size, a localized cause, and calculated/allowed unit fields. The success delivery includes unit_size. Frontend must preview the same payload through POST /delivery/price and show its returned unit and price. Payment lifecycle: zero fee is immediately paid; a positive wallet fee on a draft is deducted from usable wallet balance into reserved, without creating a wallet transaction or payment. Execute captures that hold and creates the delivery-fee transaction; cancelling the draft releases it. Non-draft sender wallet activation still charges immediately; receiver wallet remains pending until final completion; cash remains pending until its pickup/dropoff collection point.
Create a new delivery.
Key rules
external_reference_idis optional. When tenant settingdelivery_external_reference.unique_per_customer=true(default), a non-empty value must be unique among root orders for the same tenant customer. Staged child legs reuse the root reference. A duplicate returns HTTP 400external_reference_id_already_exists; when false, duplicates are accepted.Provide
pickup_locationanddelivery_locationas JSON objects with lat/long and country for non-warehouse endpoints.If
pickup_warehouse_idis provided, the API resolves pickup from that warehouse address and ignores any submittedpickup_location/pickup_addressJSON for pickup.If
dropoff_warehouse_idis provided, the API resolves delivery from that warehouse address and ignores any submitteddelivery_location/delivery_addressJSON for delivery.If only address IDs are provided by legacy callers, the API resolves them once and embeds JSON.
Contact info for labels/notifications is taken from the resolved address JSON
contact_info.For a single package, place
weight,length,width,height, and optionalquantitydirectly inpackage_details. For multipart, usepackage_details.items; the backend rebuilds that field from normalized item rows.Optional
is_draft=truecreates the order withstatus=draft; it may be sent in the JSON body or as?is_draft=true. Missing or false keeps the existingpendingbehavior. A positive wallet fee is reserved from usable balance without creating a payment or wallet transaction. Execute captures the reservation and creates the delivery-fee transaction; cancelling the draft releases the hold. Draft orders do not queue auto-assignment and are not visible to driver workflows until executed.
Warehouse staging
If
requires_warehouse_staging=true, the API creates a HYBRID parent with 2 legs (pickup→warehouse, warehouse→dropoff).staging_warehouse_idis optional; if null, system auto-selects.package_details.itemsis optional. When omitted or empty, backend creates one warehouse inventory item automatically with name{delivery_code} - {customer_name}, quantity 1, and usespackage_details.weightwhen supplied.Staged P&D database persistence is atomic: the parent, A/B legs, package items, inventory items, receiving order, sequence plan, and PRIME extension rows either all commit or all roll back.
Automatically allocated inventory item codes skip codes already claimed manually.
Guard: staging is supported for delivery types "Pickup and Delivery" and "Delivery"; otherwise API returns
warehouse_staging_only_supported_for_pickup_and_delivery.When
is_draft=true, the parent and child legs are held in draft. Execute the parent withtarget_delivery_status=pendingto release the parent and first leg.
Frontend integration contract
For warehouse toggles, send the matching
pickup_warehouse_idordropoff_warehouse_id; the backend will use the warehouse address even if the matching location JSON is present or contains null coordinates.For non-warehouse endpoints, continue sending location JSON with valid coordinates.
For a single package, send direct dimensions in
package_details; for multipart, sendpackage_details.items.Frontend does not need to block submission when item rows are empty.
If item rows are omitted, backend creates the receiving-order inventory item automatically; handle only existing HTTP errors such as invalid service, warehouse selection failure, wallet balance (
insufficient_available_balance), missing payer wallet, or unsupported service type.Use optional checkbox value as
is_draft; default it to false. The backend accepts either bodyis_draft: trueor query?is_draft=true.For wallet draft rows, show the held fee from the wallet's aggregate
reservedvalue; currency comes from tenant configuration. Show Execute and callPOST /delivery/deliveries/:deliveryId/statuswith{ "target_delivery_status": "pending" }.
Replacement orders
Set
is_replacement=trueto create a replacement forward delivery.For non-staged replacements, the created delivery is marked
delivery_flow = 'replacement_forward'.For warehouse-staged replacements, the HYBRID parent is marked
delivery_flow = 'replacement_forward'; child legs remain normal operational legs.When the replacement forward reaches the customer, the system auto-creates a zero-fee return using the same
delivery_service_id.For staged replacements, the return is created only after the final outbound child is completed; inbound/warehouse staging events do not create the return.
If that service requires warehouse staging, the return creates a second HYBRID parent with customer→warehouse inbound and warehouse→original-pickup outbound legs. Otherwise it remains one direct return delivery.
The return parent links to the replacement forward through
replacement_forward_delivery_id;parent_delivery_idis reserved for HYBRID operational children.The driver is assigned to the return inbound child for staged returns, or the direct return for non-staged returns. Every return parent and leg has
delivery_fee = 0because the replacement forward fee covers the round trip.No need to reference any original order — replacements are standalone.
Required
delivery_service_id,customer_id,payer_type,payment_method.
Query parameters
| Parameter | Type | Description |
|---|---|---|
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
requires_warehouse_staging | boolean | — |
staging_warehouse_id | null | — |
is_replacement | boolean | — |
is_draft | boolean | — |
pickup_location | object | — |
pickup_location.full_address | string | — |
pickup_location.lat | number | — |
pickup_location.long | number | — |
pickup_location.country | string | — |
pickup_location.contact_info | object | — |
pickup_location.contact_info.phone | string | — |
pickup_location.contact_info.email | string | — |
Show 31 more body fields
Additional body parameters
| Parameter | Type | Description |
|---|---|---|
delivery_location | object | — |
delivery_location.full_address | string | — |
delivery_location.lat | number | — |
delivery_location.long | number | — |
delivery_location.country | string | — |
delivery_location.contact_info | object | — |
delivery_location.contact_info.phone | string | — |
delivery_location.contact_info.name | string | — |
pickup_warehouse_id | null | — |
dropoff_warehouse_id | null | — |
delivery_service_id | string | — |
package_details | object | — |
package_details.special_instructions | string | — |
package_details.items | array<object> | — |
package_details.items.name | string | — |
package_details.items.description | string | — |
package_details.items.quantity | number | — |
package_details.items.weight | number | — |
package_details.items.length | number | — |
package_details.items.width | number | — |
package_details.items.height | number | — |
customer_id | string | — |
external_reference_id | string | — |
status | string | — |
scheduled_pickup_time | string | — |
scheduled_delivery_time | string | — |
priority | number | — |
notes | string | — |
payer_type | string | — |
payment_method | string | — |
delivery_fee | number | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/deliveries?language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "requires_warehouse_staging": false,6 "staging_warehouse_id": null,7 "is_replacement": false,8 "is_draft": false,9 "pickup_location": {10 "full_address": "Baghdad HQ",11 "lat": 33.3152,12 "long": 44.3661,13 "country": "IQ",14 "contact_info": {15 "phone": "+964700000000",16 "email": "[email protected]"17 }18 },19 "delivery_location": {20 "full_address": "Basra Customer",21 "lat": 30.5081,22 "long": 47.7835,23 "country": "IQ",24 "contact_info": {25 "phone": "+964711111111",26 "name": "Customer Name"27 }28 },29 "pickup_warehouse_id": null,30 "dropoff_warehouse_id": null,31 "delivery_service_id": "uuid",32 "package_details": {33 "special_instructions": "Handle with care",34 "items": [35 {36 "name": "Mobile phone",37 "description": "Mobile phone",38 "quantity": 2,39 "weight": 2.5,40 "length": 30,41 "width": 20,42 "height": 1543 }44 ]45 },46 "customer_id": "uuid",47 "external_reference_id": "ORD-12345",48 "status": "pending",49 "scheduled_pickup_time": "2023-08-02T14:00:00Z",50 "scheduled_delivery_time": "2023-08-02T16:00:00Z",51 "priority": 1,52 "notes": "Customer prefers delivery after 3pm",53 "payer_type": "sender",54 "payment_method": "wallet",55 "delivery_fee": 300056}'Response examples
1{2 "success": true,3 "message": "Delivery created successfully",4 "delivery": {5 "id": "uuid",6 "tenant_id": "uuid",7 "customer_id": "uuid",8 "delivery_service_id": "uuid",9 "pickup_location_id": "uuid",10 "delivery_location_id": "uuid",11 "status": "pending",12 "payment_status": "paid",13 "delivery_fee": 25.99,14 "payer_type": "sender",15 "payment_method": "wallet",16 "created_at": "2023-08-02T14:00:00Z"17 }18}Create Delivery (Carrier Routing)
/delivery/deliveriesCreate a delivery using the carrier-aware routing engine.
Behavior
external_reference_idfollowsdelivery_external_reference.unique_per_customer(defaulttrue) for the same tenant customer at the root-order level. Hybrid child legs reuse the root reference.Provide
pickup_locationanddelivery_locationas JSON with lat/long/country for non-warehouse endpoints.If
pickup_warehouse_idis provided, the pickup endpoint is resolved from that warehouse address and any submitted pickup JSON is ignored.If
dropoff_warehouse_idis provided, the delivery endpoint is resolved from that warehouse address and any submitted delivery JSON is ignored.For a single package, place dimensions directly in
package_details; for multipart, usepackage_details.items, which is rebuilt from normalized item rows.
Warehouse staging
If
requires_warehouse_staging=true, the API creates a HYBRID parent with 2 legs.staging_warehouse_idis optional; if null, system auto-selects.package_details.itemsis optional. When omitted or empty, backend creates one warehouse inventory item automatically with name{delivery_code} - {customer_name}, quantity 1, and usespackage_details.weightwhen supplied.Staged P&D database persistence is atomic: the parent, A/B legs, package items, inventory items, receiving order, sequence plan, and PRIME extension rows either all commit or all roll back.
Automatically allocated inventory item codes skip codes already claimed manually.
Guard: staging is supported for delivery types "Pickup and Delivery" and "Delivery"; otherwise API returns
warehouse_staging_only_supported_for_pickup_and_delivery.
Frontend integration contract
For warehouse toggles, send the matching
pickup_warehouse_idordropoff_warehouse_id; the backend resolves coordinates from the warehouse address.For non-warehouse endpoints, continue sending location JSON with valid coordinates.
For a single package, send direct dimensions in
package_details; for multipart, sendpackage_details.items.Frontend does not need to require at least one item row before submission.
If item rows are omitted, backend creates the receiving-order inventory item automatically.
Notes
Pure PRIME eligibility requires carrier-enabled zones and at least one endpoint being a warehouse in a zone with configured warehouse.
HYBRID creates parent + two children and sequence plan.
Non-carrier flow unchanged when
carrier_routingis false.
Query parameters
| Parameter | Type | Description |
|---|---|---|
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
carrier_routing | boolean | — |
requires_warehouse_staging | boolean | — |
staging_warehouse_id | null | — |
pickup_location | object | — |
pickup_location.full_address | string | — |
pickup_location.lat | number | — |
pickup_location.long | number | — |
pickup_location.country | string | — |
delivery_location | object | — |
delivery_location.full_address | string | — |
delivery_location.lat | number | — |
delivery_location.long | number | — |
Show 25 more body fields
Additional body parameters
| Parameter | Type | Description |
|---|---|---|
delivery_location.country | string | — |
pickup_warehouse_id | null | — |
dropoff_warehouse_id | null | — |
delivery_service_id | string | — |
customer_id | string | — |
payer_type | string | — |
payment_method | string | — |
unit_size | number | — |
notes | string | — |
cod_amount | number | — |
external_reference_id | string | — |
scheduled_pickup_time | string | — |
scheduled_delivery_time | string | — |
receiver_user_id | string | — |
priority | number | — |
package_details | object | — |
package_details.special_instructions | string | — |
package_details.items | array<object> | — |
package_details.items.name | string | — |
package_details.items.description | string | — |
package_details.items.quantity | number | — |
package_details.items.weight | number | — |
package_details.items.length | number | — |
package_details.items.width | number | — |
package_details.items.height | number | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/deliveries?language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "carrier_routing": true,6 "requires_warehouse_staging": false,7 "staging_warehouse_id": null,8 "pickup_location": {9 "full_address": "Baghdad HQ",10 "lat": 33.3152,11 "long": 44.3661,12 "country": "IQ"13 },14 "delivery_location": {15 "full_address": "Basra Customer",16 "lat": 30.5081,17 "long": 47.7835,18 "country": "IQ"19 },20 "pickup_warehouse_id": null,21 "dropoff_warehouse_id": null,22 "delivery_service_id": "uuid",23 "customer_id": "uuid",24 "payer_type": "sender",25 "payment_method": "wallet",26 "unit_size": 1,27 "notes": "Optional notes",28 "cod_amount": 0,29 "external_reference_id": "ORD-12345",30 "scheduled_pickup_time": "2025-09-30T10:00:00Z",31 "scheduled_delivery_time": "2025-09-30T12:00:00Z",32 "receiver_user_id": "uuid",33 "priority": 1,34 "package_details": {35 "special_instructions": "Handle with care",36 "items": [37 {38 "name": "Mobile phone",39 "description": "Mobile phone",40 "quantity": 1,41 "weight": 2.5,42 "length": 30,43 "width": 20,44 "height": 1545 }46 ]47 }48}'Response examples
1{2 "success": true,3 "message": "Delivery created successfully",4 "delivery": {5 "id": "uuid",6 "carrier_type": "internal",7 "status": "pending"8 }9}Get Delivery Receipt
/delivery/deliveries/:deliveryId/receiptReturns the primary signed receipt URL for a delivery.
Access
tenant admin / dispatcher with delivery module access
delivery owner customer
Resolution priority
COD_COLLECTIONreceiptDELIVERY_FEEreceipt
Path parameters
| Parameter | Type | Description |
|---|---|---|
deliveryIdRequired | string | ID of the delivery whose receipt should be retrieved Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/deliveries/${DELIVERYID}/receipt?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "data": {4 "delivery_id": "uuid",5 "payment_purpose": "COD_COLLECTION",6 "receipt_status": "success",7 "receipt_generated_at": "2026-04-04T12:00:00.000Z",8 "expires_in": 3600,9 "url": "https://example.supabase.co/storage/v1/object/sign/documents/delivery-receipts/uuid.pdf?..."10 }11}Update Delivery
/delivery/deliveries/:deliveryIdUnit pricing contract
fully measurable package_details override a simultaneously submitted unit_size; automatic fee recalculation runs when the resolved unit size changes or an endpoint actually changes. If any item has a zero, missing, or empty weight/length/width/height value, the package is treated as unmeasurable and the submitted unit is used. Negative or non-numeric measurements remain invalid. If neither measurable package details nor unit size is submitted, the existing saved unit is retained. A successful real edit appends one order_edited entry to the canonical delivery's order_events; status_updates remains status-only. When a promoted order's destination changes, its linked Transfer Manifest item is updated in the same database transaction so the Manifest Items table and order stay aligned.
Update the editable fields of a delivery order.
Authorization
Requires
manage:operations:tenant,manage:module:delivery, or system-admin access.Customer ownership, delivery service, status, and internal
delivery_floware immutable.Use the public boolean
is_replacementto enable or disable replacement handling.Status is not accepted here; use
POST /delivery/deliveries/:deliveryId/status.
Editable until final delivery
pickup_warehouse_idordropoff_warehouse_idpickup_location_idorpickup_locationdelivery_location_idordelivery_locationexternal_reference_id(string ornull; trimmed, and an empty string ornullclears it)cod_amount(minimum 0)payer_type(sender | receiver)payment_method(wallet | cash | cod;codis stored ascash)delivery_fee(minimum 0)unit_size(integer, minimum 1)is_replacement(boolean)notes(string ornull; send an empty string ornullto clear it)package_details, includingpackage_details.special_instructions(string handling instructions) and replacement semantics foritems[]; each item requires a name, a positive integer quantity, non-negative numeric dimensions, and a valid UUID whenidis supplied
External references keep the tenant delivery_external_reference.unique_per_customer rule. When enabled (default), another root order for the same customer cannot use the same non-empty value; the API returns HTTP 400 with a localized duplicate-reference message and field=external_reference_id.
A non-null warehouse ID takes priority over the matching address ID/JSON, resolves the tenant warehouse address, and becomes the endpoint. Send the warehouse field as null with a replacement address to switch back to a normal address.
If an address, warehouse, or unit size changes, the backend validates the fixed delivery service and its zones, recalculates the fee, and uses an explicitly supplied delivery_fee as the override. Saved address IDs must belong to the order customer or tenant. Address JSON must contain valid coordinates and the tenant domestic country. A missing, cross-tenant, or address-less warehouse fails before the atomic update.
Unchanged endpoint submissions and contact-only location updates do not trigger automatic fee recalculation when the effective address and warehouse IDs and every normalized non-contact location field match the saved endpoint. Any extra submitted location field must also match its saved value. These saves preserve the saved non-contact snapshot, including omitted legacy fields, and still run the existing address resolution, coordinate/country validation, atomic assignment checks, and finance rules. Real endpoint or resolved unit-size changes still run service/zone pricing. An explicitly supplied delivery_fee remains supported under the existing rules. Transfer Manifest destination changes continue to run pricing.
Order and P&D behavior
A child delivery ID resolves to its logical P&D parent.
Pickup address/warehouse changes propagate to the inbound leg; drop-off address/warehouse changes propagate to the outbound leg.
COD stays on the parent/outbound leg. Delivery fee stays on the parent and moves to the sender-paid inbound leg or receiver-paid outbound leg.
Unit size, payment terms, notes, special instructions, external reference, and package snapshot propagate to both operational legs.
is_replacementupdates the logical parent only; operational children read the parent's replacement state.An inbound child that is already delivered to the warehouse does not lock the order. The final parent/outbound
deliveredstate locks every editable field.If an active assigned driver cannot serve either new zone, the whole update fails with HTTP 409 and nothing changes.
If a larger unit size exceeds the assigned vehicle capacity, the whole update fails with HTTP 409.
Replacement behavior
is_replacement: truemaps the logical order todelivery_flow = replacement_forward;falseremoves that classification.The toggle is editable until final delivery. Final completion and return creation recheck the same state atomically.
Enabling replacement while the final payment method is cash remains subject to
allow_cod_on_replacement; a forbidden change returns HTTP 400cod_not_allowed_for_replacement_orders.Disabling replacement after its return exists returns HTTP 409
replacement_return_already_exists.A replacement return is a separate logical order and cannot be converted; submitting
is_replacementon it returns HTTP 400replacement_return_type_not_editable.A replacement forward follows normal direct/P&D update rules. Its latest addresses, package, unit size, and fixed service are used when the return is generated.
Return addresses, package details, notes, special instructions, unit size, payer type, and payment method remain editable until the return is delivered. Direct and staged return assignments receive the same zone and vehicle-capacity validation as normal orders.
Every replacement return remains zero-fee and zero-COD. Non-zero values return HTTP 400
replacement_return_delivery_fee_must_be_zeroorreplacement_return_cod_must_be_zero.
Finance
Draft wallet orders resize or move their reservation atomically as fee, payer, or method changes. No wallet transaction or payment is created until Execute; changing away from wallet releases the hold.
Non-draft sender wallet: refund the full previously settled fee first, then charge the full updated fee. These are two visible wallet transaction lines when both amounts are non-zero.
Pending sender cash to wallet: charge the full current delivery fee.
Pending sender wallet to cash: refund the full settled wallet fee and leave the fee pending for pickup collection.
Sender cash before pickup: leave the fee pending for pickup collection.
Sender terms changed after pickup: refund any full previous wallet settlement, then charge the full corrected fee through the sender wallet while preserving historical physical-cash rows.
Receiver cash/wallet: refund any settled sender fee and create no receiver payment during this update; the current fee becomes the future final-delivery obligation.
COD updates only change the future goods-collection amount; no COD collection, payment, or wallet entry is created before final delivery.
Successful delivery-fee payment rows are the settled-money source of truth; the delivery payment-status flag does not fabricate a wallet settlement.
Refund, replacement charge, payment audit, and delivery changes commit or roll back together.
Replacement-return payer or method changes move no money because its fee remains zero.
insufficient_available_balanceor insufficient wallet balance returns HTTP 409 and rolls back the entire update. A missing customer wallet returns HTTP 500.
Atomicity and webhook
Parent external reference, notes, special instructions, P&D legs, driver capacity, payment/wallet adjustments, package items, and replacement toggle commit or roll back together.
A concurrent write returns HTTP 409
delivery_update_conflict.External-reference-only, notes-only,
package_details.special_instructions-only, and replacement-only updates do not emitorder_update; existing webhook tracking remains status, COD, price, payer side, and payment method.One request emits at most one logical
order_updatewebhook when tracked fields change. Child synchronization does not emit duplicates.
Order history (order_events)
status_updatesremains the delivery tracking/status timeline used by customer and driver experiences; edit events are never written there.The canonical/root delivery stores
order_events, a complete internal history containing the same created/status events as its canonicalstatus_updates,order_editedevents, and projected operational-child driver activity (driver_unassigned,driver_switched,driver_handoff_pending, anddriver_handoff_completed). Child driver events retain their original payload and adddelivery_id,delivery_code,direction,leg_sequence, andsource=active_leg. Existing canonical orders are backfilled fromstatus_updates; future canonical status events are mirrored without changing their payload.Every successful edit appends at most one
order_editedevent inside the same transaction; a failed update or no-op save records nothing. A child inbound/outbound ID resolves to the logical parent, so P&D propagation never creates duplicate events.The event compares persisted before/after values, capturing normalized addresses and backend-recalculated fees. Shape:
{"event_type":"order_edited","timestamp":"2026-08-06T17:30:00Z","actor_id":"user-uuid","actor_name":"Dispatcher Name","actor_role":"dispatcher","changes":[{"field":"pickup_address","old_value":"Old Street","new_value":"New Street"},{"field":"delivery_fee","old_value":2500,"new_value":5000}],"metadata":{"source":"delivery_update_api"}}.changes[]uses this stable field order:pickup_address,delivery_address,delivery_fee,cod_amount,payer_type,payment_method,unit_size,package_details,special_instructions,notes,external_reference_id,is_replacement. Address values are display-friendly text, not internal location JSON.The internal update response returns the canonical parent delivery with
delivery.order_events. Existingsystem_eventswebhook behavior remains separate and unchanged.
Frontend integration contract
When a warehouse is selected, send its non-null
pickup_warehouse_idordropoff_warehouse_id; matching address input is ignored. To deselect it, send the warehouse field asnulltogether with the replacement address.Send only fields the user actually changed and use
is_replacementfor the toggle; never senddelivery_flow. Send special instructions only aspackage_details.special_instructions; top-levelspecial_instructionsis rejected.Use the returned canonical parent
delivery;delivery_flow = replacement_forwardmeans the toggle is on.Show the toggle until final delivery, except for
delivery_flow = replacement_return, where it is read-only.For
delivery_flow = replacement_return, keepdelivery_feeandcod_amountread-only at zero.Handle HTTP 400 with
field=external_reference_idas a duplicate external reference, plusinvalid_package_details,cod_not_allowed_for_replacement_orders, andreplacement_return_type_not_editable.Handle HTTP 409
delivery_already_delivered,replacement_return_already_exists, driver-zone/capacity conflicts,insufficient_available_balance,insufficient_balance, anddelivery_update_conflict.On a conflict, keep the previous local order and reload before retrying.
Conditional edit reason
The tenant setting
delivery.delivery_order.settings.edit.reason_requireddefaults tofalse.reasonis accepted as an optional string, trimmed by the backend, and limited to 500 characters.When the setting is enabled, tenant-admin/dispatcher edits require a non-blank
reason; missing or blank input returns HTTP 400delivery_edit_reason_required.External customer edits remain unchanged.
A successful real edit stores the normalized reason on the same canonical
order_editedevent. Failed and no-op updates do not create an event.
Frontend integration contract
Read
tenantSettings.delivery.delivery_order.settings.edit.reason_requiredfrom Tenant Settings.When true, require Reason before enabling Save and send it as top-level
reason.Handle HTTP 400 codes
delivery_edit_reason_required,delivery_edit_reason_must_be_text, anddelivery_edit_reason_too_long; display the localizedmessage.
Path parameters
| Parameter | Type | Description |
|---|---|---|
deliveryIdRequired | string | ID of the delivery to update Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
pickup_warehouse_id | string | — |
pickup_location_id | string | — |
dropoff_warehouse_id | null | — |
delivery_location | object | — |
delivery_location.full_address | string | — |
delivery_location.latitude | number | — |
delivery_location.longitude | number | — |
delivery_location.city | string | — |
delivery_location.country | string | — |
external_reference_id | string | — |
cod_amount | number | — |
payer_type | string | — |
Show 17 more body fields
Additional body parameters
| Parameter | Type | Description |
|---|---|---|
payment_method | string | — |
delivery_fee | number | — |
unit_size | number | — |
is_replacement | boolean | — |
package_details | object | — |
package_details.special_instructions | string | — |
package_details.items | array<object> | — |
package_details.items.id | string | — |
package_details.items.name | string | — |
package_details.items.description | string | — |
package_details.items.quantity | number | — |
package_details.items.weight | number | — |
package_details.items.length | number | — |
package_details.items.width | number | — |
package_details.items.height | number | — |
notes | string | — |
reason | string | — |
1curl --globoff --request PUT "https://staging-api.ondi.io/functions/v1/delivery/deliveries/${DELIVERYID}?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "pickup_warehouse_id": "pickup-warehouse-uuid",6 "pickup_location_id": "pickup-address-uuid",7 "dropoff_warehouse_id": null,8 "delivery_location": {9 "full_address": "Updated receiver address",10 "latitude": 36.1901,11 "longitude": 44.0091,12 "city": "Erbil",13 "country": "Iraq"14 },15 "external_reference_id": "CUSTOMER-ORDER-456",16 "cod_amount": 25000,17 "payer_type": "receiver",18 "payment_method": "cod",19 "delivery_fee": 7500,20 "unit_size": 3,21 "is_replacement": true,22 "package_details": {23 "special_instructions": "Keep upright and avoid heat",24 "items": [25 {26 "id": "package-item-uuid",27 "name": "Updated item name",28 "description": "Updated description",29 "quantity": 2,30 "weight": 3,31 "length": 35,32 "width": 25,33 "height": 2034 }35 ]36 },37 "notes": "Call the receiver before delivery",38 "reason": "Receiver requested an address correction"39}'Response examples
1{2 "success": true,3 "message": "Delivery updated successfully",4 "delivery": {5 "id": "parent-delivery-uuid",6 "status": "in_transit",7 "delivery_flow": "replacement_forward",8 "external_reference_id": "CUSTOMER-ORDER-456",9 "pickup_warehouse_id": "pickup-warehouse-uuid",10 "dropoff_warehouse_id": null,11 "pickup_location_id": "pickup-address-uuid",12 "delivery_location": {13 "full_address": "Updated receiver address",14 "latitude": 36.1901,15 "longitude": 44.009116 },17 "cod_amount": 25000,18 "payer_type": "receiver",19 "payment_method": "cash",20 "delivery_fee": 7500,21 "unit_size": 3,22 "package_details": {23 "items": [24 {25 "id": "package-item-uuid",26 "name": "Updated item name",27 "quantity": 228 }29 ]30 },31 "updated_at": "2026-07-21T12:00:00Z",32 "notes": "Call the receiver before delivery"33 }34}Update Delivery Status
/delivery/deliveries/:deliveryId/statusUpdate delivery status. Dispatchers retain all existing manual targets. The currently assigned driver may call this same endpoint only with target_delivery_status=returned, a nonblank reason, and optional notes. An open delivery exception or Waybill missing_on_receive discrepancy returns HTTP 409 delivery_has_open_exception; a missing receipt must be resolved as found or lost through the dedicated Waybill discrepancy endpoint. Direct deliveries finish Returned at their pickup. When a P&D inbound is returned to its pickup origin, the inbound leg and parent become Returned and every unfinished outbound leg becomes Cancelled because it never carried the parcel. Any active outbound assignment is cancelled, its capacity is released, and its driver receives the normal cancellation notification. P&D outbound and operational-return deliveries create a warehouse receiving handoff and return return_handoff; they are received through the existing receiving endpoint. A P&D outbound returned without an exception becomes returned and cannot be assigned until normal warehouse receipt changes it to pending; its parent becomes arrived_at_warehouse. That driver return appends the next delivery_attempt_failed event using the submitted reason; operational returns are not counted as customer-delivery attempts. If the direct delivery or any child of a P&D parent has return_status=return_approved|return_in_transit, a Delivered attempt returns HTTP 409 with code=return_required and error=return_required. When a customer delivery becomes delivered, every linked parcel leaves warehouse stock in the same transaction: positive stock becomes zero/depleted, each removal records a pick movement, and parcel custody becomes customer. Warehouse-dropoff deliveries and non-delivered parcels are unchanged.
Update delivery status manually from the dispatcher/admin panel.
Allowed target statuses
draft, pending, assigned, en_route_to_pickup, picked_up, in_transit, delivered, staged_at_warehouse, cancelled, and returned.
Request fields
target_delivery_status: the status selected by the operator.target_delivery_status=assignedrespectsrequire_accept_or_reject_for_assignments: when enabled, the active assignment and delivery remainpendinguntil the driver accepts, and the success response includesdriver_acceptance_required=true. Later operational targets (en_route_to_pickup,picked_up, andin_transit) deliberately bypass acceptance and proceed immediately.deliveredis accepted only when the direct delivery, or the resolved active child of a staged/P&D parent, is currentlypicked_uporin_transit.reason: optional when a dispatcher sendstarget_delivery_status=pending. It is required and nonblank for every other dispatcher/admin target and when the assigned driver sendstarget_delivery_status=returned. Missing or blank values return HTTP 400reason_requiredwithfield=reason. Other driver-action endpoints, automatic transitions, warehouse receiving, Waybills, and external draft confirmation keep their existing contracts.
Draft wallet behavior
drafttopendingcaptures the reserved fee and creates the single delivery-fee wallet transaction/payment in the same database transaction as the status change.Cancelling or returning an unpaid draft releases the reserved fee to usable wallet balance without creating a refund transaction.
A positive-fee wallet delivery already paid cannot be moved back to draft; HTTP 409 returns
paid_wallet_delivery_cannot_return_to_draft.Reservation conflicts return localized
insufficient_available_balance,payer_wallet_not_found, or reservation consistency errors. Currency is not part of the reservation response.
Terminal status behavior
cancelledandreturnedresolve a staged parent or operational child ID to the same logical delivery family.Both statuses use the original service effective payment policy.
refund_delivery_fee_on_cancelcontrols cancellation andrefund_delivery_fee_on_returncontrols return; defaults aretrueandfalserespectively. When allowed, the collected fee is credited to the sender wallet once and matching fee payments becomeREFUNDED. When disallowed, the settled fee is retained and remains paid. Collected COD keeps its existing independent terminal credit behavior.cancelledwrites every active assignment tocancelled, releases driver capacity, removes realtime assignment cards, and blocks every later driver action. No return handoff is created.returnedwrites the parent plus every non-final operational leg and active assignment toreturned, releases driver capacity, removes realtime assignment cards, and closes unfinished receiving, picking, and package-handoff work. Alreadydelivered,cancelled, orreturnedchild legs are preserved as final history; for example, a delivered P&D inbound leg is not rewritten when the parent/outbound leg is returned.Cancelled and returned deliveries are history-only for drivers and are available only through
GET /delivery/driver-deliveries?status=cancelledandstatus=returnedrespectively.A successful cancelled or returned response includes
reversalwith fee/COD amounts, refund flags, wallet transaction IDs, and the sender user ID.
Manual status behavior
A successful response normally guarantees
delivery.statusequalstarget_delivery_status, except anassignedrequest that requires driver acceptance returnsdelivery.status=pendingwithdriver_acceptance_required=true. For a staged/P&D parent whose active child is outbound, manualassigned,en_route_to_pickup, andpicked_upadvance only that child; the returned parentdelivery.statuskeeps its current warehouse lifecycle status (normallystaged_at_warehouse).For a staged/P&D parent, manual
deliveredfirst resolves the active child and requires that child to bepicked_uporin_transit. Once eligible, it completes every unfinished child insequence_planorder before finalizing the parent. Children already delivered remain unchanged; cancelled or returned children fail with HTTP 409staged_child_not_completable.Each child keeps its existing active assignment. When a child has no active assignment, the backend uses request
driver_id; when it is omitted, it reuses the first available driver from the ordered child assignments. If neither is available, the request fails with HTTP 400driver_id_required_for_delivery_status.Missing child assignments reuse the existing driver, zone, team, vehicle, and capacity validation. A draft future child can be assigned only through this protected dispatcher completion path and is completed immediately.
Every unfinished child follows the existing proof, payment, COD, earning, notification, package, capacity, and replacement workflow. Request proof and returned-item outcomes apply to the final unfinished child.
Child-to-parent warehouse projection is suppressed only for tagged dispatcher completion events. The parent is finalized with the last unfinished child, so skipped warehouse states are not recorded as the selected parent result. A retry also repairs an unfinished child when the parent is already delivered.
For other assignment-required staged targets (
assigned,en_route_to_pickup,picked_up, orin_transit), the backend continues resolving only the first active non-terminal child. Outboundassigned,en_route_to_pickup, andpicked_upare child-only preparation states; outboundin_transitadvances both child and parent.When a manual status changes both parent and child, both Events include the dispatcher actor and reason and metadata identifies
source=dispatcher_manual_status_override. For outbound child-only preparation states, only the child status event is written; no parent status event or manual parent override is created.
Warehouse staging action
target_delivery_status=staged_at_warehouseremains the dedicated P&D warehouse-receive operation. When the inbound child lacks an active assignment, requestdriver_idis used first. If it is omitted, a parent created from a Transfer Manifest (bothsource_transfer_manifest_idandsource_transfer_manifest_item_id) may useassignment_change_settings.default_driver_idonly whentransfer_manifest.allow_warehouse_staging_without_assigned_driver=true. Toggle Off, a missing fallback, or a normal/non-G2G order returnsdriver_id_required_for_unassigned_delivery. Deleted or cross-tenant fallback drivers returndriver_not_found; inactive or unavailable drivers returndriver_not_active; zone mismatches keep the existing zone errors. Optionalbin_idmust be active and belong to the inbound warehouse; otherwise the default receiving location is used. Assignment creation, inbound completion, inventory receipt, and parent staging remain atomic and idempotent.
Other business rules
Dispatcher/admin targets require
manage:operations:tenantormanage:module:delivery. The currently assigned driver is also authorized, but only fortarget_delivery_status=returned; any other target returns HTTP 403insufficient_permissions.For non-Delivered assignment-required targets with no active assignment,
driver_idis used first, thenassignment_change_settings.default_driver_id; otherwise the API returnsdriver_id_required_for_delivery_status.proof_of_deliveryremains optional for dispatcher Delivered.A new Delivered request from
draft,pending,assigned,en_route_to_pickup,arrived_at_warehouse,staged_at_warehouse, or another status outsidepicked_upandin_transitreturns HTTP 409 withcode=invalid_status_transitionand the localized plain-languagemessageOrder status does not allow this actionin English. Eligibility is checked before driver assignment, capacity, payment, audit, or completion side effects.Delivered keeps the existing idempotent COD/payment and driver-earning behavior for every eligible child completed by the request.
Open delivery exceptions, active Waybills, driver-zone rules, and non-internal-delivery guards remain unchanged.
Frontend integration contract
Driver app: call this endpoint only for whole-delivery Return with
{ "target_delivery_status": "returned", "reason": "...", "notes": "..." }; do not send parcel IDs orreturned_items.Dispatcher panel: when the selected target is
pending, Reason may be omitted or blank. For every other target, require and trim Reason and do not enable Save when it is blank.Send the visible parent delivery ID for staged orders. For manual Delivered, send the operator-selected
driver_idwhen available; the backend uses it only after the delivery passes thepicked_up/in_transiteligibility check and only for child legs missing an active assignment.Enable or show Delivered only when the direct delivery or resolved active P&D child is
picked_uporin_transit; backend validation remains authoritative.Handle HTTP 400
reason_requiredanddriver_id_required_for_delivery_status, plus HTTP 409invalid_status_transition,insufficient_available_balance,paid_wallet_delivery_cannot_return_to_draft,staged_child_not_completableand existing driver eligibility/capacity errors. Forinvalid_status_transition, display the returned localizedmessageand keep the current order status.On success, replace the local parent order with
delivery. When a staged parent's active child is outbound and the target isassigned,en_route_to_pickup, orpicked_up, keep the returned parent warehouse status (normallystaged_at_warehouse) instead of displaying the child target on the parent. Fortarget_delivery_status=assigned, keep the returned status whendriver_acceptance_required=true; do not display the child as Assigned until the driver accepts. Outboundin_transitanddeliveredadvance the parent normally. On terminal success, usereversalfor audit/display. No navigation action is returned.
P&D Pickup
For target_delivery_status=picked_up on a lifecycle-managed P&D/operational-return child, Pickup is one atomic custody boundary. Existing completed picking, current assignment/handoff, clear delivery and warehouse-item exceptions, and reserved warehouse stock are required. Success records one pick movement, removes stock/reservation, marks item custody in_transit, completes the handoff, marks the picking order handed_off, and advances delivery/assignment together. Missing preparation is not created here. Waybill and ordinary pickup behavior is unchanged. Frontend sends the existing status payload and must not separately complete the P&D handoff or mark its picking order handed off.
Path parameters
| Parameter | Type | Description |
|---|---|---|
deliveryIdRequired | string | ID of the delivery to update status for Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
target_delivery_status | string | — |
reason | string | — |
notes | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/deliveries/${DELIVERYID}/status?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "target_delivery_status": "returned",6 "reason": "Dispatcher instructed return",7 "notes": "Parcel handed back at the active leg origin"8}'Response examples
1{2 "success": true,3 "message": "Delivery status updated successfully",4 "delivery": {5 "id": "uuid",6 "status": "delivered",7 "delivery_code": 15338 },9 "driver_acceptance_considered": true,10 "driver_acceptance_message": "Driver acceptance was considered automatically for this status update"11}Cancel Delivery
/delivery/deliveries/:deliveryId/cancelCancel a delivery order. This endpoint:
Updates the logical delivery order status to
cancelledReverses delivery financials to the sender wallet
Cancels assignments that do not hold the parcel
Preserves picked-up driver/assignment history for audit
Sends notifications to the customer and any affected driver
Business Rules
Allowed: tenant admin (
manage:operations:tenant), delivery module manager (manage:module:delivery), or order owner (customer_id = authenticated user)A delivered, returned, failed, rejected, or already-cancelled logical order cannot be cancelled; final outbound delivery also blocks cancellation with HTTP 409
cannot_cancel_delivery_in_terminal_statusMerchant/customer cancellation is transactionally blocked after any logical-order assignment becomes active with HTTP 409
cannot_cancel_delivery_after_driver_assignmentTenant admin/dispatcher can cancel assigned, picked-up, or in-transit deliveries
Assignments not in
picked_uporen_route_to_deliverybecomecancelledand release capacity immediatelyA
picked_uporen_route_to_deliveryassignment keeps its custody status, driver, and capacity for audit; the delivery and every leg are still cancelled and all normal actions are blockedCancelled and returned deliveries are hidden from the default driver task list and are returned only from explicit
status=cancelledorstatus=returnedfiltersCancellation credits the collected delivery fee to the sender wallet only when the original service effective
refund_delivery_fee_on_cancelpolicy allows it; the default istrueSuccessful
DELIVERY_FEEpayments are resolved across the logical parent and operational children. The first policy decision is persisted per logical root so retries cannot change the result after a policy editCollected COD is resolved across the same logical family and refunded using
cod_collections.allocations.cod; driver cash liability/remittance state is not voided or reversedMatching family
DELIVERY_FEEpayments are updated toREFUNDEDonly when the cancel policy allows a fee refund. A no-refund fee remains successful and paid. COD remains independent, and a COD-only credit never marks the delivery fee as refundedA wallet-paid draft has no collected delivery fee yet: cancellation releases its reservation to usable balance without a refund wallet transaction. Other unpaid and uncollected amounts are not credited, and an unpaid cancellation does not require a sender wallet
Replacement-return orders are separate logical orders and are never included as staged children
Staged deliveries: submitting the parent or any operational child resolves to the same logical parent. A completed inbound warehouse leg does not block cancellation while the parent/final outbound leg remains cancellable. The transaction cancels:
The parent plus every non-terminal operational child; terminal child history is preserved
Receiving order (
receiving_orderswithexternal_reference = 'DEL' || delivery_code, status not inreceived,partially_received,received_with_exceptions,cancelled)Package handoffs (
package_handoffswithdelivery_idmatching an operational child, status not incompleted,cancelled)Picking orders linked to those handoffs (status not in
handed_off,cancelled)
Driver notification
The assigned/custody driver is captured before cancellation changes assignment data and receives one delivery_cancelled notification per affected operational delivery. It uses priority=critical, display_mode=modal, requires_acknowledgement=true, and portal_context=driver_app. In-app availability remains status=sent even when push fails; the push result is recorded in channel_status.push. Notification insertion, rendering, and push delivery run outside this API response path.
Success contract
delivery.idis the canonical logical root ID, including when the request used a staged child IDreversal.delivery_fee_amountis the actual successful family fee refunded
Frontend integration contract
Request body is optional; both
failure_reasonandnotesare optional.Handle HTTP 409
cannot_cancel_delivery_after_driver_assignmentandcannot_cancel_delivery_in_terminal_status.Admin/dispatcher does not need to unassign first.
Use returned
delivery.idas the canonical order ID.For staged deliveries, receiving, handoff, and eligible picking work is cancelled atomically.
Cancelled/returned parent and child rows keep the historical assigned driver in
driverandassignmentfor dispatcher/admin list and detail display.The driver app retrieves the persistent cancellation through the messaging notification API and acknowledges it only from the explicit “I understand” action.
Path parameters
| Parameter | Type | Description |
|---|---|---|
deliveryIdRequired | string | ID of the delivery to cancel Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
failure_reasonOptional | string | (string): stored in |
notesOptional | string | (string): stored in the status timeline |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/deliveries/${DELIVERYID}/cancel?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "failure_reason": "Customer changed mind",6 "notes": "Order cancelled per customer request"7}'Response examples
1{2 "success": true,3 "message": "Delivery cancelled successfully",4 "delivery": {5 "id": "uuid",6 "status": "cancelled"7 },8 "reversal": {9 "delivery_fee_refunded": true,10 "cod_refunded": true,11 "delivery_fee_amount": 5000,12 "cod_amount": 25000,13 "delivery_fee_wallet_transaction_id": "wallet-transaction-uuid",14 "cod_wallet_transaction_id": "wallet-transaction-uuid",15 "refund_recipient": "sender",16 "sender_user_id": "sender-user-uuid"17 }18}Delete Delivery
/delivery/deliveries/:deliveryIdLegacy admin cancellation endpoint. The delivery record is retained; this endpoint now delegates to the same atomic logical-order cancellation flow as POST /delivery/deliveries/{deliveryId}/cancel instead of updating status directly. The parent or an operational child identifier resolves to the canonical root. Parent and eligible children are cancelled together and active work is closed. Collected delivery fees follow the original service cancel-refund policy; collected COD keeps its existing independent terminal credit behavior. Separate replacement-return orders are excluded. Tenant-admin permission is required.
Responses
200 success, 403 unauthorized, 404 delivery not found, 409 terminal logical-order conflict. Tenant admins may cancel an assigned order through this endpoint.
Frontend integration contract
treat this as cancellation, not record deletion. Remove no record solely because the request succeeded; use the normal cancelled-order presentation. Prefer the explicit POST cancellation endpoint for new integrations.
Path parameters
| Parameter | Type | Description |
|---|---|---|
deliveryIdRequired | string | ID of the delivery to delete Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request DELETE "https://staging-api.ondi.io/functions/v1/delivery/deliveries/${DELIVERYID}?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Delivery deleted successfully"4}Create Bulk Deliveries
/delivery/deliveries/bulkCreate multiple deliveries in bulk. Only admin users can create bulk deliveries. All deliveries in the batch must have the same bulk_group value. Each row uses measurable item data or its submitted unit_size; otherwise the effective general.unit_size.settings.missing_unit_size_fallback is applied. The setting is loaded once for the batch.
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
pickup_location | object | — |
pickup_location.full_address | string | — |
pickup_location.lat | number | — |
pickup_location.long | number | — |
pickup_location.country | string | — |
delivery_location | object | — |
delivery_location.full_address | string | — |
delivery_location.lat | number | — |
delivery_location.long | number | — |
delivery_location.country | string | — |
package_details | object | — |
package_details.special_instructions | string | — |
Show 14 more body fields
Additional body parameters
| Parameter | Type | Description |
|---|---|---|
package_details.items | array<object> | — |
package_details.items.name | string | — |
package_details.items.description | string | — |
package_details.items.quantity | number | — |
package_details.items.weight | number | — |
package_details.items.length | number | — |
package_details.items.width | number | — |
package_details.items.height | number | — |
customer_id | string | — |
status | string | — |
scheduled_pickup_time | string | — |
scheduled_delivery_time | string | — |
priority | number | — |
bulk_group | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/deliveries/bulk?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '[5 {6 "pickup_location": {7 "full_address": "Baghdad HQ",8 "lat": 33.3152,9 "long": 44.3661,10 "country": "IQ"11 },12 "delivery_location": {13 "full_address": "Basra Customer",14 "lat": 30.5081,15 "long": 47.7835,16 "country": "IQ"17 },18 "package_details": {19 "special_instructions": "Handle with care",20 "items": [21 {22 "name": "Book",23 "description": "Book",24 "quantity": 1,25 "weight": 1.5,26 "length": 20,27 "width": 15,28 "height": 1029 }30 ]31 },32 "customer_id": "uuid",33 "status": "pending",34 "scheduled_pickup_time": "2023-08-02T10:00:00Z",35 "scheduled_delivery_time": "2023-08-02T14:00:00Z",36 "priority": 1,37 "bulk_group": "batch-2023-08-01"38 },39 {40 "pickup_location": {41 "full_address": "Baghdad HQ 2",42 "lat": 33.3152,43 "long": 44.3661,44 "country": "IQ"45 },46 "delivery_location": {47 "full_address": "Basra Customer 2",48 "lat": 30.5081,49 "long": 47.7835,50 "country": "IQ"51 },52 "package_details": {53 "special_instructions": "Handle with care",54 "items": [55 {56 "name": "Electronics",57 "description": "Electronics",58 "quantity": 2,59 "weight": 2.5,60 "length": 30,61 "width": 20,62 "height": 1563 }64 ]65 },66 "customer_id": "uuid",67 "status": "pending",68 "scheduled_pickup_time": "2023-08-02T11:00:00Z",69 "scheduled_delivery_time": "2023-08-02T15:00:00Z",70 "priority": 1,71 "bulk_group": "batch-2023-08-01"72 }73]'Response examples
1{2 "success": true,3 "message": "Bulk deliveries created successfully",4 "bulk_group_id": "uuid",5 "delivery_ids": [6 "uuid-1",7 "uuid-2"8 ],9 "tracking_numbers": [10 "TRK123456",11 "TRK789012"12 ]13}Handle Delivery Action
/delivery/action/:deliveryIdHandle ordinary forward driver transitions, parcel pickup confirmation, and existing partial-package outcomes. For pickup, send assignment_status=en_route_to_pickup and not_received_item_ids; an empty array records a full pickup. The inbound child records leg outcomes while accepted parcel parent status remains pending; partial package returns never refund, and an all-package return uses the existing whole-delivery Return authority. Conflicting retries return package_item_outcome_conflict. An open delivery exception or Waybill missing_on_receive discrepancy returns HTTP 409 delivery_has_open_exception. A missing receipt remains blocked until destination staff resolve it as found or lost through the dedicated Waybill discrepancy endpoint. After Retry, this endpoint works normally. After Return, the delivery card has returnStatus=return_approved; this endpoint returns HTTP 409 return_required and cannot mark the parcel Delivered. The driver must use POST /delivery/deliveries/:deliveryId/status with target_delivery_status=returned. For a returned P&D inbound, the backend returns the inbound and parent and cancels the unused outbound. This endpoint does not query resolved exceptions. A successful customer-delivery completion removes every linked parcel from warehouse stock and records the movement in the same transaction; no additional frontend stock mutation is required.
Handle delivery status transitions by drivers. Only drivers can perform actions on their assigned deliveries.
Terminal guards
A returned delivery, or any leg whose logical parent is returned, rejects every driver transition with HTTP 409
error=delivery_terminal_no_actionsand a localized plain-languagemessage.A cancelled delivery, or any leg whose logical parent is cancelled, rejects every driver transition with HTTP 409
error=cannot_perform_action_on_cancelled_delivery. No return-handoff action is available.A driver switch transfers operational custody to the replacement driver immediately, leaves
handoff_statusnull, and does not block the replacement driver's next status action.These rules are enforced in the database transaction, so stale screens cannot bypass terminal or custody changes.
Supported Assignment Statuses
assigned/accepted→en_route_to_pickup(delivery status also transitions toen_route_to_pickup)en_route_to_pickup→picked_uppicked_up→en_route_to_delivery(delivery status transitions toin_transit)en_route_to_delivery→completed(requires proof only when policy flags require it, delivery status transitions todelivered)
Replacement Orders
When completing a direct delivery with
delivery_flow = 'replacement_forward', the response includes areplacement_returnobject.For warehouse-staged replacements, the parent has
delivery_flow = 'replacement_forward'; completing the final outbound child creates the return for that parent and includesreplacement_returnin the child completion response.Inbound/staging leg completion does not create the return.
The return reuses the replacement forward service policy. If that policy requires staging, the backend creates a return HYBRID parent and assigns its inbound customer→warehouse child to the same driver; otherwise it assigns the direct return delivery.
The return parent links to the forward through
replacement_forward_delivery_id; its operational children useparent_delivery_id.The return parent and all return legs have
delivery_fee = 0andcod_amount = 0; replacement returns never collect goods COD because the forward fee covers the round trip.The driver should display
replacement_returnimmediately. It is the assigned inbound child for staged returns and the direct return for non-staged returns.The replacement return is a completion prerequisite. If its creation fails, the action returns
409witherror = replacement_return_creation_failed; the forward delivery and assignment remain uncompleted so the driver can retry.
Failed-delivery exception workflow
An open exception blocks this endpoint with HTTP 409
code=delivery_has_open_exception; the assignment, parcel custody, and reserved capacity remain with the driver.Dispatcher
retryresolves the exception, restores the prior operational status, clears any return instruction, and lets the same assigned driver continue through this ordinary action endpoint.Dispatcher
returnresolves the exception and sets persistentreturnStatus=return_approved. The driver app must hide Delivered and show one whole-delivery Mark Returned action.Mark Returned calls
POST /delivery/deliveries/{deliveryId}/statuswith{ "target_delivery_status": "returned", "reason": "...", "notes": "..." }; it does not usereturned_itemsand does not require parcel-by-parcel selection.Normal direct and P&D inbound deliveries become Returned at the active leg's original pickup location.
P&D outbound and operational-return legs go back to their pickup warehouse and the status response contains
return_handoff. A manually returned P&D outbound becomes Returned while its parent becomes Arrived at warehouse; normal warehouse receipt later changes that same outbound to Pending. Operational-return retries keep their existing warehouse-receipt behavior.An assigned driver may also choose the same whole-delivery Returned status outside an exception.
A stale ordinary delivery action after return approval receives HTTP 409
code=return_required; it must not mark the delivery Delivered.
COD Payment Record (after completion)
If the delivery payment method is COD, the system records a successful
paymentsrow for the COD amount only after the delivery is markeddelivered.The delivery customer/merchant wallet is credited immediately at delivery completion. Driver remittance later only clears the driver cash liability and does not credit that COD payout a second time.
Driver earning
When a delivery is completed, the backend credits one idempotent
wallet_transactionsearning row for the assigned driver usingtenant_settings.driver_compensation.For staged/P&D child legs, earning is calculated from the parent delivery fee and split equally across the sequenced child legs. With the normal two-leg P&D flow, the pickup driver gets half and the delivery driver gets half.
The earning row uses
type=earning,reference_type=delivery, andreference_id=<deliveryId>.
Note
If a delivery is still in picked_up status when completing (assignment status en_route_to_delivery), the system will automatically transition the delivery to in_transit first before marking it as delivered to ensure proper status flow.
Proof Enforcement
signature_urlis required only when the active leg'sproofRequirementshassignature_required=true.photo_urlsis required only when the active leg'sproofRequirementshasphoto_required=true.Non-warehouse pickup legs use
proof_settings.driver_pickup.Warehouse pickup legs use
proof_settings.warehouse_pickup.Non-warehouse delivery legs use
proof_settings.driver_delivery.Warehouse delivery legs use
proof_settings.warehouse_delivery.notesis optional.
Location Tracking
current_lat(optional): Current latitude for location trackingcurrent_lng(optional): Current longitude for location trackingRoute calculation is queued after the status transition so Google Directions latency does not delay the action response.
delivery_assignments.route_detailsmay still contain the previous route immediately after this response and is updated asynchronously when calculation succeeds.
Hybrid Parent Status
Inbound leg status drives the parent through
assigned,en_route_to_pickup,picked_up,in_transit, andarrived_at_warehouse.Warehouse receiving sets the parent to
staged_at_warehouse; assigning the outbound leg does not regress that stage.Outbound
en_route_to_pickupandpicked_upremain child-leg-only statuses and do not change the parent fromstaged_at_warehouse. Outboundin_transitsets the parent toin_transit, and outbound completion sets it todelivered.Parent
status_updatesinheritactor_idandactor_rolefrom the child status event that caused the parent transition.
Response
Returns the updated delivery object with its current status. Pickup proof is returned as
delivery.proof_of_pickup; delivery proof is returned asdelivery.proof_of_delivery.Always includes
proofRequirements,feeToCollectAtPickup, andfeeToCollectAtDelivery, calculated from the resolved effective policy for the active delivery leg.Includes
warehouseEndpointwhen the active leg starts or ends at a warehouse.For direct replacement forwards and final outbound children of staged replacements, also returns
replacement_returnwith the auto-created active return delivery.For final outbound children of staged replacements,
delivery.delivery_flowis returned asreplacement_forwardanddelivery.replacement_parent_delivery_idcontains the staged parent id.
Frontend integration contract
Replace the local delivery with
deliveryfrom this response after each successful action.On HTTP 409
cannot_perform_action_on_cancelled_delivery, remove the item from active task state and show it only in the Cancelled tab; display the localizedmessageand expose no action.On HTTP 409
delivery_terminal_no_actions, remove the item from active task state and show it only in the Returned tab; display the localizedmessage.Read
delivery.proofRequirementsbefore deciding whether to ask for signature or photo proof.If
replacement_returnis present, insert/show it immediately as the driver's next return pickup; it is already assigned to the same driver.On
409witherror = replacement_return_creation_failed, keep the forward delivery in its current local state and allow the driver to retry the completion action; do not mark it complete locally.Continue sending the child
deliveryIdto this endpoint for staged replacement actions; usereplacement_parent_delivery_idonly for linkage/display.
Event actor contract
for a driver action, new delivery and assignment status events record the authenticated driver record ID (drivers.id) as actor_id; the backend derives it from the authenticated user and validated assignment. The request must not supply an actor ID. The authenticated user ID remains the identity used for authorization and user-owned package records.
Opt-in durable completion receipt
For assignment_status=en_route_to_delivery only, send Idempotency-Key: <UUID> with the existing completion body. JWT driver authentication is required. The server atomically claims this tenant/delivery/driver operation before calling the existing action owner; this is not a server job queue. The request body is fingerprinted with object keys sorted and array order preserved. Keep uploaded proof URLs and parcel outcomes unchanged for the operation. A changed body with the same key returns HTTP 409 idempotency_conflict.
Response adds operation_id, delivery_id, and operation_state:
completed(HTTP 200): the action succeeded, required payment work and applicable driver earning succeeded, and the complete response was durably saved. Existing delivery/replacement/return fields remain present. A matching replay returns the original receipt without executing writes. Receipts describe that operation, not the current state after later staff changes.processing(HTTP 202,success=false,code=completion_operation_processing): this exact operation is already owned by an earlier request. No second action is executed.rejected(original error HTTP status): validation failed before a completion write could begin; the saved error replays for the same key/body. A corrected submission must use a new UUID.needs_attention(HTTP 409,success=false,code=completion_operation_needs_attention): the strategy may have applied some changes or a durable receipt could not be confirmed. Do not automatically repeat the completion. Preserve all evidence and check the operation status. When available,cause.messagepreserves the original localized action error andcause.codeits safe machine code; show this detail for the affected order.
An unresolved operation also blocks a different key for the same delivery (completion_operation_already_exists, HTTP 409). Completed receipts do not prevent a legitimately reopened delivery from following normal status validation under a new key. The server never steals an old processing claim or re-executes uncertain work. When a processing receipt is older than 180 seconds, reads/replays conservatively show needs_attention; this does not cancel an original request that is still running. If that original request later saves its final receipt, subsequent reads show the final state.
Mobile integration
Keep one completion POST in flight while the driver prepares the next order. Mark success only from operation_state=completed; HTTP 202, missing/empty responses, and transport errors are not success. After a timeout or app restart, use the operation status GET. A delivery status of delivered alone does not prove every completion side effect finished. After an explicit 404 operation lookup, resending the same UUID with the exact immutable body is safe: the atomic claim grants only one owner even if the first request is still arriving. Never use a new UUID to recover an uncertain submission. For processing or needs_attention, only check status; do not repeat completion. Retain the same UUID and uploaded URLs. Legacy callers without the header retain existing behavior.
Path parameters
| Parameter | Type | Description |
|---|---|---|
deliveryIdRequired | string | ID of the delivery to perform action on Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
assignment_status | string | — |
signature_url | string | — |
photo_urls | array<string> | — |
notes | string | — |
returned_items | array<object> | — |
returned_items.id | string | — |
returned_items.reason | string | — |
returned_items.notes | string | — |
returned_items.photo_urls | array<string> | — |
current_lat | string | — |
current_lng | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/action/${DELIVERYID}?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "assignment_status": "en_route_to_delivery",6 "signature_url": "https://example.com/signatures/signature-123.png",7 "photo_urls": [8 "https://example.com/photos/photo-1.jpg"9 ],10 "notes": "Optional delivery notes",11 "returned_items": [12 {13 "id": "parcel-3-uuid",14 "reason": "damaged",15 "notes": "Outer box was crushed",16 "photo_urls": [17 "https://example.com/photos/parcel-3-damage.jpg"18 ]19 }20 ],21 "current_lat": "33.3152",22 "current_lng": "44.3661"23}'Response examples
1{2 "success": true,3 "message": "Package picked up successfully",4 "delivery": {5 "id": "delivery-uuid",6 "status": "picked_up",7 "package_details": {8 "pickup_status_summary": {9 "expected": 3,10 "received": 2,11 "not_received": 1,12 "unconfirmed": 0,13 "is_confirmed": true,14 "is_short": true15 },16 "items": [17 {18 "id": "parcel-1-uuid",19 "pickup_status": "received"20 },21 {22 "id": "parcel-2-uuid",23 "pickup_status": "received"24 },25 {26 "id": "parcel-3-uuid",27 "pickup_status": "not_received"28 }29 ]30 }31 }32}Get Driver Completion Operation
/delivery/action/{{delivery_id}}/operations/{{completion_operation_id}}Read a driver completion receipt after a lost response or app restart. JWT driver only; tenant, user, and driver ownership are checked. This read never re-runs completion and never changes delivery or money state.
HTTP 200
{success:true,data:{operation_id,delivery_id,state,http_status,result}}. state is processing, completed, rejected, or needs_attention. result is the saved action JSON, including operation fields, or null while the original request has no final receipt. http_status is the saved action HTTP status or null. Processing older than 180 seconds is presented as needs_attention without releasing the claim; a late original result can still finalize it. For completed use result as the original action response. For rejected show the saved error; for needs_attention preserve evidence and request review rather than retrying blindly.
Errors
400 invalid UUID; 401 missing JWT; 403 non-driver; 404 completion_operation_not_found for missing or unauthorized receipt; 503 completion_operation_unavailable on database failure. After an explicit 404, the mobile worker may resend the exact original completion body under the same UUID; concurrent arrivals still have one owner. Never use a different UUID for this recovery. For processing or needs_attention, only check status. Operation result is historical and is never inferred from delivery status.
Path parameters
| Parameter | Type | Description |
|---|---|---|
delivery_idRequired | string | Variable used inside the request path. |
completion_operation_idRequired | string | Variable used inside the request path. |
Query parameters
| Parameter | Type | Description |
|---|---|---|
language | string | Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/action/${DELIVERY_ID}/operations/${COMPLETION_OPERATION_ID}?language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "data": {4 "operation_id": "11111111-1111-4111-8111-111111111111",5 "delivery_id": "22222222-2222-4222-8222-222222222222",6 "state": "completed",7 "http_status": 200,8 "result": {9 "success": true,10 "operation_id": "11111111-1111-4111-8111-111111111111",11 "operation_state": "completed",12 "delivery_id": "22222222-2222-4222-8222-222222222222",13 "delivery": {14 "id": "22222222-2222-4222-8222-222222222222",15 "status": "delivered"16 }17 }18 }19}Complete Cancelled Delivery Return Handoff
/delivery/action/:deliveryIdCompletes the physical return handoff for a cancelled delivery whose assignment is still picked_up or en_route_to_delivery. Driver authentication and ownership of the assignment are required. The request must contain the literal action=complete_return_handoff; assignment_status is not required for this action. The backend atomically changes the assignment to returned, releases driver capacity exactly once, records return audit context, and leaves the logical delivery/order status cancelled. Retries after a successful handoff are idempotent. A normal driver action on the cancelled delivery returns HTTP 409 cancelled_delivery_return_only; this action on a non-cancelled delivery returns HTTP 409 return_handoff_requires_cancelled_delivery. Frontend integration: this action is not surfaced by the current driver task-list contract. The default driver list does not return returnInstruction.
Path parameters
| Parameter | Type | Description |
|---|---|---|
deliveryIdRequired | string | The cancelled operational child delivery ID returned by GET /delivery/driver-deliveries Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
action | string | — |
notes | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/action/${DELIVERYID}?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "action": "complete_return_handoff",6 "notes": "Parcel handed to warehouse staff"7}'Response examples
1{2 "success": true,3 "message": "The delivery was returned successfully",4 "delivery": {5 "id": "active-child-leg-uuid",6 "status": "cancelled"7 }8}