Bulk Operations
10 API calls in this section.
Bulk action API for the Dispatcher Panel. Validate and execute actions on multiple orders at once, search orders by code or customer name, and retrieve print URLs (labels/receipts). When Mark Returned targets a P&D order whose active leg is inbound, the parent and inbound become Returned and every unfinished outbound becomes Cancelled. Outbound assignment, draft execution, route assignment, pickup, transit, and delivery actions fail per row with parent_exception_open while the parent exception remains open; cancellation and release actions remain available. Bulk driver assignment commits assignment, delivery status, capacity, and eligible P&D pending handoffs atomically; concurrent state failures use stable localized error codes.
Validate Bulk Action
/delivery/bulk/validateAccepts plain delivery codes such as 1865 and printed parcel barcodes such as DEL1865 in order_codes. Inputs are trimmed, matched case-insensitively, and canonical duplicates are processed once. For assign_to_route, use exactly one mode: session_id with an optional matching route_id for an existing session, or route_id and driver_id together to reuse/create a session. For mark_staged_at_warehouse, a visible parent or child code may identify a failed-delivery return waiting on its linked receiving order. Frontend integration: send scanner output unchanged; response data.results[].order_code uses the canonical code without the generated DEL prefix.
Pre-flight check
validates whether each order is eligible for the requested action without making any changes.
Common audit params
params.reason and params.notes are optional scalar strings for every action. One value applies to every order_code in the request; arrays are invalid. On execute, a missing/blank reason is replaced once with an action-specific system reason and that same reason is used for all selected deliveries.
action_id values
execute_draft— params: none. Direct draft codes move to Pending. An all-draft staged/P&D parent releases the parent and first inbound child. Astaged_at_warehouseparent resolves to its draft outbound child and releases that child while the parent remains staged. A parent code and its resolved child code cannot be submitted together. Outbound children require their parent to still bestaged_at_warehouse.mark_pending— params: reason (optional). Allowed only forassignedoren_route_to_pickup; atomically unassigns the active driver and returns the active delivery topending. Parent codes resolve to the active child leg. When that leg is inbound, its staged parent delivery also returns topending; driver ownership remains child-only.assign_driver— params:driver_id(required).assign_driverrespectsrequire_accept_or_reject_for_assignments: when enabled, each assignment and resolved active delivery/leg remainpendinguntil the driver accepts; when disabled, both becomeassigned. Each delivery is also checked against the driver's allowed zones at execute time; deliveries outside the driver's zones fail withdriver_not_allowed_in_pickup_zone/driver_not_allowed_in_delivery_zone. If a staged/P&D parent order code is submitted, validation resolves it to the first active non-terminal child leg bysequence_plan; assignment eligibility is checked against that child and parent assignment rows are not created.mark_en_route_to_pickup— requires an active assignment/driver and accepts eitherpendingacceptance orassigned; choosing it bypasses pending acceptance and advances immediately. Hybrid parent codes resolve to the active child leg.mark_picked_up— requires an active assignment/driver and acceptspendingacceptance,assigned, oren_route_to_pickup; choosing it bypasses pending acceptance and records pickup immediately. Hybrid parent codes resolve to the active child leg.mark_in_transit— requires an active assignment/driver on every operational delivery. Hybrid parent codes resolve to the active child leg.mark_delivered— params:signature_url(optional) andphoto_urls(optional); commonreasonandnotesremain optional. Direct orders must bepicked_uporin_transitwith an active assignment. A staged/P&D parent code resolves to its first active non-terminal child leg for validation, so a delivered inbound leg plus an in-transit assigned outbound leg is eligible.mark_staged_at_warehouse— accepts both first-time P&D warehouse staging and failed-delivery returns waiting for physical receipt. Params:driver_id(optional, used only by first-time P&D staging when an active inbound child assignment is missing) and one sharedbin_id(optional). First-time P&D staging keeps its existing accepted parent statuses:pending,assigned,en_route_to_pickup,picked_up,in_transit,arrived_at_warehouse,staged_at_warehouse. A failed return is accepted only when its operational delivery isexception, its exception is open with custodyawaiting_warehouse_receipt, and its linkedfailed_delivery_returnreceiving order ispendingorin_progress; an already received matching row is an idempotent replay. The returned parcel may be selected by its visible parent or child code. A second code resolving to the same exception fails withduplicate_delivery_ids_not_allowed. The supplied bin must belong to each row's receiving warehouse; when omitted, each receiving order uses its own active default receiving location.cancel_orders— commonreasonandnotesare optionalmark_returned— params: nonereassign_driver— params:driver_id(required); commonreasonandnotesare optional. Same zone check asassign_driver. Also supports deliveries that were self-unassigned after pickup or in transit; the new driver continues from the current delivery progress. Parent order codes resolve to the active child leg before reassignment.unassign— params: reason (optional). Same exact-status, parent-to-active-leg, and inbound-parent-to-pendingbehavior asmark_pending;pending,picked_up,in_transit, and later states are rejected.assign_to_route— use exactly one mode:session_idwith an optional matchingroute_idfor an existing session, orroute_idanddriver_idtogether to reuse/create a session. The session supplies its own route and immutable driver; a supplied route must match it. Hybrid parent codes resolve to the active child leg.
Open exception rule
Any order with an
opendelivery exception fails validation withdelivery_has_open_exceptionand cannot be moved by bulk actions until the exception is resolved, except thatmark_staged_at_warehousemay perform the exact linked physical receipt while custody isawaiting_warehouse_receipt. It does not resolve that exception.
All actions except resolve_exception accept a maximum of 500 order_codes per request. resolve_exception accepts the dispatcher's complete group in one request and chunks it internally.
Routed unassignment
Routed active deliveries cannot be marked Pending or unassigned individually. Both validation and execution reject mark_pending and unassign when the resolved operational delivery has a route_session_id, using the localized cannot_self_unassign_routed_delivery message. For parent codes, this check applies to the active child leg. Invalid rows are excluded from total_valid; execution rechecks current state and preserves per-order failures if routing changes after validation.
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
order_codes | array<string> | — |
action_id | string | — |
params | object | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/bulk/validate" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "order_codes": [6 "1865",7 "1853"8 ],9 "action_id": "execute_draft",10 "params": {}11}'Response examples
1{2 "success": true,3 "message": "Validation completed",4 "data": {5 "results": [6 {7 "order_code": "1865",8 "valid": true9 },10 {11 "order_code": "1853",12 "valid": false,13 "error_message": "Order status does not allow this action"14 }15 ],16 "total_valid": 1,17 "total_failed": 118 }19}Execute Bulk Action
/delivery/bulk/executeUses the same order-code normalization as validation
order_codes accepts plain delivery codes and printed DEL-prefixed parcel barcodes, with canonical duplicates processed once. For prepared P&D/operational-return work, assign_driver creates one pending driver handoff in the assignment transaction, reassign_driver updates that same pending handoff, and unassign/mark_pending clear only its driver context while preserving the completed pick and warehouse reservation. For assign_to_route, send session_id with an optional matching route_id when an existing session is selected; send route_id and driver_id together only when no session is selected. Successful route assignment returns data.assignment_outcome, data.requested_session, data.session, data.fallback_reason, and data.assigned_count; assignment_outcome is authoritative and session.id is the navigation target. For execute_draft, the frontend may submit a visible staged parent code: an all-draft parent releases itself and its first inbound leg, while a staged_at_warehouse parent resolves to its draft outbound leg and remains staged. For mark_returned, a staged parent whose active leg is inbound returns that inbound and parent while cancelling every unfinished outbound; the submitted parent order_code remains in the result. A parent plus its resolved child returns duplicate_delivery_ids_not_allowed for the later duplicate; an outbound child whose parent is not staged returns parent_not_staged_for_outbound_execution; a staged parent without a draft outbound child returns no_active_child_leg. For mark_staged_at_warehouse, a returned parcel may be submitted by its visible parent or child code and keeps that submitted code in the result. Frontend integration: send scanner output unchanged and do not resolve child codes client-side; response data.results[].order_code uses the canonical code without the generated DEL prefix. Realtime mobile contract for assign_driver, reassign_driver, and assign_to_route: the existing consolidated delivery_realtime_assignments row includes assignment_status and requires_driver_response. Only exact status pending sets the boolean true; assigned, later lifecycle states, missing status, and unknown status are informational. Mobile shows Accept/Reject only when requires_driver_response === true.
Execute the bulk action on the given orders. Re-validates each order at execution time (optimistic concurrency). Orders that fail re-validation are skipped and reported in results.
Same request body as validate. params.reason and params.notes are optional scalar strings shared by the entire request. Execute generates one action-specific system reason when reason is missing or blank. Open exceptions are checked in one batched query; orders with an open exception fail with delivery_has_open_exception and eligible orders still proceed.
Driver progress actions (mark_picked_up, mark_in_transit, mark_delivered):
Every operational delivery must still have an active assignment. If the driver self-unassigned, these actions fail with
bulk_no_active_assignmentuntil the delivery is reassigned.mark_en_route_to_pickupandmark_picked_upmay advance an active pending-acceptance assignment without waiting for driver acceptance; later lifecycle prerequisites remain unchanged.For lifecycle-managed P&D/operational-return children,
mark_picked_upuses one set-based atomic custody operation. It requires completed picking, the current assignment/handoff, clear delivery and warehouse-item exceptions, and fully reserved warehouse stock. Success records thepickmovement, clears stock/reservation, sets custodyin_transit, completes the handoff, marks pickinghanded_off, and advances delivery/assignment together. It never creates missing preparation. Waybill and ordinary rows keep their existing bulk behavior.mark_deliveredexecutes through the same dispatcher status workflow as a single status change. It records the supplied or system-generated reason in delivery and assignment audit events and preserves completion payments, driver earnings, notifications, proof, replacement-return handling, and retry behavior.For staged/P&D parents,
mark_deliveredvalidates the active child assignment but executes the submitted parent code. The backend completes every incomplete non-terminal leg and sets the parent todelivered; an already delivered inbound leg remains delivered. A draft/missing active leg returnsno_active_child_leg, an active leg outsidepicked_upandin_transitreturnsbulk_invalid_status_for_action, and a genuinely unassigned active leg returnsbulk_no_active_assignment.
Driver-assignment actions (assign_driver, reassign_driver, and mark_staged_at_warehouse when creating new assignments):
For
assign_driverandreassign_driver, a staged/P&D parent order code is accepted as dispatcher input but is resolved to the first active non-terminal child leg bysequence_planbefore validation or writes. The parent order code remains in the response result, but assignment rows are inserted for the resolved child delivery only. Bulkassign_driveruses the same tenant acceptance setting as single assignment: acceptance required keeps the child assignment/delivery and parent projectionpending; otherwise the child and parent projection becomeassigned.If the active child leg is still
draft, missing, or all child legs are terminal, the parent order result fails withno_active_child_leg.If a parent code and its resolved child code are submitted together, the duplicate resolved child fails with
duplicate_delivery_ids_not_allowed.All target deliveries are validated against the driver's allowed zones in O(1) DB calls. Per-delivery failures (
driver_not_allowed_in_pickup_zone,driver_not_allowed_in_delivery_zone) are returned alongside successes; eligible deliveries still proceed.reassign_driveralso supports deliveries that were self-unassigned after pickup or in transit. A fresh assignment row is created for the new driver and the delivery keeps its current progress status.Inserted assignments share a single
bulk_assignment_idso the per-row realtime trigger is suppressed; the API emits one consolidated realtime row and onebulk_delivery_assignednotification.
Unassign and mark-pending actions
pending,assigned, anden_route_to_pickupare accepted only when the resolved delivery has an active assignment with a driver; this allows a dispatcher to withdraw a pending driver offer before acceptance without making ordinary Pending orders unassignable.Driver ownership changes only on the resolved active child assignment.
When an inbound active child returns to
pending, its staged parent delivery also returns topending; sibling legs remain unchanged.An outbound child unassign does not reset a parent already at warehouse staging.
Warehouse staging action
mark_staged_at_warehouseprocesses first-time P&D staging and failed-delivery returns independently in the same request.bin_id, when supplied, is one shared batch choice and must belong to each affected receiving warehouse; a mismatch fails only that row. When omitted, each receiving order uses its own activedefault_receiving_location.The receiving order must match
DEL<parent delivery_code>, and every item must be linked to the inbound and outbound child legs.Stock increases only by the unreceived quantity; retries do not duplicate stock.
The inbound child is delivered before the receiving order and inventory are committed. The parent then moves to
staged_at_warehousewith actor audit context.When
params.driver_idis omitted and an inbound child is unassigned, a parent created from a Transfer Manifest (both source IDs present) usesassignment_change_settings.default_driver_idonly whentransfer_manifest.allow_warehouse_staging_without_assigned_driver=true. Normal/non-G2G rows and toggle-Off rows keepdriver_id_required_for_unassigned_delivery. Invalid fallback drivers and zone mismatches fail only their affected rows; assigned rows in the same request continue normally.The outbound child remains
draftunless the existing tenant settingauto_execute_delivery_order_on_receiveactivates it through the warehouse auto-execution flow.A failed-delivery return delegates to the same
receive_delivery_return_handoff(...)owner as the single warehouse receiving endpoint. Success completes its own receiving order, writes its receiving location and stock movement, changes custody tostaged_at_warehouse, releases the old assignment/capacity, stages its parent when present, and leaves the delivery and exception inexception/openfor the next dispatcher decision. Failures are row-isolated, so eligible parcels still complete. Important row-level errors includenot_a_warehouse_staged_order,inbound_child_not_found,receiving_order_not_found,receiving_order_not_failed_delivery_return,failed_delivery_return_not_receivable,failed_delivery_return_invalid_receipt_context,inventory_items_not_found,bin_id_required_for_received_items,bin_not_found,bin_must_belong_to_order_warehouse,driver_id_required_for_unassigned_delivery,duplicate_delivery_ids_not_allowed, andstaging_conflict.
Draft execution (execute_draft):
Moves selected Draft orders to Pending in bulk.
Wallet-paid draft orders atomically capture the reserved fee and create the single delivery-fee wallet transaction/payment before activation side effects. Reservation failures are returned per order; no partial financial change is kept.
Staged draft parent orders release the parent and first child leg, matching individual Execute. Currency is not returned for the reservation; the frontend uses tenant configuration.
Route assignment action
assign_to_routeaccepts exactly one mode:params.session_idwith an optional matchingparams.route_idfor an existing session, orparams.route_idandparams.driver_idtogether to reuse/create a session. In session mode, the backend derives the route and immutable driver from the tenant-scoped session and rejects a mismatched route. Successful execution returnsdata.assignment_outcome,data.requested_session,data.session,data.fallback_reason, anddata.assigned_count.
Frontend integration contract
params.reason and params.notes may be omitted for every action; if supplied, send one string value for the whole request, never an array. Keep sending the visible parent order code for staged/P&D rows; mark_staged_at_warehouse also accepts the returned parcel's visible child code. Use each returned data.results[].error_message as the row-specific failure reason, keep failed rows available for review, and after a successful failed-return receipt refetch the affected order/exception so its newly available decision actions are shown. For assign_driver, reassign_driver, mark_pending, mark_en_route_to_pickup, mark_picked_up, mark_in_transit, mark_delivered, unassign, and assign_to_route, the frontend may submit the visible parent order code. The API result still uses that submitted order_code; driver assignment writes remain on the active child leg. A successful inbound mark_pending or unassign also changes the staged parent delivery status to pending. After assign_driver, use the returned/refetched delivery status: keep it pending while driver acceptance is required and show it as assigned only after acceptance. Show Unassign for assigned and en_route_to_pickup, and for pending only when an active assignment has a driver; treat delivery_status_does_not_allow_unassign and assignment_cannot_be_unassigned as conflicts and keep the current UI state.
Routed unassignment
Routed active deliveries cannot be marked Pending or unassigned individually. Both validation and execution reject mark_pending and unassign when the resolved operational delivery has a route_session_id, using the localized cannot_self_unassign_routed_delivery message. For parent codes, this check applies to the active child leg. Invalid rows are excluded from total_valid; execution rechecks current state and preserves per-order failures if routing changes after validation.
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
order_codes | array<string> | — |
action_id | string | — |
params | object | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/bulk/execute" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "order_codes": [6 "1865",7 "1853"8 ],9 "action_id": "execute_draft",10 "params": {}11}'Response examples
1{2 "success": true,3 "message": "Bulk operation completed",4 "data": {5 "results": [6 {7 "order_code": "1865",8 "success": true9 },10 {11 "order_code": "1853",12 "success": false,13 "error_message": "Order status does not allow this action"14 }15 ],16 "total_success": 1,17 "total_failed": 118 }19}Validate Bulk Exception Resolution
/delivery/bulk/validateBuilds a dispatcher exception group from the same order-code input used by existing bulk operations. order_codes accepts the complete group from list selection, manual entry, or scanned DEL-prefixed shipment labels. Inputs are normalized and deduplicated. There is no exception-resolution product count limit; the backend divides the group into internal 100-order lookup chunks with at most three read chunks in flight.
The backend resolves each visible parent or child code to its current open delivery exception. If one logical order has multiple equally applicable open exceptions, that row returns multiple_open_delivery_exceptions and must be resolved individually. A second submitted code resolving to the same exception returns duplicate_delivery_ids_not_allowed.
Each row's actions is the intersection of the exception type's effective configured action_keys and the actions operationally valid for that order's current status/custody. Configuration can remove an action but cannot make an operationally invalid action executable. Custody awaiting_warehouse_receipt always returns an empty row actions array because physical warehouse receipt has not completed; consequently the top-level available_actions is empty when any selected row is waiting for receipt. Otherwise, top-level available_actions is the intersection across the complete submitted group and is empty if any row is invalid or if no action fits every row. Action objects contain the backend-localized label and description; Return is labeled Return to sender and described as Send the parcel back to its original sender. Configured Cancel follows dispatcher cancellation eligibility outside the warehouse-receipt wait and is offered with reason_required=true for eligible non-terminal logical orders. Retry reports override_reason_required=true when at least one selected order reached its delivery-attempt limit. Exception and delivery database IDs remain backend-internal.
Frontend integration contract
accumulate the complete list selection, typed codes, and scanner values in one order_codes array; submit action_id=resolve_exception; and render only data.available_actions. Complete pending warehouse receiving work before selecting any row whose custody is awaiting_warehouse_receipt. Do not split the group, enforce a count limit, store exception IDs, or derive outcomes from delivery status or settings.
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
order_codes | array<string> | — |
action_id | string | — |
params | object | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/bulk/validate" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "order_codes": [6 "1865",7 "DEL1853"8 ],9 "action_id": "resolve_exception",10 "params": {}11}'Response examples
1{2 "success": true,3 "message": "Validation completed",4 "data": {5 "results": [6 {7 "order_code": "1865",8 "valid": true,9 "exception_type": "refused",10 "actions": [11 {12 "key": "retry",13 "label": "Retry delivery",14 "description": "Try to deliver the parcel again",15 "override_reason_required": false16 }17 ]18 },19 {20 "order_code": "1853",21 "valid": true,22 "exception_type": "not_home",23 "actions": [24 {25 "key": "retry",26 "label": "Retry delivery",27 "description": "Try to deliver the parcel again",28 "override_reason_required": true29 }30 ],31 "attempt": {32 "number": 2,33 "allowed": 2,34 "override_reason_required": true35 }36 }37 ],38 "available_actions": [39 {40 "key": "retry",41 "label": "Retry delivery",42 "description": "Try to deliver the parcel again",43 "override_reason_required": true44 }45 ],46 "total_valid": 2,47 "total_failed": 048 }49}Execute Bulk Exception Resolution
/delivery/bulk/executeApplies one exception outcome to the complete group. params.action_key must be the literal retry, return, or cancel key returned by bulk validation. notes, override_reason, and object resolution_details are optional shared values. For Retry, a supplied override_reason is recorded as the retry event's reason, and supplied notes are recorded as its notes. For Cancel, params.reason is required and is applied to every successfully cancelled logical order. No exception IDs are accepted from or returned to the frontend.
Execution divides the normalized group into internal 100-order chunks and processes those chunks sequentially. Each database chunk loads the effective configured action policy once, looks up and locks current open exceptions, revalidates the configured-and-operational action intersection and retry-attempt requirements, and preserves row-level partial success. A disabled or no-longer-valid action, including any action attempted while custody is still awaiting_warehouse_receipt, fails that row with exception_action_not_available. A failed database chunk is reported as exception_action_failed for its orders and later chunks are still attempted. Results are recombined in the original normalized order with total_success and total_failed. Return settings are read once for the complete operation.
Every successful row delegates to the same canonical resolver as single-order exception resolution. It preserves status, custody, assignment, warehouse, return-order, finance, and driver notification behavior. Retry creates one canonical retry event; Return keeps the canonical exception_resolved event. For a warehouse-prepared P&D Retry or Return, the successful row contains the completed picking_order_id and handoff_id: null; driver assignment creates the pending handoff later. Cancel uses the canonical logical-order cancellation owner, reverses applicable finance, cancels active assignments and unfinished child work, records the cancellation reason, and keeps only the single canonical cancelled order event when it resolves the exception with decision cancel. It does not append an additional exception_resolved event. If cancellation fails, that exception remains open and the row fails.
Frontend integration contract
disable Confirm while execute is pending; send the complete selected group once with one chosen action_key; when the chosen action has reason_required=true, send one non-empty params.reason; branch on row code, display row error_message, and keep failed rows available for review. Complete the linked warehouse receiving order and validate again before retrying a row blocked by pending warehouse receipt. For a successful warehouse P&D Retry or Return, do not expect a handoff until the delivery is assigned. Do not split the request or enforce an order-count limit. The backend owns chunking, exception lookup, and revalidation.
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
order_codes | array<string> | — |
action_id | string | — |
params | object | — |
params.action_key | string | — |
params.notes | string | — |
params.override_reason | string | — |
params.resolution_details | object | — |
params.resolution_details.decision_note | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/bulk/execute" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "order_codes": [6 "1865",7 "DEL1853"8 ],9 "action_id": "resolve_exception",10 "params": {11 "action_key": "retry",12 "notes": "Customers confirmed availability",13 "override_reason": "Dispatcher approved another attempt",14 "resolution_details": {15 "decision_note": "Resolved from bulk operations"16 }17 }18}'Response examples
1{2 "success": true,3 "message": "Bulk operation completed",4 "data": {5 "action_key": "retry",6 "results": [7 {8 "order_code": "1865",9 "success": true,10 "action_key": "retry"11 },12 {13 "order_code": "1853",14 "success": false,15 "code": "delivery_has_no_open_exception",16 "error_message": "This delivery has no open exception"17 }18 ],19 "total_success": 1,20 "total_failed": 121 }22}Bulk Cancel Orders
/delivery/bulk/executeCancels each selected logical order using the same atomic cancellation and family financial-reversal workflow as individual cancellation. For staged/P&D, submitting the parent or any operational child code resolves to the parent in one batched lookup. A completed inbound warehouse leg is accepted while the parent/final outbound leg remains cancellable. The transaction cancels the parent and every non-terminal operational child, including Draft. Assignments without parcel custody are cancelled and their capacity is released immediately. An assignment already picked_up or en_route_to_delivery keeps that custody status and driver/capacity for audit, but its delivery leg is cancelled, hidden from the default driver task list, and cannot move through normal delivery states. Receiving work, package handoffs, and eligible picking orders are cancelled; terminal child history and separate replacement-return orders are preserved. Successful delivery-fee payments and collected COD are resolved across the parent/children and refunded once to the sender wallet. Parent and child codes from the same staged order are deduplicated into one cancellation while every submitted code receives the same result. Different logical orders preserve bulk partial-success behavior and return HTTP 200 with per-order results. Frontend integration: send visible order codes and required params.reason; do not resolve child IDs or unassign drivers first. Cancelled/returned parent and child rows keep the historical assigned driver in driver and assignment for dispatcher/admin list and detail display. Driver frontend should use GET /delivery/driver-deliveries?status=cancelled or status=returned only for terminal-status tabs; the default task list excludes those statuses.
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
order_codes | array<string> | — |
action_id | string | — |
params | object | — |
params.reason | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/bulk/execute" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "order_codes": [6 "1865",7 "1853"8 ],9 "action_id": "cancel_orders",10 "params": {11 "reason": "Cancelled by dispatcher"12 }13}'Response examples
1{2 "success": true,3 "message": "Bulk operation completed",4 "data": {5 "results": [6 {7 "order_code": "1865",8 "success": true9 },10 {11 "order_code": "1853",12 "success": true13 }14 ],15 "total_success": 2,16 "total_failed": 017 }18}Search Orders
/delivery/orders/searchCompact autocomplete search for bulk-operation and other admin/dispatcher order pickers. The primary entity is one logical delivery order: every returned row is a standalone/root delivery (parent_delivery_id=null). Operational child legs are never returned. When q matches a child delivery code or barcode, the backend resolves that child to its root order before deduplication, stable ordering, and pagination, so a hybrid order appears once.
Authentication and access
Requires a tenant-authenticated bearer token.
Results are tenant-scoped.
Users restricted by teams receive only customers and delivery services available to their teams, matching the normal delivery-list rules.
Ordering and pagination
Stable order is
created_at DESC, id DESC.The endpoint reads
limit + 1rows to calculatehas_more; it does not run an exact count.next_pageis the next page number when more rows exist, otherwisenull.
Money contract
amount_to_collectis the outstanding physical amount for the logical order. It equals positivecod_amountplus a positive delivery fee only when the delivery-feepayment_methodiscash/codandpayment_statusis notpaid.COD is independent from the delivery-fee payment method.
payment_status=refundedmeans a verified positive delivery-fee credit was returned; it is never inferred from cancellation/return status or from a COD-only credit.Delivered, failed, cancelled, and returned orders return
amount_to_collect=null; any other row with no outstanding amount also returnsnull.
Frontend integration
render customer.full_name, delivery_location.full_address, delivery_services.name, status, and amount_to_collect. Store the selected result object for the pre-execute review, but submit only its parent delivery_code to bulk validate/execute. Use pagination.has_more and pagination.next_page for infinite scroll.
Query parameters
| Parameter | Type | Description |
|---|---|---|
qOptional | string | Optional partial search across root order code/barcode, external reference, customer, service, and operational child code/barcode. A child match resolves to its logical parent. Example: |
page | string | Page number. Defaults to 1. Example: |
limit | string | Rows per page. Defaults to 20 and is capped at 100. Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/orders/search?q=1865&page=1&limit=20" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Orders retrieved successfully",4 "data": {5 "results": [6 {7 "id": "parent-or-standalone-uuid",8 "delivery_code": "1865",9 "parent_delivery_id": null,10 "is_parent": true,11 "external_reference_id": "EXT-1001",12 "barcode": "DEL1865",13 "customer": {14 "id": "customer-uuid",15 "full_name": "John Doe",16 "phone": "+9647000000000"17 },18 "customer_name": "John Doe",19 "pickup_location": {20 "full_address": "Warehouse A"21 },22 "delivery_location": {23 "full_address": "123 Main St"24 },25 "address": "123 Main St",26 "delivery_services": {27 "id": "service-uuid",28 "name": "Same Day",29 "delivery_type": {30 "id": "type-uuid",31 "name": "Parcel"32 }33 },34 "status": "pending",35 "unit_size": 1,36 "cod_amount": 10000,37 "payment_method": "cash",38 "payment_status": "pending",39 "amount_to_collect": 12000,40 "delivery_type": "parcel",41 "direction": null,42 "requires_warehouse_staging": true,43 "driver": {44 "id": "driver-uuid",45 "full_name": "Driver Name"46 },47 "priority": 1,48 "has_open_exception": false,49 "created_at": "2026-08-03T07:30:00.000Z"50 }51 ],52 "pagination": {53 "page": 1,54 "limit": 20,55 "has_more": true,56 "next_page": 257 }58 }59}Bulk Print — Shipping Labels
/delivery/bulk/printReturns shipment_label_url from each delivery record. If not generated yet, url is null with a message. Frontend handles opening/printing.
Query parameters
| Parameter | Type | Description |
|---|---|---|
order_codes[] | string | Order code Example: |
order_codes[] | string | Order code Example: |
type | string |
Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/bulk/print?order_codes[]=1865&order_codes[]=1853&type=shipping_label" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Print data retrieved successfully",4 "data": {5 "type": "shipping_label",6 "results": [7 {8 "order_code": "1865",9 "url": "https://label-url...",10 "status": "generated"11 },12 {13 "order_code": "1853",14 "url": null,15 "status": "pending",16 "message": "label_not_generated"17 }18 ]19 }20}Generate Bulk Shipment Labels
/delivery/bulk/shipment-labelsGenerates one combined shipment-label PDF from the canonical single-label PDFs. Requires a tenant-authenticated bearer token. Successful and processing-error responses include X-Correlation-ID, which matches the bulk, shipment-label, batch-generation, and renderer timing logs.
Request
delivery_codes is required and accepts 1-200 non-empty typed delivery codes or printed label barcodes. DEL-prefix normalization: values are trimmed and uppercased, a generated DEL prefix before a numeric code is removed, and canonical duplicates are removed in first-seen order. Mixed values such as 2501, DEL2502, and DEL2501 therefore print two labels. The 200-code limit is checked after normalization and de-duplication. Optional print_source is bulk (default), manifest, or order_screen and is stored in the successful label-generation event.
Canonical labels
A label is regenerated only when its URL is missing, it needs reprinting or recovery (active generation is joined), or its stored PDF is unreadable. Initially missing or stale labels are split into parts of at most 50, with no more than two internal generate-pdf Edge invocations running concurrently. Each invocation generates up to 16 labels simultaneously and returns an ordered result for every requested delivery. It always reads receiver name, phone, address, and zone from the delivery's current delivery_location. Each successful generation uses the next numeric file version, such as <delivery_id>-1.pdf then <delivery_id>-2.pdf; older files and log rows remain unchanged. The delivery becomes success only after the new file is ready, and order_events receives shipment_label_generated with old/new URLs, actor, source, timestamp, and is_reprint.
Large-batch output
Every successful request for 1-200 labels returns one combined PDF in data.url. The service reads only the five delivery and label fields it needs, keeps 20 source downloads prefetched, and appends exactly one page per delivery sequentially in first-seen request order. It does not create orders and does not return multiple PDF parts.
All-or-nothing response
Every requested label must be obtained, parsed, and contribute exactly one page. If any label fails, the endpoint returns an error and no partial PDF URL. A retry reuses canonical labels that completed previously and an existing combined artifact when available. A failed reprint keeps the previous URL and leaves the delivery at needs_reprint.
Private cache
The V2 cache key uses the ordered delivery ID, canonical label version, and canonical label URL. Unrelated delivery updates therefore do not invalidate a combined PDF, while a regenerated label always produces a new cache key.
Errors
A tenant-scoped unknown delivery returns HTTP 404 with stable data.code=delivery_not_found. The localized top-level message names the submitted unknown codes, and data.delivery_codes preserves those submitted values. If Supabase rate-limits an internal batch invocation, the endpoint returns HTTP 503 with data.code=label_generation_rate_limited, data.retry_after_ms, data.retry_after_seconds, and a matching Retry-After response header.
Limits
200 deliveries after normalization and de-duplication, 50 missing/stale labels per internal Edge invocation, at most two generation invocations concurrently, generation concurrency 16 inside each invocation, 2 MB per source PDF, 100 MB cumulative source bytes for the combined PDF, 50 MB output, and source prefetch 20.
Frontend integration
Send typed codes and scanner values unchanged; both 3053 and DEL3053 resolve to the same delivery. Send print_source=manifest from the manifest button and print_source=bulk from Bulk Operations. On HTTP 200, wait for and download the single data.url; data.files is no longer returned by this endpoint. Handle 400 invalid body/source/code or batch limit, 404 tenant-scoped delivery not found, 413 byte limit, 422 invalid canonical PDF or non-single-page canonical label, 500 lookup/cache/signing/upload errors, 502 canonical generation/download errors, and 503 temporary internal generation throttling. For HTTP 503, honor Retry-After before retrying the same ordered request.
Concurrent printing
Successful labels with a URL are reused. Missing or needs_reprint labels use an atomic shared claim. A queued creation job can be claimed immediately by either foreground printing or its background worker. The atomic delivery-row lock permits only one generator; an unexpired active generator is joined instead of superseded, and delayed background jobs reuse completed labels. Generation and batch polling share a 90-second budget per request, leaving time for PDF merge/upload. Polls fetch pending IDs together. If the budget expires, HTTP 503 with code label_generation_in_progress asks the caller to retry; no partial PDF is returned. A later request may recover an abandoned active claim after 120 seconds; unstarted queued jobs have no grace-period delay. Version checks fence off superseded results. Unreadable-PDF repair explicitly replaces the observed version, and concurrent repairs reuse its replacement. Creation and reprint triggers are unchanged. On this endpoint, the code is in data.code, with data.retry_after_seconds=2 and Retry-After: 2. Existing successful response fields and 200-label limit are unchanged.
G2G inputs
delivery_codes also accepts G2G item barcodes mixed with normal and DEL-prefixed order codes. Normal order matches take precedence. Unmatched codes use one tenant-scoped indexed institution_item_barcodes lookup and one tenant-scoped, soft-delete-aware linked-delivery lookup. No orders are created. Deduplicate resolved delivery IDs in first-seen order before generating PDFs or cache keys. delivery_count reports distinct orders.
Validation-only
Optional boolean validate_only=true performs the same lookup without PDF generation, events, or storage access. The 200 normalized-input limit still applies. HTTP 200 data.results preserves every submitted input in order, including duplicates. Each row has input_code, valid, and either delivery_id/delivery_code or code/message. Known unlinked G2G codes return g2g_barcode_has_no_order; unknown codes and inaccessible/deleted linked orders return delivery_not_found. Messages are localized. data.delivery_count counts distinct eligible orders. Invalid rows still return HTTP 200 in validation-only mode. Non-boolean validate_only returns HTTP 400.
Print errors
Any unresolved input prevents printing. HTTP 404 data.code=g2g_barcode_has_no_order means the selection contains unlinked G2G barcodes; data.delivery_codes lists all unresolved submitted inputs, and data.g2g_codes_without_order lists normalized unlinked barcodes. Unknown-only selections retain delivery_not_found.
Frontend
Validate the entire typed/scanned selection in groups of up to 200. Show invalid inputs for correction/removal. Collect valid delivery_id/delivery_code mappings from ALL groups, deduplicate globally by delivery_id, then split canonical codes into print batches of 200. This prevents cross-batch duplicate labels. Keep original inputs in error feedback. Printing rechecks current orders and labels. Successful PDF response fields are unchanged.
G2G return labels
When a linked return's original canonical G2G order carries a manifest_origin_v1 snapshot, generation uses the independent manifest_return_shipment_label assignment (default template manifest_return_shipment_label_v1). The label uses the return order's code, barcode, service, creation date, pickup warehouse address, destination and zones; only the original manifest origin and item reference come from the original snapshot. Outbound G2G orders retain manifest_shipment_label, and ordinary returns retain their existing labels. Request and response fields are unchanged.
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
delivery_codes | array<string> | — |
print_source | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/bulk/shipment-labels" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "delivery_codes": [6 "2501",7 "DEL2502",8 "DEL2501"9 ],10 "print_source": "bulk"11}'Response examples
1{2 "success": true,3 "message": "Shipment labels generated successfully",4 "data": {5 "url": "https://.../storage/v1/object/sign/shipment-label-pdfs/bulk-shipment-labels-v2/tenant-uuid/cache-key.pdf?token=...",6 "delivery_count": 2,7 "cached": false,8 "expires_in": 9009 }10}Print Return Shipment Labels
/delivery/bulk/return-shipment-labelsResolves selected original P&D orders to their linked return-generated orders and creates one combined PDF from the linked return orders' canonical shipment labels. Requires a tenant-authenticated bearer token with delivery view/management access.
Request
original_delivery_codes is required and accepts 1-150 non-empty order-code strings after trimming, DEL-prefix normalization, and de-duplication. Send original order codes retained by frontend bulk selection across pages.
Resolution and access
The endpoint resolves originals in one tenant-scoped, soft-delete-aware lookup and applies the same delivery-service/customer team restrictions as the delivery list. It then resolves all return_delivery_id values in one tenant-scoped lookup. A linked row is eligible only when it points back through original_delivery_id to the selected original. The endpoint never falls back to the original order's label.
Partial structural eligibility
Missing, child, return-generated, unlinked, deleted, inconsistent, or duplicate-linked selections are returned as status=skipped with a stable row code and localized message. Eligible linked returns are still printed. HTTP 422 no_eligible_return_orders is returned when every selection is skipped.
Label correctness and failure boundary
Only linked return delivery codes are passed to the existing canonical bulk-label service. That service reads/generates each return delivery's own shipment label, whose payload contains that return delivery's code and barcode. The combined PDF is all-or-nothing for eligible returns: if any eligible return label cannot be generated/read/merged, no PDF URL is returned and every eligible row is status=blocked; structural skips remain status=skipped. Existing POST /delivery/bulk/shipment-labels behavior is unchanged.
Success data
url, delivery_count, cached, expires_in, results, total_printed, and total_skipped. Successful rows include original_delivery_code, return_delivery_id, return_delivery_code, return_barcode, success=true, and status=printed.
Frontend integration
Show the action as Print return shipment labels on original-order selections. Download data.url only on HTTP 200. Display skipped row messages after a partial success. On non-2xx, do not print; display blocked/skipped row feedback from data.results.
Concurrent printing
Successful labels with a URL are reused. Missing or needs_reprint labels use an atomic shared claim. A queued creation job can be claimed immediately by either foreground printing or its background worker. The atomic delivery-row lock permits only one generator; an unexpired active generator is joined instead of superseded, and delayed background jobs reuse completed labels. Generation and batch polling share a 90-second budget per request, leaving time for PDF merge/upload. Polls fetch pending IDs together. If the budget expires, HTTP 503 with code label_generation_in_progress asks the caller to retry; no partial PDF is returned. A later request may recover an abandoned active claim after 120 seconds; unstarted queued jobs have no grace-period delay. Version checks fence off superseded results. Unreadable-PDF repair explicitly replaces the observed version, and concurrent repairs reuse its replacement. Creation and reprint triggers are unchanged. This endpoint returns the stable code in top-level code, preserving its blocked/skipped result envelope.
G2G return labels
When a linked return's original canonical G2G order carries a manifest_origin_v1 snapshot, generation uses the independent manifest_return_shipment_label assignment (default template manifest_return_shipment_label_v1). The label uses the return order's code, barcode, service, creation date, pickup warehouse address, destination and zones; only the original manifest origin and item reference come from the original snapshot. Outbound G2G orders retain manifest_shipment_label, and ordinary returns retain their existing labels. Request and response fields are unchanged.
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
original_delivery_codes | array<string> | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/bulk/return-shipment-labels" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "original_delivery_codes": [6 "2501",7 "2502"8 ]9}'Response examples
1{2 "success": true,3 "message": "Return shipment labels generated successfully",4 "data": {5 "url": "https://.../combined-return-labels.pdf",6 "delivery_count": 1,7 "cached": false,8 "expires_in": 900,9 "results": [10 {11 "original_delivery_code": "2501",12 "return_delivery_id": "return-delivery-uuid",13 "return_delivery_code": "2601",14 "return_barcode": "DEL2601",15 "success": true,16 "status": "printed"17 },18 {19 "original_delivery_code": "2502",20 "return_delivery_id": null,21 "return_delivery_code": null,22 "return_barcode": null,23 "success": false,24 "status": "skipped",25 "code": "return_order_not_created",26 "message": "No linked return order has been created for this order"27 }28 ],29 "total_printed": 1,30 "total_skipped": 131 }32}Bulk Print — Receipts
/delivery/bulk/printCreates one ordered, printable PDF containing one primary receipt per requested delivery and returns its signed URL in data.url. order_codes[] accepts bare codes such as 1865 and scanned barcodes such as DEL1865; values are trimmed, normalized case-insensitively, and canonical duplicates are processed once. Maximum batch size is 150. Every delivery must exist and have a generated successful receipt. For a staged/P&D parent code, receipt lookup covers that parent and the operational children in its sequence_plan, because payment collection can be recorded on the relevant child; a directly submitted child code remains scoped to that exact child. When multiple family receipts are ready, COD_COLLECTION is preferred over DELIVERY_FEE, then the newest receipt is selected. Payment lookup remains batched, uses bounded parallel chunks for large parent selections, and does not query once per order. The combined file is cached by ordered receipt identity; signed URLs expire after 1 hour. data.results[] remains available for compatibility and contains each submitted canonical order code and individual signed receipt URL. The request is all-or-nothing: an unknown code returns HTTP 404 and names the submitted code; any unready receipt returns HTTP 409 with machine code receipt_not_ready. Frontend integration: submit scanner output unchanged and open or print data.url after HTTP 200. Parent codes do not need to be replaced with child codes. Show the localized top-level message for errors and use code only for branching.
Query parameters
| Parameter | Type | Description |
|---|---|---|
order_codes[] | string | Order code Example: |
order_codes[] | string | Bare delivery code or scanned DEL-prefixed barcode Example: |
type | string |
Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/bulk/print?order_codes[]=1865&order_codes[]=DEL1853&type=receipt" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Print data retrieved successfully",4 "data": {5 "type": "receipt",6 "url": "https://example.supabase.co/storage/v1/object/sign/shipment-label-pdfs/bulk-payment-receipts/tenant-uuid/cache-key.pdf?token=...",7 "delivery_count": 2,8 "cached": false,9 "expires_in": 3600,10 "results": [11 {12 "order_code": "1865",13 "url": "https://example.supabase.co/storage/v1/object/sign/documents/delivery-receipts/payment-1.pdf?token=...",14 "receipt_generated_at": "2026-09-06T10:00:00Z"15 },16 {17 "order_code": "1853",18 "url": "https://example.supabase.co/storage/v1/object/sign/documents/delivery-receipts/payment-2.pdf?token=...",19 "receipt_generated_at": "2026-09-06T10:01:00Z"20 }21 ]22 }23}