Exceptions
6 API calls in this section.
Endpoints for managing delivery exceptions, which record and track exceptional situations that occur during the delivery process. Exceptions can be reported by drivers or operations managers, and include details about the issue, its resolution, and impact on delivery.
List Exceptions
/delivery/exceptionsList delivery exceptions with pagination, sorting, column filters and stats. Exception status is always open or resolved. Each open exception includes only the actions that are both tenant-configured and operationally valid. Custody awaiting_warehouse_receipt means a receiving order exists but the parcel has not yet been received back into warehouse custody, so actions is empty for every delivery type. After warehouse receipt changes custody to staged_at_warehouse, a P&D outbound returns its configured Retry and Return actions while an operational return returns its configured Retry action. Each action is { key, label, description }; resolved exceptions return an empty array. A failed customer-delivery exception also includes attempt with number, allowed count, driver, date and reason; operational-return failures are excluded from customer attempt counting. Requires manage:operations:tenant or view:tenant permission.
Query parameters
| Parameter | Type | Description |
|---|---|---|
page | string | Page number (default: 1) Example: |
limit | string | Items per page (default: 10) Example: |
status | string | Filter by exception status: open | resolved |
delivery_id | string | Filter by delivery UUID (standalone param) |
driver_id | string | Filter by driver UUID (standalone param) |
exception_type | string | Filter by exception type (standalone param) |
sort_by | string | Sort column: |
sort_order | string | asc or desc (default: desc) Example: |
column_filters | string | JSON object. Keys: |
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/exceptions?page=1&limit=10&status=&delivery_id=&driver_id=&exception_type=&sort_by=&sort_order=desc&column_filters=&tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "exceptions_retrieved_successfully",4 "exceptions": [5 {6 "id": "uuid",7 "exception_type": "address_issue",8 "status": "open",9 "created_at": "2025-01-15T08:00:00Z",10 "resolved_at": null,11 "resolution_type": null,12 "actions": [13 {14 "key": "retry",15 "label": "Retry delivery",16 "description": "Try to deliver the parcel again"17 },18 {19 "key": "return",20 "label": "Return to sender",21 "description": "Send the parcel back to its original sender"22 }23 ],24 "exception_details": {25 "trigger_point": "delivery",26 "reason": "Address not found"27 },28 "delivery": {29 "id": "uuid",30 "status": "exception",31 "return_status": null,32 "external_reference_id": "EXT-001",33 "delivery_type": "OnDemand",34 "delivery_type_id": "452ffddf-a8d9-4d16-a5ea-46d16491979c",35 "delivery_fee": 1036 },37 "driver": {38 "id": "uuid",39 "full_name": "John Driver"40 }41 }42 ],43 "page": 1,44 "limit": 10,45 "total": 42,46 "stats": {47 "totalExceptions": 200,48 "openCount": 70,49 "resolvedCount": 13050 }51}List Available Exception Types
/delivery/exceptions/availableReturns tenant-configured exception types valid for the current delivery status and caller role. Exception-type settings control the type code, label, allowed statuses, optional allowed roles, and later resolution permissions through action_keys. Missing allowed_statuses means all non-terminal delivery statuses for backwards compatibility.
Business rules
Backend loads the delivery by
delivery_idin the authenticated tenant.At
arrived_at_warehouseorstaged_at_warehouse, a P&D parent remains the exception target while its outbound is Draft/Pending and has no active assignment. Once the outbound is assigned or moving, the active outbound child is the target.Delivered, Failed, Cancelled, and Returned deliveries return an empty
exception_typesarray.An exception type is returned only when its allowed status and optional role rules match.
When the created exception is later resolved, its offered actions are its configured
action_keysintersected with its current operationally valid actions.
Frontend integration contract
Render
exception_typesonly. Do not select a resolution action during exception creation.Submit the selected order ID and
exception_type=exception_types[].codetoPOST /delivery/exceptions.Display localized
messagefor HTTP 400/404/409.
Query parameters
| Parameter | Type | Description |
|---|---|---|
delivery_id | string | Delivery/order UUID whose current status is used to filter exception types Example: |
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/exceptions/available?delivery_id=${DELIVERY_ID}&tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Available exception types retrieved successfully",4 "delivery_id": "uuid-child-outbound",5 "delivery_status": "en_route_to_pickup",6 "requested_delivery_id": "uuid-parent",7 "resolved_from_parent_delivery_id": "uuid-parent",8 "order_statuses": [9 "draft",10 "pending",11 "assigned",12 "en_route_to_pickup",13 "picked_up",14 "in_transit",15 "arrived_at_warehouse",16 "staged_at_warehouse"17 ],18 "exception_types": [19 {20 "code": "vehicle_issue",21 "label": "Vehicle Issue",22 "allowed_statuses": [23 "en_route_to_pickup",24 "picked_up",25 "in_transit"26 ]27 }28 ]29}Create Exception
/delivery/exceptionsCreate a new delivery exception. Can be created by either the assigned driver or a user with manage:operations:tenant permission.
delivery_idis required and may be a direct delivery, P&D child, P&D parent, or operational return ID.driver_idis optional. If omitted and the actual target has an active assignment, backend stores that assignment driver.exception_typemust be tenant-configured and valid for the actual target's status.Backend always creates the exception as
open; status snapshots, attempt data and custody are server-owned.
Warehouse-origin custody rule
If status at exception is Picked up or In transit and
pickup_warehouse_idexists, the same transaction creates a pending normal receiving order for that warehouse. This rule is based on custody, not delivery subtype.An Arrived-at-Warehouse staged P&D parent reuses and links its existing staged receiving order; no duplicate receiving order is created.
exception.exception_details.custodyreturnsstate=awaiting_warehouse_receipt,warehouse_id, andreceiving_order_id. This means the receiving order is pending and physical warehouse custody has not yet been restored, soactionsis empty until warehouse staff complete that receiving order.For P&D, a moving exception remains on the active outbound child; no duplicate parent exception is created.
Business rules
Delivered, Failed, Cancelled, and Returned deliveries reject creation with HTTP 409
exception_not_allowed_for_terminal_delivery.Only one open exception is allowed on the actual target.
Creating an exception snapshots the lifecycle state and changes the target to
exception. Customer-delivery attempts are recorded separately; operational returns never increment that count.The generated or linked receiving order is warehouse custody work, not inventory stock and not a new return-to-sender decision.
Frontend integration contract
Submit the visible order ID; backend chooses the correct target.
Use
exception.delivery_id/resolved_delivery_idas the actual target.Treat
exception.exception_details.custody.receiving_order_idas context; the warehouse discovers the work through its normal scan/receiving flow.Do not show or submit an exception resolution action while
actionsis empty.Do not send server-owned snapshot, attempt, or custody fields.
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
delivery_id | string | — |
driver_id | null | — |
exception_type | string | — |
exception_details | object | — |
exception_details.reason | string | — |
exception_details.severity | string | — |
exception_details.location | object | — |
exception_details.location.latitude | number | — |
exception_details.location.longitude | number | — |
exception_details.location.address | string | — |
exception_details.photos | array<object> | — |
exception_details.photos.url | string | — |
Show 8 more body fields
Additional body parameters
| Parameter | Type | Description |
|---|---|---|
exception_details.photos.timestamp | string | — |
exception_details.photos.caption | string | — |
exception_details.customer_contacted | boolean | — |
exception_details.customer_contact_method | string | — |
exception_details.customer_contact_timestamp | string | — |
exception_details.customer_response | string | — |
exception_details.eta_impact | number | — |
notes | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/exceptions?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "delivery_id": "delivery-uuid",6 "driver_id": null,7 "exception_type": "address_issue",8 "exception_details": {9 "reason": "Address not found",10 "severity": "high",11 "location": {12 "latitude": 37.7749,13 "longitude": -122.4194,14 "address": "123 Invalid Street"15 },16 "photos": [17 {18 "url": "https://example.com/photo1.jpg",19 "timestamp": "2023-08-01T14:28:00Z",20 "caption": "Location where address should be"21 }22 ],23 "customer_contacted": true,24 "customer_contact_method": "phone",25 "customer_contact_timestamp": "2023-08-01T14:35:00Z",26 "customer_response": "Will provide updated address",27 "eta_impact": 4528 },29 "notes": "Customer provided incorrect address"30}'Response examples
1{2 "success": true,3 "message": "Exception created successfully",4 "exception": {5 "id": "uuid-exception",6 "tenant_id": "uuid-tenant",7 "delivery_id": "uuid-child-outbound",8 "driver_id": "uuid-driver",9 "exception_type": "not_home",10 "status": "open",11 "reported_at": "2026-08-19T10:30:00Z",12 "resolved_at": null,13 "resolution_type": null,14 "reported_by": "uuid-user",15 "resolved_by": null,16 "exception_details": {17 "reason": "Customer was not available",18 "delivery_status_at_exception": "in_transit",19 "parent_delivery_id_at_exception": "uuid-parent",20 "parent_delivery_status_at_exception": "in_transit",21 "attempt": {22 "number": 2,23 "allowed": 2,24 "limit_reached": true,25 "driver_id": "uuid-driver",26 "failed_at": "2026-08-19T10:30:00Z",27 "reason": "not_home"28 },29 "custody": {30 "state": "awaiting_warehouse_receipt",31 "warehouse_id": "uuid-warehouse",32 "receiving_order_id": "uuid-receiving-order"33 }34 },35 "resolution_details": null,36 "notes": "Customer was not available",37 "created_at": "2026-08-19T10:30:00Z",38 "updated_at": "2026-08-19T10:30:00Z"39 },40 "requested_delivery_id": "uuid-child-outbound",41 "resolved_delivery_id": "uuid-child-outbound",42 "resolved_from_parent_delivery_id": null43}Update Exception
/delivery/exceptions/:exceptionIdEdit exception information only. Use POST /delivery/exceptions/:exceptionId/resolve for the fixed retry or return decision. Sending status=resolved here returns HTTP 400 code=exception_action_required. A successful real exception_type change atomically appends one exception_edited entry; notes/details-only and no-op updates do not add that event. Its changes array contains { field: "exception_type", old_value, new_value }, and metadata contains source=delivery_exception_update, exception_id, and the affected delivery identity. Direct deliveries store it once on themselves. A P&D child exception stores the event on that child's own order_events and on the canonical parent's order_events in the same transaction. The delivery detail endpoint returns order_events for the requested row. Frontend must render exception.actions, send the selected literal key to the resolve endpoint, and display localized messages.
Path parameters
| Parameter | Type | Description |
|---|---|---|
exceptionIdRequired | string | ID of the exception to update Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
exception_type | string | — |
exception_details | object | — |
exception_details.reason | string | — |
exception_details.severity | string | — |
notes | string | — |
1curl --globoff --request PUT "https://staging-api.ondi.io/functions/v1/delivery/exceptions/${EXCEPTIONID}?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "exception_type": "address_issue",6 "exception_details": {7 "reason": "Address not found",8 "severity": "medium"9 },10 "notes": "Customer provided more address information"11}'Response examples
1{2 "success": true,3 "message": "Exception updated successfully",4 "exception": {5 "id": "uuid",6 "tenant_id": "uuid",7 "delivery_id": "uuid",8 "driver_id": "uuid",9 "exception_type": "address_issue",10 "status": "open",11 "reported_at": "2023-08-01T14:30:00Z",12 "resolved_at": null,13 "resolution_type": null,14 "reported_by": "uuid",15 "resolved_by": null,16 "exception_details": {17 "reason": "Address not found",18 "severity": "medium"19 },20 "resolution_details": null,21 "notes": "Customer provided more address information",22 "created_at": "2023-08-01T14:30:00Z",23 "updated_at": "2023-08-01T15:45:00Z"24 }25}Resolve Exception Action
/delivery/exceptions/:exceptionId/resolveResolve a failed-delivery exception. Requires delivery-management permission.
Request
Submit a literal
action_keyreturned byexception.actions:retry,return, orcancel.notes,override_reason, and objectresolution_detailsare optional. For Retry, a suppliedoverride_reasonis recorded as the retry event'sreason, and suppliednotesare recorded as itsnotes.override_reasonapplies only to counted customer-delivery attempts.For Cancel, top-level
reasonis required and must be a non-empty string.
Business rules
The backend offers and executes only the intersection of the exception type's effective configured
action_keysand its current operationally valid actions. Directly submitting a disabled or invalid action returnsexception_action_not_available.Custody
awaiting_warehouse_receiptblocks Retry, Return, and Cancel for every delivery type. A pending receiving order is an instruction to return the parcel, not proof that warehouse custody has been restored. A direct action request during this state returns HTTP 409exception_action_not_available.Normal warehouse receipt changes custody to
staged_at_warehouse, restores parcel stock through the receiving order, removes the old route/trip, marks the old assignment Unassigned, releases driver capacity, and leaves the exception open. The configured actions are then recalculated.For an exception captured at
assignedoren_route_to_pickup, Retry preserves the same active assignment and restores that exact status. This rule also applies when a warehouse-origin parcel already hasstaged_at_warehousecustody because it never left the warehouse; staged custody alone is not proof of a post-pickup receipt.Outside the warehouse-receipt wait, configured Cancel follows the canonical dispatcher cancellation boundary for a non-terminal logical order. Cancel delegates to the canonical logical-order cancellation owner. Applicable finance is reversed, active assignments/capacity and unfinished P&D work are cancelled, the normal cancellation audit is kept, and the exception is resolved with decision
cancelonly if the cancellation succeeds.Retry at or above the allowed-attempt limit requires
override_reason; without it the API returns HTTP 409delivery_attempt_limit_reachedwith attempt details.After warehouse receipt, P&D outbound Retry makes Leg B Pending. Return to Sender keeps the hybrid outbound delivery Failed whether or not a delivery attempt exists, and creates the existing separate operational return. Both actions create or reuse one completed picking order and reserve the restored stock, but create no handoff. Driver assignment creates the pending handoff for that new custody cycle.
A parent exception created while the parcel is already
staged_at_warehousemay Return when its unused outbound is Draft or Pending. The parent remains Staged at warehouse, the old outbound becomes Failed, and one prepared operational return is created atomically.A pre-pickup Return closes only the unused pick, reservation, assignment, and pending handoff before preparing the operational return. A pre-pickup Retry preserves that existing work unchanged.
Only the explicit Cancel action produces a Cancelled delivery. Return approval does not refund the original delivery fee. The configured terminal refund policy is evaluated idempotently only when the operational return is delivered and the original order becomes Returned.
Operational-return Retry before pickup restores
assignedoren_route_to_pickupwith the same assignment. After warehouse receipt, Retry changes the same operational return to Pending for redispatch, creates a new completed pick and reservation, and waits for assignment to create a new handoff. It creates no nested return order and does not count a customer-delivery attempt.Repeating the same decision remains idempotent.
If a database transition guard detects stale or internally inconsistent state, the API returns HTTP 409
invalid_status_transition; no partial Return work is committed.
Frontend integration contract
Render and submit only the literal
exception.actions[].key; do not derive availability from settings or status.While custody is
awaiting_warehouse_receipt,exception.actionsis empty. Do not show or submit Retry, Return, or Cancel. Warehouse staff must complete the linked receiving order first.When the selected action is
cancel, collect one required reason and send it as top-levelreason.A successful pre-pickup Retry returns the exact restored
data.delivery_status,data.pre_pickup_status, and the preserveddata.assignment_id.After receipt, refetch the exception and render the actions returned by the backend. Successful warehouse Retry or Return returns
data.picking_order_idanddata.handoff_id=null; assign a driver next, then use the assignment-created handoff.Successful operational-return Retry returns
data.retry_mode=warehouse_redispatchand the same delivery becomes Pending.Display localized
message; branch on stablecode.
Path parameters
| Parameter | Type | Description |
|---|---|---|
exceptionIdRequired | string | Open exception ID Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
action_key | string | — |
notes | string | — |
override_reason | string | — |
resolution_details | object | — |
resolution_details.decision_note | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/delivery/exceptions/${EXCEPTIONID}/resolve?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "action_key": "retry",6 "notes": "Parcel is ready for another attempt",7 "override_reason": "Customer confirmed the corrected address",8 "resolution_details": {9 "decision_note": "Retry approved"10 }11}'Response examples
1{2 "success": true,3 "message": "Exception action completed successfully",4 "action_key": "retry",5 "action": {6 "key": "retry",7 "label": "Retry delivery",8 "description": "Try to deliver the parcel again"9 },10 "data": {11 "success": true,12 "action_key": "retry",13 "exception_id": "exception-uuid",14 "delivery_id": "delivery-uuid",15 "delivery_status": "in_transit",16 "root_delivery_id": "delivery-uuid",17 "root_delivery_status": "in_transit",18 "driver_id": "driver-uuid",19 "assignment_id": "assignment-uuid",20 "retry_mode": "resolve_only",21 "pre_exception_status": "in_transit",22 "notify_driver": true,23 "attempt_number": 1,24 "allowed_attempts": 2,25 "idempotent_replay": false26 }27}Get Exception
/delivery/exceptions/:exceptionIdGet one delivery exception. exception.actions is the intersection of the exception type's effective configured action_keys and its current operationally valid actions. Custody awaiting_warehouse_receipt always returns actions: []; exception_details.custody.receiving_order_id identifies the pending warehouse receipt that must be completed first. After receipt, custody becomes staged_at_warehouse, the exception remains open, and the backend exposes the configured actions that are operationally valid for that P&D outbound, parent, or operational return. Outside the receipt wait, configured Cancel is exposed with key cancel for an eligible non-terminal logical order and has reason_required=true. exception.attempts contains counted customer-delivery failures; operational-return failures are excluded. Requires manage:operations:tenant or view:tenant permission.
Path parameters
| Parameter | Type | Description |
|---|---|---|
exceptionIdRequired | string | ID of the exception to retrieve Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_idOptional | string | Tenant ID (optional if in auth token) |
language | string | Preferred language for response messages Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --request GET "https://staging-api.ondi.io/functions/v1/delivery/exceptions/${EXCEPTIONID}?tenant_id=&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Exception retrieved successfully",4 "exception": {5 "id": "uuid",6 "tenant_id": "uuid",7 "delivery_id": "uuid",8 "driver_id": "uuid",9 "exception_type": "address_issue",10 "status": "open",11 "reported_at": "2023-08-01T14:30:00Z",12 "resolved_at": null,13 "resolution_type": null,14 "reported_by": "uuid",15 "resolved_by": null,16 "actions": [17 {18 "key": "retry",19 "label": "Retry delivery",20 "description": "Try to deliver the parcel again"21 },22 {23 "key": "return",24 "label": "Return to sender",25 "description": "Send the parcel back to its original sender"26 }27 ],28 "delivery": {29 "id": "uuid",30 "status": "exception",31 "return_status": null32 },33 "exception_details": {34 "reason": "Address not found",35 "severity": "high"36 },37 "resolution_details": null,38 "notes": "Customer provided incorrect address",39 "created_at": "2023-08-01T14:30:00Z",40 "updated_at": "2023-08-01T14:30:00Z"41 }42}