Skip to main content

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 & Last-Mile / Exceptions
GET/delivery/exceptions

List 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

ParameterTypeDescription
pagestring

Page number (default: 1)

Example: 1

limitstring

Items per page (default: 10)

Example: 10

statusstring

Filter by exception status: open | resolved

delivery_idstring

Filter by delivery UUID (standalone param)

driver_idstring

Filter by driver UUID (standalone param)

exception_typestring

Filter by exception type (standalone param)

sort_bystring

Sort column: exception_type | status | created_at (default) | resolved_at

sort_orderstring

asc or desc (default: desc)

Example: desc

column_filtersstring

JSON object. Keys: exception_type (string), status (open | resolved), resolution_type (string), delivery_id (uuid), driver_id (uuid), trigger_point (string, matches exception_details.trigger_point), resolution_trigger_point (string), delivery_code (string substring match), delivery_type (delivery type UUID via deliveries.delivery_service_id -> delivery_services.delivery_type_id), delivery_fee (number or {min,max}), created_at ({from?, to?}). Example: {"delivery_fee":{"min":10,"max":100},"delivery_type":"452ffddf-a8d9-4d16-a5ea-46d16491979c","delivery_code":"123"}

tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

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

200OK
Response body
json
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 & Last-Mile / Exceptions
GET/delivery/exceptions/available

Returns 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_id in the authenticated tenant.

  • At arrived_at_warehouse or staged_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_types array.

  • 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_keys intersected with its current operationally valid actions.

Frontend integration contract

  • Render exception_types only. Do not select a resolution action during exception creation.

  • Submit the selected order ID and exception_type=exception_types[].code to POST /delivery/exceptions.

  • Display localized message for HTTP 400/404/409.

Query parameters

ParameterTypeDescription
delivery_idstring

Delivery/order UUID whose current status is used to filter exception types

Example: {{delivery_id}}

tenant_idOptionalstring

Tenant ID (optional if in auth token)

languagestring

Preferred language

Example: en

Headers

ParameterTypeDescription
Authorizationstring

Example: Bearer {{access_token}}

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

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

Create a new delivery exception. Can be created by either the assigned driver or a user with manage:operations:tenant permission.

  • delivery_id is required and may be a direct delivery, P&D child, P&D parent, or operational return ID.

  • driver_id is optional. If omitted and the actual target has an active assignment, backend stores that assignment driver.

  • exception_type must 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_id exists, 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.custody returns state=awaiting_warehouse_receipt, warehouse_id, and receiving_order_id. This means the receiving order is pending and physical warehouse custody has not yet been restored, so actions is 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_id as the actual target.

  • Treat exception.exception_details.custody.receiving_order_id as context; the warehouse discovers the work through its normal scan/receiving flow.

  • Do not show or submit an exception resolution action while actions is empty.

  • Do not send server-owned snapshot, attempt, or custody fields.

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
delivery_idstring—
driver_idnull—
exception_typestring—
exception_detailsobject—
exception_details.reasonstring—
exception_details.severitystring—
exception_details.locationobject—
exception_details.location.latitudenumber—
exception_details.location.longitudenumber—
exception_details.location.addressstring—
exception_details.photosarray<object>—
exception_details.photos.urlstring—
Show 8 more body fields

Additional body parameters

ParameterTypeDescription
exception_details.photos.timestampstring—
exception_details.photos.captionstring—
exception_details.customer_contactedboolean—
exception_details.customer_contact_methodstring—
exception_details.customer_contact_timestampstring—
exception_details.customer_responsestring—
exception_details.eta_impactnumber—
notesstring—
Staging
Request
curl
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

201Created
Response body
json
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 & Last-Mile / Exceptions
PUT/delivery/exceptions/:exceptionId

Edit 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

ParameterTypeDescription
exceptionIdRequiredstring

ID of the exception 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
exception_typestring—
exception_detailsobject—
exception_details.reasonstring—
exception_details.severitystring—
notesstring—
Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Exceptions
POST/delivery/exceptions/:exceptionId/resolve

Resolve a failed-delivery exception. Requires delivery-management permission.

Request

  • Submit a literal action_key returned by exception.actions: retry, return, or cancel.

  • notes, override_reason, and object resolution_details are optional. For Retry, a supplied override_reason is recorded as the retry event's reason, and supplied notes are recorded as its notes. override_reason applies only to counted customer-delivery attempts.

  • For Cancel, top-level reason is 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_keys and its current operationally valid actions. Directly submitting a disabled or invalid action returns exception_action_not_available.

  • Custody awaiting_warehouse_receipt blocks 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 409 exception_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 assigned or en_route_to_pickup, Retry preserves the same active assignment and restores that exact status. This rule also applies when a warehouse-origin parcel already has staged_at_warehouse custody 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 cancel only if the cancellation succeeds.

  • Retry at or above the allowed-attempt limit requires override_reason; without it the API returns HTTP 409 delivery_attempt_limit_reached with 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_warehouse may 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 assigned or en_route_to_pickup with 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.actions is 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-level reason.

  • A successful pre-pickup Retry returns the exact restored data.delivery_status, data.pre_pickup_status, and the preserved data.assignment_id.

  • After receipt, refetch the exception and render the actions returned by the backend. Successful warehouse Retry or Return returns data.picking_order_id and data.handoff_id=null; assign a driver next, then use the assignment-created handoff.

  • Successful operational-return Retry returns data.retry_mode=warehouse_redispatch and the same delivery becomes Pending.

  • Display localized message; branch on stable code.

Path parameters

ParameterTypeDescription
exceptionIdRequiredstring

Open exception ID

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
action_keystring—
notesstring—
override_reasonstring—
resolution_detailsobject—
resolution_details.decision_notestring—
Staging
Request
curl
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

200OK
Response body
json
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 & Last-Mile / Exceptions
GET/delivery/exceptions/:exceptionId

Get 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

ParameterTypeDescription
exceptionIdRequiredstring

ID of the exception 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/exceptions/${EXCEPTIONID}?tenant_id=&language=en" \2  --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"

Response examples

200OK
Response body
json
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}