Skip to main content

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 & Last-Mile / Deliveries
GET/delivery/dispatcher-contacts

Returns 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 dispatcher role 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 message is localized; English for a non-driver is Only 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

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/dispatcher-contacts" \2  --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"

Response examples

200OK
Response body
json
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 & Last-Mile / Deliveries
GET/delivery/deliveries

Get a list of deliveries with filtering and pagination. Access is restricted by role.

Search

  • search matches the delivery search fields, barcode, and external_reference_id.

  • Delivery order codes match with or without the DEL prefix. Examples: DEL1911, DEL19, 1911, and 19 can all match delivery_code 1911.

  • A non-empty search returns matching orders in ANY status: the default draft/delivered exclusions are skipped and any status or column_filters.status value is ignored for that request. Clearing search restores 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 excludes draft and delivered rows.

  • A non-empty standalone assignment_status or column_filters.assignment_status skips only those default exclusions, allowing matching assignments on terminal deliveries to remain visible. It does not override explicit status, column_filters.status, or is_completed constraints. 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 standalone assignment_status=<status>; omit it when clearing the filter. No response fields or navigation behavior change.

  • When search is 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.status is a real status value (and no search), the list applies exact delivery-status matching.

  • returning is stored on the canonical original/P&D parent only after its linked operational_return reaches picked_up or in_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.status is the literal string All (and no search), the list applies no delivery-status equality filter and returns every status, including draft, 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 assignment object 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, and assignment.leg_sequence identify the active/source child leg.

  • assignment.status_updates is a merged child-assignment timeline. Each event includes source = "child_assignment", assignment_id, delivery_id, delivery_code, direction, and leg_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, and delivery.assignment.status_updates; no separate parent-specific field is required.

Exceptions

  • Each delivery row includes has_exception and exception, plus compatibility fields has_open_exception and open_exception.

  • has_exception=true means exception contains the compact snapshot of the latest open delivery_exceptions row.

  • column_filters.has_open_exception accepts boolean true/false or the strings "true"/"false". true returns only orders with a current open exception, false returns only orders without one, and omitting the key returns both. The filter is applied before pagination and counting.

  • exception includes id, exception_type, exception_label, option, status, reported_at, and updated_at.

  • option is the tenant/system exception option snapshot matched by exception_type; use it for list rendering when present.

  • Frontend integration: continue sending column_filters={"has_open_exception":"true"} for the Has Exception selection and omit has_open_exception when the filter is cleared. Show the exception indicator only when has_exception is true. Use exception.exception_label or exception.option.label for the list label. Fetch full details from GET /delivery/exceptions?delivery_id=<delivery_id>&status=open only when the user opens the exception context.

Query parameters

ParameterTypeDescription
pagestring

Page number for pagination

Example: 1

limitstring

Number of items per page

Example: 10

statusstring

Filter by delivery status. returning is the canonical original/P&D-parent status after a linked operational return is physically picked up; assignment alone keeps that order staged_at_warehouse.

searchstring

Case-insensitive partial search across delivery search fields, barcode, external_reference_id, and the contact phone saved in both pickup_location.contact_info.phone and delivery_location.contact_info.phone. Numeric input ignores spaces, punctuation, and Arabic/Persian digit glyphs without assuming a country or dial code. Longer numeric input also matches by its final seven digits so local and international representations of the same saved number can resolve to the order. Delivery order codes match with or without the DEL prefix, so DEL1911, DEL19, 1911, and 19 can all match delivery_code 1911. A non-empty search term returns matching orders in ANY status: it bypasses the default draft/delivered exclusions and ignores status and column_filters.status for the request. Frontend integration: continue sending the user's value through the existing search query parameter; the response shape is unchanged.

customer_idstring

Filter by customer ID

driver_idstring

Filter by driver ID

assignment_statusOptionalstring

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 unassigned for rows with no effective current/projected assignment. Filtering, exact count, and pagination are applied in the database before response enrichment. When non-empty, the list skips its default draft/delivered exclusions so matching assignments remain discoverable on terminal deliveries. Any explicit delivery status, column_filters.status, or is_completed constraint still applies. AND-combined with column_filters.assignment_status when both are supplied.

bulk_groupstring

Filter by bulk group identifier

delivery_typestring

Filter by delivery type (Delivery, Pickup, OnDemand, etc.)

tenant_idOptionalstring

Tenant ID (optional if in auth token)

column_filtersOptionalstring

Optional JSON string for column-level filters. Keys are AND-combined and ANDed with existing standalone filters. Supported keys: delivery_code, customer, driver, barcode, pickup_zone, dropoff_zone, pickup_address, delivery_address, pickup_warehouse, vehicle, notes, external_reference_id (ILIKE); status, return_status, delivery_flow, readiness_status, assignment_status, carrier_type, payment_status, payer_type, payment_method, direction, priority, delivery_service_id, source_transfer_manifest_id (=); has_open_exception, requires_warehouse_staging, is_ready_to_deliver, has_return_order, is_return_order, g2g_only, is_parent (boolean fields); cod_amount, delivery_fee (number or {min,max}); created_at, updated_at ({from,to}). delivery_service_id and source_transfer_manifest_id accept exact string IDs, are AND-combined with other filters, and execute in the database before exact counting and pagination. A non-empty assignment_status matches the effective assignment displayed on the row: the direct assignment for ordinary rows and the projected child-leg assignment for parent/P&D rows. Use unassigned when the row has no effective assignment. The filter bypasses the default draft/delivered exclusions, so matching terminal delivery statuses remain included; explicit delivery-status constraints still apply. Use {"assignment_status":"assigned"} or {"assignment_status":"unassigned"}. Filtering, exact count, and pagination execute in the database. g2g_only=true returns only deliveries with a transfer-manifest source, g2g_only=false returns only deliveries without a transfer-manifest source, and omitting g2g_only returns both. The G2G predicate runs in the database before exact counting and pagination. is_parent=true returns parent orders, is_parent=false returns non-parent top-level orders, and omitting is_parent preserves the normal top-level-order behavior. The exact is_parent predicate runs before counting and pagination. has_return_order=true returns original root orders with a non-null return_delivery_id; use it for the P&D Has return order filter. is_return_order=true returns return-generated orders with a non-null original_delivery_id; use it for the pickup/return order list. Both predicates execute before count and pagination and use the maintained delivery projection. When either filter is true, the list's default delivered/draft exclusion is bypassed so historical linked rows are not hidden. Boolean false applies the inverse predicate while retaining normal status handling; omit the key to return both states. return_status accepts exactly return_requested, return_approved, return_denied, return_in_transit, return_received, or return_completed; it returns matching original orders only and is applied before counting and pagination. Use {"return_status":"return_in_transit"}; omit return_status when clearing the filter. delivery_flow accepts exactly replacement_forward, replacement_return, operational_return, or commercial_return; use {"delivery_flow":"commercial_return"} and omit the key when clearing it. readiness_status accepts exactly ready_to_pick_up or ready_to_deliver. ready_to_pick_up means the next pending work collects from a non-warehouse origin, including a normal delivery or P&D inbound Leg A. ready_to_deliver means pending work starts from a warehouse, including a released P&D outbound Leg B; a staged parent is ready only when it is staged_at_warehouse and its outbound child is pending. Received-but-unreleased orders have no readiness value. The legacy is_ready_to_deliver boolean remains temporarily for deployed-client compatibility and is true only when readiness_status=ready_to_deliver. has_open_exception, is_ready_to_deliver, has_return_order, is_return_order, and is_parent accept boolean true/false or the strings true/false; omit the key to return both states. Use {"readiness_status":"ready_to_deliver"} for the delivery-ready list and omit readiness_status to clear the filter. Use {"status":"All"} to explicitly include every delivery status and bypass the default draft/delivered exclusions without applying a status equality filter. A non-empty search overrides only delivery-status handling; return status, return relationship, delivery flow, readiness, and other filters still apply. Example: {"has_return_order":true}.

column_filters.is_parentOptionalstring

Documentation-only example for the nested column_filters JSON object. Send is_parent as boolean true or false; true returns parent orders and false returns non-parent top-level orders. The exact predicate is AND-combined with other filters and runs before counting and pagination. Omit the key to return both types under the endpoint's normal top-level-order behavior.

Example: true

column_filters.delivery_service_idOptionalstring

Documentation-only example for the nested column_filters JSON object. Send an exact string ID to return deliveries using that delivery service. The predicate is AND-combined with other filters and runs in the database before counting and pagination.

Example: delivery-service-uuid

column_filters.source_transfer_manifest_idOptionalstring

Documentation-only example for the nested column_filters JSON object. Send an exact string ID to return deliveries originating from that Transfer Manifest. The predicate is AND-combined with other filters and runs in the database before counting and pagination.

Example: transfer-manifest-uuid

sort_byOptionalstring

Optional column to sort by. Allowed values: delivery_code, status, delivery_status, return_status, priority, created_at, updated_at, cod_amount, delivery_fee, customer, driver. status and delivery_status both sort by the delivery status column. Default: created_at.

sort_orderOptionalstring

Optional sort order: asc or desc. Default: desc.

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
GET/delivery/driver-deliveries

Driver 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 updatedAt descending order. Rows with the same updatedAt are ordered by deliveryId descending, 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_up or en_route_to_delivery; this represents physical parcel custody, not permission to continue delivery

  • Excludes child deliveries whose Waybill is active (drafted, sealed, assigned, picked_up, in_transit, or awaiting_receipt). Those packages are represented by the Waybill card from GET /delivery/driver-waybills and must not be independently startable from the normal delivery list.

Exception decision contract

  • Every row includes returnStatus: null, return_approved, return_in_transit, or return_completed.

  • While status=exception, an open exception blocks driver actions.

  • After dispatcher Retry, returnStatus is 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 using POST /delivery/deliveries/:deliveryId/status.

Cancelled custody contract

  • Every row exposes assignmentId and assignmentStatus

  • A cancelled custody row includes returnInstruction with action=complete_return_handoff, a localized message, and destination.warehouseId plus the existing location snapshot

  • For 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 all total until handoff completion

Filtering

  • status=delivered: returns only delivered deliveries

  • status=pending: returns pending deliveries. If tenant driver_config.driver_can_self_assign is true, it also includes unassigned pending deliveries. Pending deliveries that are already assigned to this driver (pending acceptance) are returned first and include pendingAcceptance: 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, or cancelled Waybills follow normal delivery filtering.

Proof Requirements

  • Each delivery card includes proofRequirements.pickup and proofRequirements.delivery when 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=cancelled and returnInstruction is present, show the instruction and destination, hide every normal delivery action, and offer only the literal returnInstruction.action.

  • Submit the row's child deliveryId to POST /delivery/action/:deliveryId; do not use a staged parent ID.

  • Render status tabs in the backend-provided order: all, pending (label Available), 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_assignments rows for active-Waybill children; use waybill_realtime to refresh Waybill cards.

  • Use each card's proofRequirements to 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, use replacementParentDeliveryId only as the parent linkage id; continue sending the child deliveryId for 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 replacementParentDeliveryId with the staged parent id.

Response includes

  • deliveries: simplified UI shape

  • statuses: list of status counts for the driver UI tabs/filters, ordered as all, 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

ParameterTypeDescription
pagestring

Page number for pagination (default: 1)

Example: 1

limitstring

Number of items per page (default: 10)

Example: 10

statusOptionalstring

Filter by delivery status (optional)

searchOptionalstring

Search term for filtering deliveries (optional)

tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
GET/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

ParameterTypeDescription
delivery_idRequiredstring

Variable used inside the request path.

Query parameters

ParameterTypeDescription
filter_typeRequiredstring

Required lookup mode. Allowed values are exactly id and delivery_code; defaults to id. Barcode lookups must use the scanner resolver.

Example: id

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
GET/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

ParameterTypeDescription
barcodeRequiredstring

Variable used inside the request path.

Query parameters

ParameterTypeDescription
languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
GET/delivery/driver-deliveries/status-summary

The 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 with status, label, and count fields

  • Status order: all, pending (label Available), assigned, en_route_to_pickup, picked_up, in_transit, delivered, cancelled

Query parameters

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
GET/delivery/deliveries/:deliveryId

Delivery 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_events belongs 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_events field is returned. The requested history is loaded with one bounded primary-key database query. Customer and driver tracking continues to use status_updates.

Children

  • children_summary is a compact operational-leg list with id, type, direction, status, and optional prime.

  • children is included for staged/hybrid parent detail responses when child rows are available from delivery_list_view.

  • Each child includes the normal delivery list view fields, including delivery_flow when present. Replacement-return children are excluded from this parent operational children list.

Parent assignment projection

  • For staged/P&D parent details, the existing assignment object 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, and assignment.leg_sequence identify the active/source child leg.

  • assignment.status_updates is a merged child-assignment timeline. Each event includes source = "child_assignment", assignment_id, delivery_id, delivery_code, direction, and leg_sequence.

  • driver and vehicle follow 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, and delivery.assignment.status_updates; no separate parent-specific field is required.

Proofs

  • The optional proofs array contains one row per submitted asset. Each row has actor_type, actor_id, stage (pickup or delivery), proof_type (photo or signature), and storage_url.

  • proof_of_pickup and proof_of_delivery are delivery-level aggregate proof summaries when captured, shaped as { signature, photos }.

Exceptions

  • delivery.has_exception and delivery.exception (with compatibility aliases has_open_exception and open_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_exception and exception fields. Starting or progressing that outbound returns HTTP 409 parent_exception_open.

  • The top-level exceptions array contains the delivery's exception history.

  • Each item includes the canonical exception_type and the user-facing exception_label from the tenant exception_types setting. Render exception_label; do not send it when creating or updating an exception.

Replacement orders

  • delivery_flow indicates if this is a replacement_forward or replacement_return (null for normal deliveries).

  • If delivery_flow = 'replacement_forward' and the return delivery has been auto-created, response includes replacement_delivery with the full return delivery details and its current status.

  • If delivery_flow = 'replacement_return', response includes replacement_delivery with the forward delivery details.

  • replacement_delivery is 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" and replacement_parent_delivery_id with 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 through replacement_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

ParameterTypeDescription
deliveryIdRequiredstring

ID of the delivery to retrieve

Example: uuid

Query parameters

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
GET/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

ParameterTypeDescription
delivery_barcodeRequiredstring

Variable used inside the request path.

Query parameters

ParameterTypeDescription
filter_typestring

Example: barcode

tenant_idstring

Example: {{tenant_id}}

languagestring

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
POST/delivery/deliveries

Unit 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_id is optional. When tenant setting delivery_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 400 external_reference_id_already_exists; when false, duplicates are accepted.

  • Provide pickup_location and delivery_location as JSON objects with lat/long and country for non-warehouse endpoints.

  • If pickup_warehouse_id is provided, the API resolves pickup from that warehouse address and ignores any submitted pickup_location/pickup_address JSON for pickup.

  • If dropoff_warehouse_id is provided, the API resolves delivery from that warehouse address and ignores any submitted delivery_location/delivery_address JSON 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 optional quantity directly in package_details. For multipart, use package_details.items; the backend rebuilds that field from normalized item rows.

  • Optional is_draft=true creates the order with status=draft; it may be sent in the JSON body or as ?is_draft=true. Missing or false keeps the existing pending behavior. 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_id is optional; if null, system auto-selects.

  • package_details.items is optional. When omitted or empty, backend creates one warehouse inventory item automatically with name {delivery_code} - {customer_name}, quantity 1, and uses package_details.weight when 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 with target_delivery_status=pending to release the parent and first leg.

Frontend integration contract

  • For warehouse toggles, send the matching pickup_warehouse_id or dropoff_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, send package_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 body is_draft: true or query ?is_draft=true.

  • For wallet draft rows, show the held fee from the wallet's aggregate reserved value; currency comes from tenant configuration. Show Execute and call POST /delivery/deliveries/:deliveryId/status with { "target_delivery_status": "pending" }.

Replacement orders

  • Set is_replacement=true to 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_id is 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 = 0 because 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

ParameterTypeDescription
languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Content-Typestring

Example: application/json

Body parameters

ParameterTypeDescription
requires_warehouse_stagingboolean—
staging_warehouse_idnull—
is_replacementboolean—
is_draftboolean—
pickup_locationobject—
pickup_location.full_addressstring—
pickup_location.latnumber—
pickup_location.longnumber—
pickup_location.countrystring—
pickup_location.contact_infoobject—
pickup_location.contact_info.phonestring—
pickup_location.contact_info.emailstring—
Show 31 more body fields

Additional body parameters

ParameterTypeDescription
delivery_locationobject—
delivery_location.full_addressstring—
delivery_location.latnumber—
delivery_location.longnumber—
delivery_location.countrystring—
delivery_location.contact_infoobject—
delivery_location.contact_info.phonestring—
delivery_location.contact_info.namestring—
pickup_warehouse_idnull—
dropoff_warehouse_idnull—
delivery_service_idstring—
package_detailsobject—
package_details.special_instructionsstring—
package_details.itemsarray<object>—
package_details.items.namestring—
package_details.items.descriptionstring—
package_details.items.quantitynumber—
package_details.items.weightnumber—
package_details.items.lengthnumber—
package_details.items.widthnumber—
package_details.items.heightnumber—
customer_idstring—
external_reference_idstring—
statusstring—
scheduled_pickup_timestring—
scheduled_delivery_timestring—
prioritynumber—
notesstring—
payer_typestring—
payment_methodstring—
delivery_feenumber—
Staging
Request
curl
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": "sender@example.com"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

201Created
Response body
json
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 & Last-Mile / Deliveries
POST/delivery/deliveries

Create a delivery using the carrier-aware routing engine.

Behavior

  • external_reference_id follows delivery_external_reference.unique_per_customer (default true) for the same tenant customer at the root-order level. Hybrid child legs reuse the root reference.

  • Provide pickup_location and delivery_location as JSON with lat/long/country for non-warehouse endpoints.

  • If pickup_warehouse_id is provided, the pickup endpoint is resolved from that warehouse address and any submitted pickup JSON is ignored.

  • If dropoff_warehouse_id is 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, use package_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_id is optional; if null, system auto-selects.

  • package_details.items is optional. When omitted or empty, backend creates one warehouse inventory item automatically with name {delivery_code} - {customer_name}, quantity 1, and uses package_details.weight when 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_id or dropoff_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, send package_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_routing is false.

Query parameters

ParameterTypeDescription
languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Content-Typestring

Example: application/json

Body parameters

ParameterTypeDescription
carrier_routingboolean—
requires_warehouse_stagingboolean—
staging_warehouse_idnull—
pickup_locationobject—
pickup_location.full_addressstring—
pickup_location.latnumber—
pickup_location.longnumber—
pickup_location.countrystring—
delivery_locationobject—
delivery_location.full_addressstring—
delivery_location.latnumber—
delivery_location.longnumber—
Show 25 more body fields

Additional body parameters

ParameterTypeDescription
delivery_location.countrystring—
pickup_warehouse_idnull—
dropoff_warehouse_idnull—
delivery_service_idstring—
customer_idstring—
payer_typestring—
payment_methodstring—
unit_sizenumber—
notesstring—
cod_amountnumber—
external_reference_idstring—
scheduled_pickup_timestring—
scheduled_delivery_timestring—
receiver_user_idstring—
prioritynumber—
package_detailsobject—
package_details.special_instructionsstring—
package_details.itemsarray<object>—
package_details.items.namestring—
package_details.items.descriptionstring—
package_details.items.quantitynumber—
package_details.items.weightnumber—
package_details.items.lengthnumber—
package_details.items.widthnumber—
package_details.items.heightnumber—
Staging
Request
curl
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

201Created
Response body
json
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 & Last-Mile / Deliveries
GET/delivery/deliveries/:deliveryId/receipt

Returns the primary signed receipt URL for a delivery.

Access

  • tenant admin / dispatcher with delivery module access

  • delivery owner customer

Resolution priority

  • COD_COLLECTION receipt

  • DELIVERY_FEE receipt

Path parameters

ParameterTypeDescription
deliveryIdRequiredstring

ID of the delivery whose receipt should be retrieved

Example: uuid

Query parameters

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
PUT/delivery/deliveries/:deliveryId

Unit 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_flow are immutable.

  • Use the public boolean is_replacement to enable or disable replacement handling.

  • Status is not accepted here; use POST /delivery/deliveries/:deliveryId/status.

Editable until final delivery

  • pickup_warehouse_id or dropoff_warehouse_id

  • pickup_location_id or pickup_location

  • delivery_location_id or delivery_location

  • external_reference_id (string or null; trimmed, and an empty string or null clears it)

  • cod_amount (minimum 0)

  • payer_type (sender | receiver)

  • payment_method (wallet | cash | cod; cod is stored as cash)

  • delivery_fee (minimum 0)

  • unit_size (integer, minimum 1)

  • is_replacement (boolean)

  • notes (string or null; send an empty string or null to clear it)

  • package_details, including package_details.special_instructions (string handling instructions) and replacement semantics for items[]; each item requires a name, a positive integer quantity, non-negative numeric dimensions, and a valid UUID when id is 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_replacement updates 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 delivered state 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: true maps the logical order to delivery_flow = replacement_forward; false removes 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 400 cod_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_replacement on it returns HTTP 400 replacement_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_zero or replacement_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_balance or 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 emit order_update; existing webhook tracking remains status, COD, price, payer side, and payment method.

  • One request emits at most one logical order_update webhook when tracked fields change. Child synchronization does not emit duplicates.

Order history (order_events)

  • status_updates remains 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 canonical status_updates, order_edited events, and projected operational-child driver activity (driver_unassigned, driver_switched, driver_handoff_pending, and driver_handoff_completed). Child driver events retain their original payload and add delivery_id, delivery_code, direction, leg_sequence, and source=active_leg. Existing canonical orders are backfilled from status_updates; future canonical status events are mirrored without changing their payload.

  • Every successful edit appends at most one order_edited event 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. Existing system_events webhook behavior remains separate and unchanged.

Frontend integration contract

  • When a warehouse is selected, send its non-null pickup_warehouse_id or dropoff_warehouse_id; matching address input is ignored. To deselect it, send the warehouse field as null together with the replacement address.

  • Send only fields the user actually changed and use is_replacement for the toggle; never send delivery_flow. Send special instructions only as package_details.special_instructions; top-level special_instructions is rejected.

  • Use the returned canonical parent delivery; delivery_flow = replacement_forward means 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, keep delivery_fee and cod_amount read-only at zero.

  • Handle HTTP 400 with field=external_reference_id as a duplicate external reference, plus invalid_package_details, cod_not_allowed_for_replacement_orders, and replacement_return_type_not_editable.

  • Handle HTTP 409 delivery_already_delivered, replacement_return_already_exists, driver-zone/capacity conflicts, insufficient_available_balance, insufficient_balance, and delivery_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_required defaults to false.

  • reason is 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 400 delivery_edit_reason_required.

  • External customer edits remain unchanged.

  • A successful real edit stores the normalized reason on the same canonical order_edited event. Failed and no-op updates do not create an event.

Frontend integration contract

  • Read tenantSettings.delivery.delivery_order.settings.edit.reason_required from 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, and delivery_edit_reason_too_long; display the localized message.

Path parameters

ParameterTypeDescription
deliveryIdRequiredstring

ID of the delivery to update

Example: uuid

Query parameters

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Content-Typestring

Example: application/json

Body parameters

ParameterTypeDescription
pickup_warehouse_idstring—
pickup_location_idstring—
dropoff_warehouse_idnull—
delivery_locationobject—
delivery_location.full_addressstring—
delivery_location.latitudenumber—
delivery_location.longitudenumber—
delivery_location.citystring—
delivery_location.countrystring—
external_reference_idstring—
cod_amountnumber—
payer_typestring—
Show 17 more body fields

Additional body parameters

ParameterTypeDescription
payment_methodstring—
delivery_feenumber—
unit_sizenumber—
is_replacementboolean—
package_detailsobject—
package_details.special_instructionsstring—
package_details.itemsarray<object>—
package_details.items.idstring—
package_details.items.namestring—
package_details.items.descriptionstring—
package_details.items.quantitynumber—
package_details.items.weightnumber—
package_details.items.lengthnumber—
package_details.items.widthnumber—
package_details.items.heightnumber—
notesstring—
reasonstring—
Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
POST/delivery/deliveries/:deliveryId/status

Update 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=assigned respects require_accept_or_reject_for_assignments: when enabled, the active assignment and delivery remain pending until the driver accepts, and the success response includes driver_acceptance_required=true. Later operational targets (en_route_to_pickup, picked_up, and in_transit) deliberately bypass acceptance and proceed immediately. delivered is accepted only when the direct delivery, or the resolved active child of a staged/P&D parent, is currently picked_up or in_transit.

  • reason: optional when a dispatcher sends target_delivery_status=pending. It is required and nonblank for every other dispatcher/admin target and when the assigned driver sends target_delivery_status=returned. Missing or blank values return HTTP 400 reason_required with field=reason. Other driver-action endpoints, automatic transitions, warehouse receiving, Waybills, and external draft confirmation keep their existing contracts.

Draft wallet behavior

  • draft to pending captures 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

  • cancelled and returned resolve 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_cancel controls cancellation and refund_delivery_fee_on_return controls return; defaults are true and false respectively. When allowed, the collected fee is credited to the sender wallet once and matching fee payments become REFUNDED. When disallowed, the settled fee is retained and remains paid. Collected COD keeps its existing independent terminal credit behavior.

  • cancelled writes every active assignment to cancelled, releases driver capacity, removes realtime assignment cards, and blocks every later driver action. No return handoff is created.

  • returned writes the parent plus every non-final operational leg and active assignment to returned, releases driver capacity, removes realtime assignment cards, and closes unfinished receiving, picking, and package-handoff work. Already delivered, cancelled, or returned child 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=cancelled and status=returned respectively.

  • A successful cancelled or returned response includes reversal with fee/COD amounts, refund flags, wallet transaction IDs, and the sender user ID.

Manual status behavior

  • A successful response normally guarantees delivery.status equals target_delivery_status, except an assigned request that requires driver acceptance returns delivery.status=pending with driver_acceptance_required=true. For a staged/P&D parent whose active child is outbound, manual assigned, en_route_to_pickup, and picked_up advance only that child; the returned parent delivery.status keeps its current warehouse lifecycle status (normally staged_at_warehouse).

  • For a staged/P&D parent, manual delivered first resolves the active child and requires that child to be picked_up or in_transit. Once eligible, it completes every unfinished child in sequence_plan order before finalizing the parent. Children already delivered remain unchanged; cancelled or returned children fail with HTTP 409 staged_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 400 driver_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, or in_transit), the backend continues resolving only the first active non-terminal child. Outbound assigned, en_route_to_pickup, and picked_up are child-only preparation states; outbound in_transit advances 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_warehouse remains the dedicated P&D warehouse-receive operation. When the inbound child lacks an active assignment, request driver_id is used first. If it is omitted, a parent created from a Transfer Manifest (both source_transfer_manifest_id and source_transfer_manifest_item_id) may use assignment_change_settings.default_driver_id only when transfer_manifest.allow_warehouse_staging_without_assigned_driver=true. Toggle Off, a missing fallback, or a normal/non-G2G order returns driver_id_required_for_unassigned_delivery. Deleted or cross-tenant fallback drivers return driver_not_found; inactive or unavailable drivers return driver_not_active; zone mismatches keep the existing zone errors. Optional bin_id must 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:tenant or manage:module:delivery. The currently assigned driver is also authorized, but only for target_delivery_status=returned; any other target returns HTTP 403 insufficient_permissions.

  • For non-Delivered assignment-required targets with no active assignment, driver_id is used first, then assignment_change_settings.default_driver_id; otherwise the API returns driver_id_required_for_delivery_status.

  • proof_of_delivery remains 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 outside picked_up and in_transit returns HTTP 409 with code=invalid_status_transition and the localized plain-language message Order status does not allow this action in 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 or returned_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_id when available; the backend uses it only after the delivery passes the picked_up/in_transit eligibility 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_up or in_transit; backend validation remains authoritative.

  • Handle HTTP 400 reason_required and driver_id_required_for_delivery_status, plus HTTP 409 invalid_status_transition, insufficient_available_balance, paid_wallet_delivery_cannot_return_to_draft, staged_child_not_completable and existing driver eligibility/capacity errors. For invalid_status_transition, display the returned localized message and 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 is assigned, en_route_to_pickup, or picked_up, keep the returned parent warehouse status (normally staged_at_warehouse) instead of displaying the child target on the parent. For target_delivery_status=assigned, keep the returned status when driver_acceptance_required=true; do not display the child as Assigned until the driver accepts. Outbound in_transit and delivered advance the parent normally. On terminal success, use reversal for 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

ParameterTypeDescription
deliveryIdRequiredstring

ID of the delivery to update status for

Example: uuid

Query parameters

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Content-Typestring

Example: application/json

Body parameters

ParameterTypeDescription
target_delivery_statusstring—
reasonstring—
notesstring—
Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
POST/delivery/deliveries/:deliveryId/cancel

Cancel a delivery order. This endpoint:

  • Updates the logical delivery order status to cancelled

  • Reverses 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_status

  • Merchant/customer cancellation is transactionally blocked after any logical-order assignment becomes active with HTTP 409 cannot_cancel_delivery_after_driver_assignment

  • Tenant admin/dispatcher can cancel assigned, picked-up, or in-transit deliveries

  • Assignments not in picked_up or en_route_to_delivery become cancelled and release capacity immediately

  • A picked_up or en_route_to_delivery assignment keeps its custody status, driver, and capacity for audit; the delivery and every leg are still cancelled and all normal actions are blocked

  • Cancelled and returned deliveries are hidden from the default driver task list and are returned only from explicit status=cancelled or status=returned filters

  • Cancellation credits the collected delivery fee to the sender wallet only when the original service effective refund_delivery_fee_on_cancel policy allows it; the default is true

  • Successful DELIVERY_FEE payments 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 edit

  • Collected COD is resolved across the same logical family and refunded using cod_collections.allocations.cod; driver cash liability/remittance state is not voided or reversed

  • Matching family DELIVERY_FEE payments are updated to REFUNDED only 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 refunded

  • A 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_orders with external_reference = 'DEL' || delivery_code, status not in received, partially_received, received_with_exceptions, cancelled)

    • Package handoffs (package_handoffs with delivery_id matching an operational child, status not in completed, 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.id is the canonical logical root ID, including when the request used a staged child ID

  • reversal.delivery_fee_amount is the actual successful family fee refunded

Frontend integration contract

  • Request body is optional; both failure_reason and notes are optional.

  • Handle HTTP 409 cannot_cancel_delivery_after_driver_assignment and cannot_cancel_delivery_in_terminal_status.

  • Admin/dispatcher does not need to unassign first.

  • Use returned delivery.id as 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 driver and assignment for 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

ParameterTypeDescription
deliveryIdRequiredstring

ID of the delivery to cancel

Example: uuid

Query parameters

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Content-Typestring

Example: application/json

Body parameters

ParameterTypeDescription
failure_reasonOptionalstring

(string): stored in delivery_failure_reason and the status timeline

notesOptionalstring

(string): stored in the status timeline

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
DELETE/delivery/deliveries/:deliveryId

Legacy 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

ParameterTypeDescription
deliveryIdRequiredstring

ID of the delivery to delete

Example: uuid

Query parameters

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
1{2  "success": true,3  "message": "Delivery deleted successfully"4}

Create Bulk Deliveries​

Delivery & Last-Mile / Deliveries
POST/delivery/deliveries/bulk

Create 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

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Content-Typestring

Example: application/json

Body parameters

ParameterTypeDescription
pickup_locationobject—
pickup_location.full_addressstring—
pickup_location.latnumber—
pickup_location.longnumber—
pickup_location.countrystring—
delivery_locationobject—
delivery_location.full_addressstring—
delivery_location.latnumber—
delivery_location.longnumber—
delivery_location.countrystring—
package_detailsobject—
package_details.special_instructionsstring—
Show 14 more body fields

Additional body parameters

ParameterTypeDescription
package_details.itemsarray<object>—
package_details.items.namestring—
package_details.items.descriptionstring—
package_details.items.quantitynumber—
package_details.items.weightnumber—
package_details.items.lengthnumber—
package_details.items.widthnumber—
package_details.items.heightnumber—
customer_idstring—
statusstring—
scheduled_pickup_timestring—
scheduled_delivery_timestring—
prioritynumber—
bulk_groupstring—
Staging
Request
curl
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

201Created
Response body
json
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 & Last-Mile / Deliveries
POST/delivery/action/:deliveryId

Handle 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_actions and a localized plain-language message.

  • 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_status null, 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 to en_route_to_pickup)

  • en_route_to_pickup → picked_up

  • picked_up → en_route_to_delivery (delivery status transitions to in_transit)

  • en_route_to_delivery → completed (requires proof only when policy flags require it, delivery status transitions to delivered)

Replacement Orders

  • When completing a direct delivery with delivery_flow = 'replacement_forward', the response includes a replacement_return object.

  • For warehouse-staged replacements, the parent has delivery_flow = 'replacement_forward'; completing the final outbound child creates the return for that parent and includes replacement_return in 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 use parent_delivery_id.

  • The return parent and all return legs have delivery_fee = 0 and cod_amount = 0; replacement returns never collect goods COD because the forward fee covers the round trip.

  • The driver should display replacement_return immediately. 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 409 with error = 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 retry resolves the exception, restores the prior operational status, clears any return instruction, and lets the same assigned driver continue through this ordinary action endpoint.

  • Dispatcher return resolves the exception and sets persistent returnStatus=return_approved. The driver app must hide Delivered and show one whole-delivery Mark Returned action.

  • Mark Returned calls POST /delivery/deliveries/{deliveryId}/status with { "target_delivery_status": "returned", "reason": "...", "notes": "..." }; it does not use returned_items and 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 payments row for the COD amount only after the delivery is marked delivered.

  • 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_transactions earning row for the assigned driver using tenant_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, and reference_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_url is required only when the active leg's proofRequirements has signature_required=true.

  • photo_urls is required only when the active leg's proofRequirements has photo_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.

  • notes is optional.

Location Tracking

  • current_lat (optional): Current latitude for location tracking

  • current_lng (optional): Current longitude for location tracking

  • Route calculation is queued after the status transition so Google Directions latency does not delay the action response.

  • delivery_assignments.route_details may 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, and arrived_at_warehouse.

  • Warehouse receiving sets the parent to staged_at_warehouse; assigning the outbound leg does not regress that stage.

  • Outbound en_route_to_pickup and picked_up remain child-leg-only statuses and do not change the parent from staged_at_warehouse. Outbound in_transit sets the parent to in_transit, and outbound completion sets it to delivered.

  • Parent status_updates inherit actor_id and actor_role from 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 as delivery.proof_of_delivery.

  • Always includes proofRequirements, feeToCollectAtPickup, and feeToCollectAtDelivery, calculated from the resolved effective policy for the active delivery leg.

  • Includes warehouseEndpoint when the active leg starts or ends at a warehouse.

  • For direct replacement forwards and final outbound children of staged replacements, also returns replacement_return with the auto-created active return delivery.

  • For final outbound children of staged replacements, delivery.delivery_flow is returned as replacement_forward and delivery.replacement_parent_delivery_id contains the staged parent id.

Frontend integration contract

  • Replace the local delivery with delivery from 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 localized message and 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 localized message.

  • Read delivery.proofRequirements before deciding whether to ask for signature or photo proof.

  • If replacement_return is present, insert/show it immediately as the driver's next return pickup; it is already assigned to the same driver.

  • On 409 with error = 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 deliveryId to this endpoint for staged replacement actions; use replacement_parent_delivery_id only 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.message preserves the original localized action error and cause.code its 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

ParameterTypeDescription
deliveryIdRequiredstring

ID of the delivery to perform action on

Example: uuid

Query parameters

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Content-Typestring

Example: application/json

Body parameters

ParameterTypeDescription
assignment_statusstring—
signature_urlstring—
photo_urlsarray<string>—
notesstring—
returned_itemsarray<object>—
returned_items.idstring—
returned_items.reasonstring—
returned_items.notesstring—
returned_items.photo_urlsarray<string>—
current_latstring—
current_lngstring—
Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
GET/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

ParameterTypeDescription
delivery_idRequiredstring

Variable used inside the request path.

completion_operation_idRequiredstring

Variable used inside the request path.

Query parameters

ParameterTypeDescription
languagestring

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Deliveries
POST/delivery/action/:deliveryId

Completes 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

ParameterTypeDescription
deliveryIdRequiredstring

The cancelled operational child delivery ID returned by GET /delivery/driver-deliveries

Example: active-child-leg-uuid

Query parameters

ParameterTypeDescription
tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language for response messages

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

Content-Typestring

Example: application/json

Body parameters

ParameterTypeDescription
actionstring—
notesstring—
Staging
Request
curl
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

200OK
Response body
json
1{2  "success": true,3  "message": "The delivery was returned successfully",4  "delivery": {5    "id": "active-child-leg-uuid",6    "status": "cancelled"7  }8}