# OnDi API reference

Staging base URL: https://staging-api.ondi.io/functions/v1

946 documented request entries. Replace {{placeholders}} with your own tenant values before calling an endpoint. Use X-API-Key for an approved tenant integration or Authorization: Bearer for a signed-in user; send only one authentication method.

[Interactive reference](https://docs-ondi.web.app/api-docs/) · [Integration quickstart](https://docs-ondi.web.app/api-docs/quickstart)

## Delivery & Last-Mile / Deliveries — shared behavior

Endpoints for managing deliveries. These APIs provide functionality to create, retrieve, update, and delete deliveries, both individually and in bulk. Driver-action HTTP response messages are localized plain-language sentences. For the `picked_up` to `in_transit` move, the English response message is `The package is now on the way to its destination`; clients must display the returned `message` and must not build visible copy from raw status values. A P&D parent exposes its open exception through the existing `has_exception` and `exception` fields. A re-staged failed P&D outbound leg resolved as Return to Sender is returned as machine status `failed`; it is read-only and the parent continues through its separate return lifecycle. Starting or progressing its outbound child returns HTTP 409 `{success:false,code:'parent_exception_open',message:'Resolve the parent order exception before starting this delivery'}`; cancellation remains available.

## Delivery & Last-Mile / Assignments — shared behavior

Endpoints for managing delivery assignments. Assignments link deliveries to drivers and include routing information. When a staged order cannot move because its next child delivery is not ready, the localized English response message is `This order cannot move yet because its next delivery step is not ready`; clients must display the returned `message` rather than the internal `no_active_child_leg` key.

## Delivery & Last-Mile / Delivery Services — shared behavior

Delivery services may use tenant defaults, an active fulfillment policy profile, or custom `policy_overrides`. `allow_returns` controls whether the service can be selected for a return delivery and defaults to false. Under custom mode, `policy_overrides.payment_policies.refund_delivery_fee_on_cancel` and `refund_delivery_fee_on_return` are optional booleans. Missing fields inherit from the lower policy layer; effective defaults are Cancel = refund and Return = no refund.

## Delivery & Last-Mile / Delivery Types — shared behavior

Endpoints for managing delivery types.

## Delivery & Last-Mile / Price — shared behavior

Delivery price calculation and package-aware order price preview.

## Delivery & Last-Mile / Return Requests — shared behavior

Delivery Return Requests API (single-flow).

## Delivery & Last-Mile / Exceptions — shared behavior

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.

## Delivery & Last-Mile / Driver Performance — shared behavior

Driver listing and reporting endpoints for delivery totals, success, failures, on-time performance, duration, value, ranking, and monthly comparison.

## Delivery & Last-Mile / Customer Communications — shared behavior

Endpoints for managing and tracking communications with customers, including notifications about their deliveries through various channels (SMS, email, push notifications, calls).

## Delivery & Last-Mile / Customer Preferences — shared behavior

Endpoints for managing customer delivery preferences. These preferences define how, when, and where customers wish to receive their deliveries.

## Delivery & Last-Mile / Drivers — shared behavior

Endpoints for managing drivers, including multi-vehicle assignments, wallet/accounting views, creation, updates, deletion, and location tracking.

## Delivery & Last-Mile / Vehicles — shared behavior

Endpoints for managing delivery vehicles, which define different types of vehicles used for deliveries, including their capacity unit, requirements, specifications, and image URL.

## Delivery & Last-Mile / Financial Dashboard (Delivery) — shared behavior

Payout success and failure response messages are localized using `language` or `Accept-Language`. Database error text is never returned for display. English examples include `The payout was approved successfully`, `The payout was rejected successfully`, and `The manual payout request was created successfully`.

## Delivery & Last-Mile / Trips — shared behavior

Trip management — driver work plans with ordered delivery stops.

A Trip groups multiple deliveries for one driver. Storage is delivery-level (each delivery holds trip_id + trip_sequence), while an optional trip-level stop order stores dispatcher-defined stop priorities. The GET detail endpoint returns the effective stop_list for the dispatcher and driver execution views. Pickup stops are grouped only when their coordinates match, and drop-off stops are grouped only when their coordinates match. Address IDs, labels, names, and formatted addresses are not used for stop matching. A grouped drop-off is always placed after every pickup containing one of its parcels. Each stop exposes a stable `stop_key`, and each delivery exposes its active `assignment_status`, `next_action`, and `bulk_eligible` state for driver group actions. Successful pickup rows from the stop bulk-action endpoint are published as one bounded notification operation; customer and driver events remain per applicable delivery while dispatchers/admins receive one operation summary. Notification delivery is asynchronous and is never the source of trip or pickup truth. Every trip domain error returns a stable machine-readable `code`, a localized display `message`, and `data` (usually null; ordered delivery validation uses it for per-order results).

## Delivery & Last-Mile / Routes — shared behavior

Route feature — reusable named delivery lanes from origin to destination.

A Route is a fixed lane. Its optional `driver_id` is reserved only for automatic scheduled-session creation and does not own the route. A Route Session is one operational run and owns exactly one immutable driver. Different sessions of the same route may use different drivers.

### Session lifecycle

open → started → picked_up → in_transit → completed (auto) | cancelled

- Route `driver_id`, `schedule_time`, `schedule_days`, and `start_date` must be provided together or all cleared.
- Changing the route driver affects future scheduled sessions only. Existing sessions never change driver.
- Manual assign-deliveries requests use their explicit `driver_id` or `session_id` and never infer the route driver.
- `driver_id` assignment mode reuses the newest open or started session for the same route and driver; it creates a new session only when no accepting session exists.
- `session_id` mode adds deliveries only to that exact open/started session.
- A session driver cannot be changed by any role. Replacement is cancel the pre-pickup session and create a new session with the correct driver.
- Drivers see only sessions assigned to their own driver record; route list/detail are dispatcher-only.
- A driver cannot start an empty session; at least one active non-terminal delivery must be assigned.
- Any session action that moves or resets deliveries is blocked when one of the session deliveries has an open delivery exception; resolve the exception first.
- Route success, validation, conflict, and failure messages are localized for English, Arabic, Kurdish, and Persian through the existing `language` query parameter or `Accept-Language` header.

## Delivery & Last-Mile / Bulk Operations — shared behavior

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.

## Delivery & Last-Mile / Driver Bulk Pick — shared behavior

Driver-only continuous shipment-label validation and one-confirm pickup. The API preserves one result per submitted barcode, resolves staged parent labels to the active operational child, and resolves an original label with no active forward leg directly to its active linked return. It never exposes the failed forward delivery as a candidate or exposes the dispatcher generic bulk endpoint to the driver app. An outbound row whose parent has an open exception is rejected with stable reason `parent_exception_open`; the driver must not pick it until dispatch resolves the parent exception. After payment processing, successful Execute rows are published as one bounded notification operation (maximum 250 entities per queue job). Customer and driver notifications remain per applicable delivery; dispatchers/admins receive one summary per operation. Notification delivery is asynchronous, does not alter the Execute response, and is never the source of pickup truth.

## Delivery & Last-Mile / Bulk Delivery Order Creations — shared behavior

Bulk delivery order import via signed-URL upload.

### Three-step flow
1. `POST /delivery/bulk-order-creations/upload-url` — server issues a one-time signed PUT URL.
2. Client `PUT`s the file bytes directly to that URL.
3. `POST /delivery/bulk-order-creations` — JSON body referencing the uploaded `file_key` plus all job-level fields. Server verifies the file landed in storage, inserts the job row, and kicks off the background worker. Returns 202.

Polling endpoints expose status, signed download URLs for `success.csv` / `failed.csv`, the `event_timeline`, and per-row failure diagnostics.

### Notes
- `delivery_type_id` is NOT a field — it is derived server-side from `delivery_service_id`.
- File limits: ≤ 15 MB; extensions: `csv`, `xls`, `xlsx`.
- `is_draft=true` creates imported deliveries as Draft while preserving normal payment processing; use Bulk Operations `execute_draft` to release selected Draft orders to Pending.
- All endpoints (except `_resume`) require `manage:operations:tenant` or system admin.

## Delivery & Last-Mile / Teams — shared behavior

Team management for multi-team access control. Teams group services, drivers, users, and customers by operation area.

## Delivery & Last-Mile / Driver Finance — shared behavior

### Driver finance management endpoints

manual wallet transactions (credit/debit), accounting summaries, and wallet transaction history.

## Delivery & Last-Mile / Service Fulfillment Policies — shared behavior

CRUD for delivery service fulfillment policy profiles. `proof_settings` has five independent stages: `driver_pickup`, `client_pickup`, `driver_delivery`, `warehouse_pickup`, and `warehouse_delivery`. Each stage accepts `photo_required` and `signature_required`. `payment_policies.refund_delivery_fee_on_cancel` and `payment_policies.refund_delivery_fee_on_return` control whether a collected delivery fee is credited to the sender wallet for whole-order cancellation or return. Defaults are Cancel = refund (`true`) and Return = no refund (`false`). COD settlement is independent. Driver delivery flows enforce `driver_pickup` for non-warehouse pickup endpoints, `warehouse_pickup` for warehouse pickup endpoints, `driver_delivery` for non-warehouse delivery endpoints, and `warehouse_delivery` for warehouse delivery endpoints.

## Delivery & Last-Mile / Institution Transfer Manifest V1 — shared behavior

G2G / Institution Transfer Manifest APIs. Covers processing points, institution branches, barcode batches, driver manifest lifecycle, dispatcher receiving/processing, exceptions, and event history. Use `{{base_url}}/delivery/...` for the standard generated collection. If calling the staging function proxy directly at `https://staging-api.ondi.io/functions/v1/delivery`, remove the `/delivery` path prefix.

### Feature gate

every endpoint in this group requires tenant setting `transfer_manifest.enabled` to be `true`. When disabled, the API performs no operation and returns HTTP `403` with `{ "success": false, "code": "transfer_manifest_disabled", "message": "Transfer manifest is disabled for this tenant" }`.

### Tenant setting contract

`GET /tenants/:tenantId/settings?keys=transfer_manifest` returns `delivery.transfer_manifest.settings`. Supported settings include `enabled`, `receiving_mode`, `allow_driver_select_processing_point`, `allow_driver_create_manifest_from_form`, `allow_driver_scan_institution_qr`, `allow_driver_scan_item_barcode`, `allow_warehouse_staging_without_assigned_driver`, `driver_scan_mode`, `default_delivery_service_id`, `delivery_fee_origin`, `reasons`, and derived `default_delivery_service` when configured. `allow_warehouse_staging_without_assigned_driver` defaults to false; when true, an unassigned Transfer Manifest P&D inbound leg may use `assignment_change_settings.default_driver_id` during the existing warehouse staging operation. `driver_scan_mode` accepts `continuous` or `manual`; missing or invalid stored values are returned as `continuous`. `delivery_fee_origin` is `processing_point` (default) or `source_location`. It changes only the price basis when an item becomes an order: the order pickup location always remains the processing point, while `source_location` prices from the manifest's stored source address. `reasons` is the tenant-enabled array of `{ value, label, description }` exception reasons that the frontend should show and manual exception endpoints accept. The three `allow_driver_*` workflow toggles are optional booleans, but at least one must be `true`; `allow_driver_create_manifest_from_form` defaults to `true`, while the scan toggles default to `false`. `proof_settings` is an object with `{ pickup: { photo_required, signature_required }, delivery: { photo_required, signature_required } }` — all booleans. Defaults: pickup requires photo only; delivery requires both photo and signature.

### Frontend contract

hide or disable transfer-manifest navigation and actions when the effective setting is disabled. If a stale screen still calls an endpoint. For proof capture, read `proofRequirements` from the manifest detail response to determine which fields to show/enforce at each lifecycle step, handle `403` with code `transfer_manifest_disabled` by stopping the action and returning the user to a non-transfer-manifest screen. Driver UI must show at least one enabled entry workflow. Use `allow_driver_create_manifest_from_form` to show manual manifest form creation, `allow_driver_scan_institution_qr` to show institution QR scanning, and `allow_driver_scan_item_barcode` to show item barcode scanning. Read `driver_scan_mode` to choose continuous or manual item capture; never infer mode from an error message. In Transfer Manifest Settings, submit `delivery_fee_origin` as either `processing_point` or `source_location` and `driver_scan_mode` as either `continuous` or `manual`; show backend HTTP 400 validation errors directly.

## Delivery & Last-Mile / G2G Transfer Manifest Items — Bulk Scan — shared behavior

Dispatcher-panel bulk workflow for processing G2G manifest items at the processing point.

### Typical flow
1. `GET /search` — autocomplete as the dispatcher scans / types barcodes.
2. `POST /bulk/validate` — pre-flight check for a batch of identifiers (UUIDs or barcode strings).
3. `POST /bulk/set-destination` — in one frontend action, start receiving when needed, receive the selected items, complete receiving when every manifest item is accounted for, then set destination and create delivery orders.
4. `POST /bulk/resolve-exceptions` — resolve selected exception queue items, or all exception items under one manifest, as `reprocess` or `void`.

Driver arrival is never skipped. A manifest still in `draft`, `picked_up`, or `in_transit` is rejected with its manifest number and current status, leaving the driver arrival/photo/signature step available. `awaiting_receipt` starts receiving inside the same call; `receiving` continues receiving; `received`, `received_with_exceptions`, and `processing` are receipt-safe retry states.

Barcode strings passed to `/bulk/validate` must be valid 14-character G2G postal barcodes with a matching MOD 11 check digit. A malformed or checksum-invalid input returns a localized per-item `invalid_barcode` result without a database barcode lookup.

All endpoints are tenant-scoped via the `Authorization` bearer token. Search exposes unpromoted `picked_up`, `in_transit`, and `received` items. Validation inspects found items regardless of lifecycle status so rejected rows can name their manifest and current state.

### Feature gate

these endpoints require tenant setting `transfer_manifest.enabled` to be `true`. When disabled, no validation, search, update, exception resolution, or order creation runs; the API returns HTTP `403` with `{ "success": false, "code": "transfer_manifest_disabled", "message": "Transfer manifest is disabled for this tenant" }`.

### Frontend contract

keep one action/button and call `bulk/set-destination` once. Do not add a separate receiving action. Handle the stable error codes and display their localized messages.

## Delivery & Last-Mile / Customer Portal Custom Domain — shared behavior

Tenant-side endpoints to attach a custom domain (e.g. portal.tenant.com) to the delivery customer portal. All endpoints require an authenticated `tenant_admin` role.

## Delivery & Last-Mile / Maps — shared behavior

Backend map utilities. Static map previews proxy Google Static Maps through the backend so mobile clients do not receive the Google Maps provider key.

## Delivery & Last-Mile / Route Calculation — shared behavior

Endpoints for calculating optimized delivery routes using Google Maps Directions API.

## Delivery & Last-Mile / Error Scenarios — shared behavior

Collection of error scenarios and edge cases for testing the Route Calculation API.

## Delivery & Last-Mile / Testing Scenarios — shared behavior

Collection of testing scenarios for different user roles and permissions.

## Customer Portal / Delivery Management / Support Tickets — shared behavior

Unified support tickets API. The same endpoints are used by the customer portal and the storefront market — pass `source` (and `related_order_id` when source=storefront) to differentiate origin. Tenant admins can filter the support queue by source from the messaging APIs.

## Storefront & Commerce / Advanced Features / Analytics & Reporting — shared behavior

APIs for comprehensive analytics, sales reports, and business intelligence for storefront administrators.

## Storefront & Commerce / Advanced Features / Payment Methods Management — shared behavior

APIs for configuring and managing payment methods available in the storefront.

## Storefront & Commerce / Advanced Features / Theme Customization — shared behavior

APIs for customizing the visual appearance and branding of the storefront.

## Storefront & Commerce / Advanced Features / Stock Management — shared behavior

APIs for managing and refreshing stock quantities for storefront catalog items.

## Storefront & Commerce / Order Status Rules Management — shared behavior

Dynamic Order Status Rules Management API for configuring how order group statuses are aggregated into order statuses. Provides complete control over business logic without code changes.

## Storefront & Commerce / Custom Domain Management — shared behavior

Tenant-facing APIs for connecting, verifying, viewing, and disconnecting a storefront custom domain.

## Warehousing / Inventory Audits — shared behavior

Endpoints for managing inventory audits and stock verification processes. These APIs support different audit types (cycle counts, full counts, spot checks) for validating physical inventory against recorded quantities. Includes complete workflow from audit creation through completion with stock adjustments.

## Warehousing / Package Handoffs Management — shared behavior

Endpoints for managing package handoffs between warehouse staff and delivery drivers. These APIs provide functionality for creating, retrieving, listing, and updating handoff records with their status, signature URLs, and related entities.

## Warehousing / Warehouse Staff — shared behavior

Endpoints for managing warehouse staff assignments, including assigning users to warehouses with specific roles, listing staff with filtering options, and allowing warehouse staff to manage their own profiles.

## Warehousing / Picking Orders — shared behavior

Endpoints for managing picking orders, which represent instructions to retrieve items from inventory for shipment, transfer, or internal use. Supports creating orders, assigning them to staff, and tracking their fulfillment status.

## Warehousing / Inventory Stock Management — shared behavior

Endpoints for managing inventory stock levels and movements within warehouses. These APIs provide real-time tracking of item quantities across different bin locations, supporting lot and serial number tracking, expiration date management, and historical stock level tracking.

## Warehousing / Receiving Orders — shared behavior

Endpoints for managing receiving orders (incoming shipments) in warehouses, including creating orders and listing them with various filtering options. The receiving-order database write is the atomic stock boundary: it projects only the newly received cumulative delta into stock and one receive movement, so normal, quick, package, and item-bulk paths cannot add the same units twice. Inventory item rows preserve `has_open_exception` and the latest `open_exception`, and also expose every current `open_exceptions` entry plus `open_exception_count`. For an open damaged exception with an exact package mapping and current available stock in the same warehouse, that compact exception returns `package_item_id` and `available_actions[].key=return_to_origin`; this also applies when receiving created stock after the exception and `inventory_stock_id` remains null. An explicit stock ID must still match exactly. Execute the action through `POST /warehousing/exceptions/:id/return-to-origin`. Its `receiving_order_id` is optional provenance. A staged P&D receiving order remains blocked by an unrelated open parent exception. When an Arrived-at-Warehouse parent exception is explicitly linked to that existing receiving order, warehouse staff may receive that exact order while the exception remains open; Retry and Return stay blocked until receipt confirms custody. Executing an outbound P&D delivery remains blocked until the parent exception is resolved.

## Warehousing / Inventory Items Management — shared behavior

Endpoints for managing inventory items in the warehouse management system. These APIs provide CRUD operations for item master data with support for detailed specifications, categorization, and stock tracking across warehouses.

## Warehousing / Warehouse Locations — shared behavior

APIs for managing warehouse locations, providing capabilities to organize warehouse space hierarchically (zones, aisles, shelves, bins) for inventory management.

## Warehousing / Warehouses — shared behavior

APIs for managing warehouses in the system

## Warehousing / Inventory Transactions Management — shared behavior

Endpoints for managing inventory transactions that track all stock movements and adjustments within warehouses. These APIs provide a comprehensive audit trail of inventory changes, including what was moved, from where, to where, by whom, and when.

## Warehousing / Inventory Transfers — shared behavior

Endpoints for managing cross-warehouse inventory stock transfers. Transfers atomically move stock from a source warehouse/bin to a destination warehouse/bin, creating paired inventory transactions for full traceability.

## Warehousing / Package Transfers — shared behavior

Endpoints for transferring packages/deliveries between warehouses. Supports two modes:
- **system_only**: Updates warehouse association in the system without creating delivery operations.
- **delivery_operation**: Creates delivery orders and optionally a waybill for physical movement managed by the delivery module.

## Warehousing / Warehouse Package Scan — shared behavior

Read-only barcode scan endpoint for warehouse staff. Resolves staged P&D physical parent barcodes and separate operational-return delivery barcodes without processing either order. If an original barcode has an eligible active linked return, the response includes compact `delivery` and `return_delivery` plus independently resolved `delivery_target` and `return_target`. Every displayed option has a non-null target using only `open_receiving_order`, `open_handoff`, or `open_delivery`. A receiving-order or handoff target is used only when its delivery association belongs to that selected delivery or its P&D family; otherwise the target falls back to `open_delivery` with the displayed delivery ID and reason `delivery_details_available`. Mobile shows Delivery and Return as explicit choices and uses only the selected delivery ID and corresponding target. Dismissing the choices does not navigate, assign, receive, hand off, or change status. A direct return scan keeps the return in `delivery`, exposes its workflow in `delivery_target`, and includes `original_delivery` plus an independently resolved `original_target`. If no eligible active return exists, `return_delivery` and `return_target` are null and the existing original scan flow remains unchanged. Linked summaries include `id`, `delivery_code`, `barcode`, `status`, `delivery_flow`, `is_active`, and `next_action`. Returned `items` preserve `has_exception`, `has_open_exception`, and the latest `open_exception`, and also expose every current `open_exceptions` entry plus `open_exception_count`; the deprecated `exception` alias is not returned. Persistent execution blocks and missing handoffs are not advertised as executable targets; the displayed delivery instead uses the read-only `open_delivery` fallback.

## Warehousing / Warehouse Exceptions — shared behavior

Warehouse item exceptions for receiving, picking, inventory audits, and inventory handling. Source of truth is `warehouse_exceptions`. An item may have multiple open exceptions. Tenant-defined types may set `write_off_stock=true`; the exception flow is the sole owner of that stock write-off. Audit-created exceptions expose `inventory_audit_id`; their stock transaction references the audit directly and does not rewrite historical receiving quantities. An open damaged exception with an exact package mapping and current available warehouse stock exposes `available_actions[].key=return_to_origin` so only that box can be sent back while the parent order continues. Current stock is matched by tenant, inventory item, and warehouse when the exception predates stock creation and therefore has no `inventory_stock_id`; an explicit stock ID remains exact. `receiving_order_id` is optional audit provenance and is inferred from inventory custody metadata when available.

## Express Shipping / Configuration / Tenant Configuration — shared behavior

APIs for managing tenant-specific express shipping configuration

## Express Shipping / Configuration / Shipping Services — shared behavior

APIs for retrieving available shipping services and options

## Express Shipping / Configuration / System — shared behavior

System health and information endpoints

## Express Shipping / Order / Invoice Processing — shared behavior

APIs for extracting invoice data using AI document processing

## Express Shipping / Order / Order Management — shared behavior

APIs for confirming orders after invoice extraction and review

## Express Shipping / Order / Package Tracking — shared behavior

APIs for tracking packages and listing customer orders

## Express Shipping / Order / Admin Operations — shared behavior

Admin-only endpoints for detailed package management

## Express Shipping / Express — shared behavior

Active Express API endpoints for package management

## External & Carrier Integrations / External & Carrier Integrations — shared behavior

Global JSON response contract for every `/external/*` endpoint:

- `success` is always a boolean.
- `error` is `null` for success and a stable snake_case code for failure.
- `message` is always a readable message localized from the request language.
- Existing endpoint-specific data fields remain unchanged at the top level.
- Existing HTTP status codes remain unchanged.
- `POST /external/deliveries/shipment-labels` keeps its successful `application/pdf` binary response so existing downloads continue to work; every JSON error from that endpoint follows this contract.

### Frontend integration

branch on HTTP status and `error`; display `message`; continue reading existing endpoint-specific payload fields exactly as before.

### Global JSON response contract for every /external/* endpoint

```json
{
  "success": true,
  "error": null,
  "message": "Localized readable message"
}
```

## External & Carrier Integrations / External International Delivery — shared behavior

### This endpoint uses the global External API JSON response contract

`success` is always boolean, `error` is `null` on success or a stable snake_case code on failure, and `message` is always a localized readable message. Existing delivery, inventory, receiving-order, validation-detail, and other endpoint-specific fields remain unchanged.

## Platform & Access / Providers — shared behavior

Provider responses return stored provider values in plain text, including api_key and config. Existing authenticated GET behavior is unchanged. PATCH /provider/providers/:key is JWT-only and requires manage:providers:system; API keys and callers without that permission receive 403 rbac_forbidden before request parsing or provider database access.

## Platform & Access / Users / Wallets — shared behavior

Endpoints for managing user wallet operations including topup, transfers, and transaction history.

## System Configuration / Storefront Administration — shared behavior

APIs for system administrators to manage all storefronts and global settings across the platform. These endpoints require system admin privileges.

## System Configuration / Storefront Administration / Customer Portal Domain Administration — shared behavior

System admin endpoints to manage delivery customer portal custom domains across tenants.

## System Configuration / Storefront Administration / Manage Global Payment Methods — shared behavior

Endpoints for system admins to manage the globally available payment methods.

## System Configuration / Restaurant Administration — shared behavior

System-level APIs for cross-tenant restaurant administration in the Ondi admin portal.

## Billing & Subscriptions / Wallets — shared behavior

API collection for managing user wallets, transactions, and transfers within the SendRoad system. These endpoints allow users to top up wallets, transfer funds between users, and perform other wallet operations.

## Billing & Subscriptions / Wallets / Top-up — shared behavior

Endpoints for managing wallet top-up operations.

## Billing & Subscriptions / Wallets / Transfer — shared behavior

Endpoints for transferring funds between wallets.

## Billing & Subscriptions / Wallets / Transactions — shared behavior

Endpoints for retrieving wallet transaction history with pagination and filtering.

## Billing & Subscriptions / Wallets / Admin — shared behavior

Admin endpoints for querying wallets for other users within the same tenant.

## Billing & Subscriptions / Tenant Admin — shared behavior

Payment APIs for tenant administrators to manage payment methods and view transactions.

## Billing & Subscriptions / User — shared behavior

Payment APIs for end users to view available payment methods and process payments.

## Billing & Subscriptions / Webhook — shared behavior

Public webhook endpoints for payment gateway notifications.

## Billing & Subscriptions / System Admin — shared behavior

Payment APIs for system administrators to manage payment method types and tenant gateway configurations.

## Communications & Feedback / Messaging — shared behavior

Frontend-facing response messages are localized using `language` or `Accept-Language`. For example, a non-driver calling the dispatcher conversation endpoint receives the plain-language message `Only drivers can perform this action`, not the internal key `driver_only`.

## Communications & Feedback / Messaging / Conversations — shared behavior

Endpoints for managing message conversations between users.

## Communications & Feedback / Messaging / Messages — shared behavior

Endpoints for managing messages within conversations.

### Realtime frontend contract
- Subscribe to Supabase Postgres Changes on schema `public`, table `messages`, event: `INSERT` using a valid authenticated user access token.
- The database delivers a message only when the JWT user has a `message_participants` row for that message's conversation with `left_at` equal to null.
- A user does not receive messages from conversations where they have no active participant row or where their participant row has `left_at` set.
- Do not subscribe with the `anon` role; anonymous database access to `messages` and `message_participants` is revoked.
- Do not use event `*` for this listener. The app-wide listener must be INSERT-only.
- Message creation and participant changes continue through the authenticated Messaging API; frontend clients have read-only table access.
- On each received INSERT, refetch `GET /messaging/conversations/unread-count` once and update the shared unread badge state used by every chat icon.

## Communications & Feedback / Messaging / Participants — shared behavior

Endpoints for managing participants in conversations.

## Communications & Feedback / Messaging / Notifications — shared behavior

Endpoints for listing, acknowledging, marking as read, and getting notification statistics. Acknowledgement is intentionally separate from read state.

## Communications & Feedback / Messaging / Support Tickets — shared behavior

Support operations endpoints for managing support tickets in a shared queue. Tickets start unassigned, can be claimed manually, and may auto-assign on first reply if enabled in customer portal settings.

## Reporting, Audit & Webhooks / Report / Report Exports (Async) — shared behavior

Generic report export framework. Catalogs describe what can be exported; jobs run async and the file is downloaded via a signed URL.

## Reporting, Audit & Webhooks / Webhooks / Webhook Subscriptions — shared behavior

Endpoints for managing webhook subscriptions. These APIs allow you to set up, view, update, and delete subscriptions to receive webhook notifications for various system events.

## Reporting, Audit & Webhooks / Webhooks / Webhook Events — shared behavior

Endpoints for viewing and managing webhook event delivery logs. These APIs allow you to monitor the delivery status of webhook events and retry failed deliveries.

## POST /addresses — Create Address

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/addresses#post-create-address)

Creates a new address in the system, returns an existing saved address for duplicate user coordinates, or returns normalized address JSON when `save_to_address_book=false`.

### Key rules
- Required address fields are `country`, `latitude`, and `longitude`.
- `address_line1` is optional when `full_address` is provided; the backend copies `full_address` into `address_line1`.
- `address_type` is optional and defaults to `user` when omitted.
- `city`, `state`, `postal_code`, `address_line2`, title, contact info, and other descriptive fields are optional; empty optional strings are accepted.
- `save_to_address_book=false` returns normalized JSON (no DB insert and no saved-coordinate lookup).
- `save_to_address_book=true` (default) uses exact active coordinates scoped only by `user_id`. The identity is `user_id + latitude + longitude`; `tenant_id`, system scope, and `address_type` do not change the match. Coordinates are compared at the database's 8-decimal precision. Inactive addresses do not block reuse.
- New coordinates create a row and return HTTP `201` with `action=address_created`.
- Existing coordinates skip insertion and return HTTP `200` with `success`, a localized `Address already saved as {existing saved title}` message, and the complete existing `address` object. The newly submitted title does not replace the existing title or appear in this message. The duplicate response intentionally has no `action` field.
- `is_primary` is the frontend default-address toggle. For a customer with existing active addresses, `false` keeps the new address non-primary and `true` makes it primary, unsetting the prior primary address. The backend makes the first active address primary even when this field is `false`.

### Frontend integration contract
- Send `country`, `latitude`, and `longitude` for create and save/normalize requests.
- Send `address_line1` when available. If omitted, send `full_address` and it will be used as `address_line1`.
- Send the user's label in `title`, for example `{"en":"home"}`, when creating an address at new coordinates. If the coordinates already exist, the response message uses the title from the returned existing `address` object.
- Send `city`, `postal_code`, `state`, and other optional address fields only when available; `""` is allowed for optional strings.
- Send `is_primary: true` only when the user explicitly selects "Use this address as default".
- HTTP `201` with `action=address_created` means a new row was created; select the returned `address.id` and continue normally.
- HTTP `200` with `success=true` means those coordinates were already saved. Show `message`, close the address modal, select the returned `address.id`, and continue the original flow. Do not expect an `action` field or a new address row.
- `400` means the address was not saved; use `details[].field` to highlight the exact invalid field.

### Required Fields
- `country`, `latitude`, `longitude`
- `address_line1` or `full_address` (if missing, `full_address` is copied to `address_line1`)

### Owner Constraint
- `user_id` defaults to the authenticated user when omitted. `tenant_id` can be provided directly or through the query/context when needed.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/addresses?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| address_type | string |  |  |  |
| user_id | string |  |  |  |
| title | object |  |  |  |
| title.en | string |  |  |  |
| full_address | string |  |  |  |
| address_line1 | string |  |  |  |
| address_line2 | string |  |  |  |
| street | string |  |  |  |
| building | string |  |  |  |
| floor | string |  |  |  |
| apartment | string |  |  |  |
| city | string |  |  |  |
| state | string |  |  |  |
| country | string |  |  |  |
| country_code | string |  |  |  |
| postal_code | string |  |  |  |
| latitude | number |  |  |  |
| longitude | number |  |  |  |
| contact_info | object |  |  |  |
| contact_info.phone | string |  |  |  |
| contact_info.email | string |  |  |  |
| is_primary | boolean |  |  |  |
| save_to_address_book | boolean |  |  |  |



### Request body (raw)

```json
{
  "address_type": "user",
  "user_id": "{{user_id}}",
  "title": {
    "en": "Home Address"
  },
  "full_address": "123 Main St, Anytown, CA 12345",
  "address_line1": "123 Main St",
  "address_line2": "Apt 4B",
  "street": "Main St",
  "building": "123",
  "floor": "4",
  "apartment": "4B",
  "city": "Anytown",
  "state": "CA",
  "country": "United States",
  "country_code": "USA",
  "postal_code": "12345",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "contact_info": {
    "phone": "+123456789",
    "email": "a@example.com"
  },
  "is_primary": false,
  "save_to_address_book": true
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "action": "address_created",
  "message": "Address created successfully",
  "address": {
    "id": "uuid-example",
    "user_id": "user-uuid",
    "tenant_id": null,
    "address_type": "user",
    "is_primary": false,
    "title": {
      "en": "Home Address"
    },
    "address_line1": "123 Main St",
    "city": "Anytown",
    "country": "United States",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "created_at": "2023-01-01T12:00:00Z",
    "updated_at": "2023-01-01T12:00:00Z"
  }
}
```

### Response 400 Error - Missing Required Fields

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "address_line1: Field required",
  "details": [
    {
      "field": "address_line1",
      "message": "Field required"
    },
    {
      "field": "country",
      "message": "Field required"
    },
    {
      "field": "latitude",
      "message": "Field required"
    },
    {
      "field": "longitude",
      "message": "Field required"
    }
  ]
}
```

### Response 400 Error - Invalid Address Type

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "address_type: Must be one of: user, tenant, branch, warehouse, customer, pickup, delivery",
  "details": [
    {
      "field": "address_type",
      "message": "Must be one of: user, tenant, branch, warehouse, customer, pickup, delivery"
    }
  ]
}
```

### Response 400 Error - Owner Constraint Violation

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Address must belong to either user or tenant, not both"
}
```

### Response 200 Existing Address Selected

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Address already saved as Home Address",
  "address": {
    "id": "existing-address-uuid",
    "user_id": "user-uuid",
    "tenant_id": "tenant-uuid",
    "address_type": "user",
    "is_primary": true,
    "is_active": true,
    "title": {
      "en": "Home Address"
    },
    "address_line1": "123 Main St",
    "country": "United States",
    "latitude": 37.7749,
    "longitude": -122.4194
  }
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## GET /addresses/:addressId — Get Address

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/addresses#get-get-address)

Retrieves a specific address by ID.

- Users can only access their own addresses
- Users can access addresses of tenants they belong to
- System admins can access any address

### Response
- Returns the complete address object with all fields
- Returns 404 if address not found
- Returns 403 if user is not authorized to access the address

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/addresses/:addressId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| addressId | path string | Yes | UUID of the address to retrieve |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Address retrieved successfully",
  "address": {
    "id": "uuid-example",
    "user_id": "user-uuid",
    "tenant_id": null,
    "address_type": "user",
    "is_primary": true,
    "title": {
      "en": "Home Address"
    },
    "full_address": "123 Main St, Anytown, CA 12345",
    "address_line1": "123 Main St",
    "address_line2": "Apt 4B",
    "street": "Main St",
    "building": "123",
    "floor": "4",
    "apartment": "4B",
    "city": "Anytown",
    "state": "CA",
    "country": "United States",
    "country_code": "USA",
    "postal_code": "12345",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "is_billing": false,
    "is_shipping": true,
    "is_active": true,
    "metadata": {
      "delivery_instructions": "Leave at front door",
      "landmark": "Blue house with white fence",
      "is_verified": false,
      "verification_date": null
    },
    "created_at": "2023-01-01T12:00:00Z",
    "updated_at": "2023-01-01T12:00:00Z"
  }
}
```

### Response 404 Error - Address Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Address not found"
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## GET /addresses — List Addresses

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/addresses#get-list-addresses)

Lists addresses with pagination and filtering options.

### Authentication & Scope
- No role: own addresses only
- tenant_admin + tenantId: returns all addresses from all tenant_admin users of that tenant
- userId param: filter to specific user's addresses
- System admins can see all addresses

### Response
- Returns a paginated list of addresses
- Includes pagination metadata (page, limit, total)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/addresses?page=1&limit=10&userId=&tenantId=&addressType=&search=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination (default: 1) | 1 |
| limit | query string |  | Number of items per page (default: 10) | 10 |
| userId | query string |  | Filter addresses by user ID |  |
| tenantId | query string |  | Filter addresses by tenant ID (or from auth context) |  |
| addressType | query string |  | Filter by address type (user, tenant, branch, warehouse, customer, pickup, delivery) |  |
| search | query string |  | Search term to filter addresses by full_address field |  |
| language | query string |  | Language of the response | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Addresses retrieved successfully",
  "addresses": [
    {
      "id": "uuid-example-1",
      "user_id": "user-uuid",
      "tenant_id": null,
      "address_type": "user",
      "title": {
        "en": "Home Address"
      },
      "city": "Anytown",
      "country": "United States",
      "created_at": "2023-01-01T12:00:00Z",
      "updated_at": "2023-01-01T12:00:00Z"
    },
    {
      "id": "uuid-example-2",
      "user_id": "user-uuid",
      "tenant_id": null,
      "address_type": "user",
      "title": {
        "en": "Work Address"
      },
      "city": "Business City",
      "country": "United States",
      "created_at": "2023-01-02T12:00:00Z",
      "updated_at": "2023-01-02T12:00:00Z"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 2
}
```

## PUT /addresses/:addressId — Update Address

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/addresses#put-update-address)

Updates an existing address.

### Required Fields
- `country`, `latitude`, `longitude`
- `address_line1` or `full_address` (if missing, `full_address` is copied to `address_line1`)

### Optional Fields
- `address_type`: Type of address (must be one of: 'user', 'tenant', 'branch', 'warehouse', 'customer', 'pickup', 'delivery')
- `title`: Title/name for this address (JSONB object with language keys)
- `full_address`: Complete formatted address as a single text field
- `city`: City name; optional and may be an empty string
- `address_line2`: Second line of the address (e.g., apartment, suite)
- `street`: Street name
- `building`: Building name or number
- `floor`: Floor number or name
- `apartment`: Apartment or unit number
- `state`: State/region; optional and may be an empty string
- `postal_code`: Postal code; optional and may be an empty string
- `is_primary`: Set to `true` only when the user explicitly selects "Set as Primary". The backend then atomically unsets the previous primary address and promotes this address. Sending `false` does not unset an existing primary address.
- `is_active`: Flag indicating if this address is currently active

### Coordinate Uniqueness
- The address may keep its own current coordinates.
- It cannot be updated to the exact coordinates of another active address belonging to the same `user_id`.
- The uniqueness key excludes `tenant_id`, system scope, and `address_type`; coordinates are compared at 8-decimal database precision. Inactive addresses do not block reuse.

- Users can only update their own addresses
- Users can update addresses of tenants they are admins for
- System admins can update any address

### Frontend integration contract
- Send `country`, `latitude`, and `longitude` on address update.
- Send `address_line1` if available; otherwise send `full_address` and it will be copied into `address_line1`.
- Do not block save when `city`, `postal_code`, `state`, or other optional fields are unavailable; empty optional strings are accepted.
- Send `is_primary: true` to set this address as the customer's default.
- Use the returned `address.is_primary` value to update the Primary badge and default-address selection.
- On `400`, use `details[].field` to show the exact field that failed validation.
- On HTTP `409` with `code=address_coordinates_already_exist`, retain the edited form and show the localized `message`.
- On `500`, retain the existing selected delivery/default address because the primary promotion did not complete.

### Note
- `user_id` and `tenant_id` are ignored; ownership cannot be changed through this endpoint.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/addresses/:addressId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| addressId | path string | Yes | UUID of the address to update |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| title | object |  |  |  |
| title.en | string |  |  |  |
| address_line1 | string |  |  |  |
| city | string |  |  |  |
| postal_code | string |  |  |  |
| country | string |  |  |  |
| latitude | number |  |  |  |
| longitude | number |  |  |  |
| is_primary | boolean |  |  |  |



### Request body (raw)

```json
{
  "title": {
    "en": "Updated Home Address"
  },
  "address_line1": "456 New St",
  "city": "",
  "postal_code": "",
  "country": "United States",
  "latitude": 37.7749,
  "longitude": -122.4194,
  "is_primary": true
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Address updated successfully",
  "address": {
    "id": "uuid-example",
    "user_id": "user-uuid",
    "tenant_id": null,
    "address_type": "user",
    "title": {
      "en": "Updated Home Address"
    },
    "address_line1": "456 New St",
    "city": "",
    "postal_code": "",
    "country": "United States",
    "latitude": 37.7749,
    "longitude": -122.4194,
    "is_primary": true,
    "created_at": "2023-01-01T12:00:00Z",
    "updated_at": "2023-01-03T15:30:00Z"
  }
}
```

### Response 400 Error - Validation

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "address_line1: Field required",
  "details": [
    {
      "field": "address_line1",
      "message": "Field required"
    }
  ]
}
```

### Response 404 Error - Address Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Address not found"
}
```

### Response 409 Error - Duplicate User Coordinates

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "address_coordinates_already_exist",
  "message": "You already have an active address saved at these exact coordinates"
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## DELETE /addresses/:addressId — Delete Address

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/addresses#delete-delete-address)

Deletes an address by ID.

- Users can only delete their own addresses
- Users can delete addresses of tenants they are admins for
- System admins can delete any address

### Response
- Returns success message on successful deletion
- Returns 404 if address not found
- Returns 403 if user is not authorized to delete the address

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/addresses/:addressId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| addressId | path string | Yes | UUID of the address to delete |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Address deleted successfully"
}
```

### Response 404 Error - Address Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Address not found"
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## POST /api-keys — Create API Key

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/api-keys#post-create-api-key)

Creates a new API key for the authenticated user. The API key is returned only once in the response and cannot be retrieved later.

### Required Fields
- `name`: A descriptive name for the API key

### Optional Fields
- `expires_at`: ISO 8601 date string when the key should expire (must be in the future)
- `description`: Additional information about the key's purpose or usage

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/api-keys?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| expires_at | string |  |  |  |
| description | string |  |  |  |



### Request body (raw)

```json
{
  "name": "My API Key",
  "expires_at": "2025-12-31T23:59:59Z",
  "description": "API key for integration with my service"
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | no-store, no-cache, must-revalidate |



```json
{
  "success": true,
  "message": "API key created successfully",
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "My API Key",
    "description": "API key for integration with my service",
    "created_at": "2023-01-01T00:00:00Z",
    "expires_at": "2025-12-31T23:59:59Z",
    "is_active": true,
    "api_key": "sr_a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
  }
}
```

### Response 400 Error - Missing Name

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Name is required and cannot be empty"
}
```

### Response 400 Error - Invalid Expiry Date

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Expiry date must be a valid future date"
}
```

### Response 201 Success (201 Created)



```json
{
  "success": true,
  "message": "API key created successfully",
  "data": {
    "id": "uuid",
    "name": "My API Key",
    "description": "API key for integration with my service",
    "created_at": "2023-01-01T00:00:00Z",
    "expires_at": "2025-12-31T23:59:59Z",
    "is_active": true,
    "api_key": "sr_a1b2c3d4e5f6..." // Only returned once, store securely
  }
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Failed to create API key",
  "details": "Error details"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Internal server error",
  "details": "Error details"
}
```

## GET /api-keys — List API Keys

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/api-keys#get-list-api-keys)

Retrieves a paginated list of API keys belonging to the authenticated user. This endpoint supports pagination and searching by name.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/api-keys?page=1&limit=10&search=&tenant_id=&user_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (Optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of results per page (Optional, default: 10) Number of results per page (default: 10) | 10 |
| search | query string | No | Search term to filter API keys by name (Optional) |  |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "API keys retrieved successfully",
  "apiKeys": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": "Production API Key",
      "created_at": "2023-01-01T00:00:00Z",
      "created_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "expires_at": "2025-12-31T23:59:59Z",
      "is_active": true,
      "revoked_at": null
    },
    {
      "id": "660e8400-e29b-41d4-a716-446655440000",
      "name": "Development API Key",
      "created_at": "2023-01-02T00:00:00Z",
      "created_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "expires_at": null,
      "is_active": true,
      "revoked_at": null
    }
  ],
  "total": 2,
  "page": 1,
  "limit": 10
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Failed to fetch API keys"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "API keys retrieved successfully",
  "apiKeys": [
    {
      "id": "uuid",
      "name": "API Key Name",
      "created_at": "2023-01-01T00:00:00Z",
      "created_by": "user-uuid",
      "expires_at": "2025-12-31T23:59:59Z",
      "is_active": true,
      "revoked_at": null
    }
  ],
  "total": 5,
  "page": 1,
  "limit": 10
}
```

## PUT /api-keys/:apiKeyId — Update API Key

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/api-keys#put-update-api-key)

Updates an existing API key's properties. Users can only update their own API keys.

### Optional Fields (at least one required)
- `name`: New name for the API key
- `expires_at`: New expiration date (must be in the future) or null for no expiration
- `is_active`: Whether the API key is active
- `description`: Updated description or null to remove description

### Validations
- At least one field must be provided for update
- If expires_at is provided, it must be a valid future date
- API key must not be revoked

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/api-keys/:apiKeyId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| apiKeyId | path string | Yes | ID of the API key to update |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| expires_at | string |  |  |  |
| is_active | boolean |  |  |  |
| description | string |  |  |  |



### Request body (raw)

```json
{
  "name": "Updated API Key Name",
  "expires_at": "2026-12-31T23:59:59Z",
  "is_active": true,
  "description": "Updated description"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "API key updated successfully",
  "apiKey": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Updated API Key Name",
    "created_at": "2023-01-01T00:00:00Z",
    "expires_at": "2026-12-31T23:59:59Z",
    "is_active": true,
    "description": "Updated description"
  }
}
```

### Response 404 Error - API Key Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "API key not found"
}
```

### Response 400 Error - No Valid Fields

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "No valid fields to update"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "API key updated successfully",
  "apiKey": {
    "id": "uuid",
    "name": "Updated API Key Name",
    "created_at": "2023-01-01T00:00:00Z",
    "expires_at": "2026-12-31T23:59:59Z",
    "is_active": true,
    "description": "Updated description"
  }
}
```

### Response 400 Validation Error (400 Bad Request)



```json
{
  "success": false,
  "message": "API key ID required"
}
```

### Response 400 Validation Error (400 Bad Request)



```json
{
  "success": false,
  "message": "Invalid expires_at"
}
```

### Response 404 Not Found (404 Not Found)



```json
{
  "success": false,
  "message": "API key not found or revoked"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Failed to fetch API key"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Failed to update API key"
}
```

## DELETE /api-keys/:apiKeyId — Revoke API Key

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/api-keys#delete-revoke-api-key)

Revokes (permanently disables) an API key. This action cannot be undone. Users can only revoke their own API keys.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/api-keys/:apiKeyId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| apiKeyId | path string | Yes | ID of the API key to revoke |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "API key revoked successfully"
}
```

### Response 404 Error - API Key Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "API key not found"
}
```

### Response 404 Error - Already Revoked

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "API key not found or revoked"
}
```

### Response 400 Validation Error (400 Bad Request)



```json
{
  "success": false,
  "message": "API key ID required"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Failed to fetch API key"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Failed to revoke API key"
}
```

## GET /api-keys/{{api_key}}/health — Health Check API Key

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/api-keys#get-health-check-api-key)

Verifies the validity of a tenant-generated API key and retrieves its details, including accessible modules.

- No authentication required (API key in path)

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/api-keys/{{api_key}}/health

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| api_key | path string | Yes | The API key to validate |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "message": "API key is valid",
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "name": "Production API Key",
  "created_at": "2023-01-01T00:00:00Z",
  "created_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "expires_at": "2025-12-31T23:59:59Z",
  "is_active": true,
  "revoked_at": null,
  "modules": [
    "delivery",
    "warehousing"
  ]
}
```

### Response 400 Error - API key is required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "API key is required"
}
```

### Response 401 Error - API key is revoked, expired, or inactive

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "API key is revoked, expired, or inactive"
}
```

### Response 404 Error - API key not found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "API key not found"
}
```

## GET /carriers/admin — List System Carriers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/system-admin#get-list-system-carriers)

GET /carriers/admin — List all carriers with global status and defaults

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/carriers/admin

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## PATCH /carriers/admin/PRIME — Update System Carrier

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/system-admin#patch-update-system-carrier)

### PATCH /carriers/admin/

code — Enable/disable and set default price

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/carriers/admin/PRIME

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_active | boolean |  |  |  |
| default_base_price | number |  |  |  |



### Request body (raw)

```json
{
  "is_active": true,
  "default_base_price": 6.5
}
```

## GET /carriers/tenant — List Tenant Carriers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/tenant-admin#get-list-tenant-carriers)

GET /carriers/tenant — List globally active carriers with this tenant's config

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/carriers/tenant

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## PATCH /carriers/tenant/PRIME — Update Tenant Carrier

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/tenant-admin#patch-update-tenant-carrier)

### PATCH /carriers/tenant/

code — Upsert tenant config for a carrier. When enabling PRIME, response includes webhook.url and webhook.secret (to provide to Prime).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/carriers/tenant/PRIME

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_enabled | boolean |  |  |  |
| prefer_internal | boolean |  |  |  |
| default_price | number |  |  |  |
| credentials | object |  |  |  |
| credentials.login_id | string |  |  |  |
| credentials.password | string |  |  |  |
| credentials.initial_token | string |  |  |  |



### Request body (raw)

```json
{
  "is_enabled": true,
  "prefer_internal": false,
  "default_price": 4.5,
  "credentials": {
    "login_id": "{{prime_login_id}}",
    "password": "{{prime_password}}",
    "initial_token": "{{prime_initial_token}}"
  }
}
```

## POST /carriers/webhooks/prime — Prime Webhook (Status Update)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/prime-webhook#post-prime-webhook-status-update)

POST /carriers/webhooks/prime — Prime sends status updates here. Authenticate using Authorization: Bearer <webhook_secret>.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/carriers/webhooks/prime

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{prime_webhook_secret}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| stepCode | string |  |  |  |
| caseId | string |  |  |  |
| custReceiptNoOri | string |  |  |  |



### Request body (raw)

```json
{
  "stepCode": "ONWAY",
  "caseId": "123456",
  "custReceiptNoOri": "DEL-00001"
}
```

## GET /customer/delivery/dashboard — Get Dashboard

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/dashboard#get-get-dashboard)

### Returns dashboard data

KPIs, wallet snapshot, chart data, and recent deliveries. Metrics and chart series are aggregated server-side for lower latency.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/dashboard?tz=UTC&recent_limit=5&chart_days=7

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tz | query string |  | IANA timezone | UTC |
| recent_limit | query string |  | Number of recent deliveries (1-20) | 5 |
| chart_days | query string |  | Number of days for chart (7-31) | 7 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "metrics": {
      "created_mtd": 15,
      "in_transit": 3,
      "pending": 2,
      "delivered": 10
    },
    "wallet": {
      "balance": 500,
      "spend_mtd": 150
    },
    "charts": {
      "delivery_week": {
        "title": "Orders This Week",
        "period": {
          "start": "2026-01-28",
          "end": "2026-02-04"
        },
        "data": {
          "labels": [
            "2026-01-28",
            "2026-01-29",
            "2026-01-30"
          ],
          "series": {
            "created": [
              2,
              3,
              1
            ],
            "delivered": [
              1,
              2,
              1
            ]
          }
        },
        "insights": {
          "total_created": 6,
          "total_delivered": 4,
          "completion_rate": 67,
          "trend": "up"
        }
      }
    },
    "recent_deliveries": [
      {
        "id": "uuid",
        "delivery_code": "DEL-12345",
        "from": "123 Main St",
        "to": "456 Oak Ave",
        "status": "in_transit",
        "payment_status": "paid",
        "delivery_fee": 25,
        "created_at": "2026-02-04T10:00:00Z"
      }
    ]
  }
}
```

## GET /customer/delivery/reports/summary — Get Reports Summary

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/reports#get-get-reports-summary)

Customer-scoped delivery reports summary.

### Returns
- KPIs (orders/delivered/in_transit/cancelled+returned/spend)
- Orders over time chart
- Status breakdown
- COD summary (scoped to the customer)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/reports/summary?range=last_7_days&group_by=auto

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| range | query string |  | last_7_days \| last_30_days \| this_week \| this_month \| this_year \| till_now \| custom | last_7_days |
| from_date | query string |  | Required when range=custom (YYYY-MM-DD) |  |
| to_date | query string |  | Required when range=custom (YYYY-MM-DD) |  |
| group_by | query string |  | auto \| day \| week \| month (auto picks based on range) | auto |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "range": {
      "key": "last_7_days",
      "from": "2026-02-24",
      "to": "2026-03-02",
      "tz": "UTC",
      "group_by": "day",
      "chart_from": "2026-02-24",
      "chart_to": "2026-03-02"
    },
    "kpis": {
      "orders": 120,
      "delivered": 90,
      "in_transit": 12,
      "cancelled_returned": 4,
      "spend": 250,
      "completion_rate": 75
    },
    "charts": {
      "orders_over_time": {
        "labels": [
          "2026-02-24",
          "2026-02-25"
        ],
        "series": {
          "created": [
            12,
            18
          ],
          "delivered": [
            8,
            15
          ]
        }
      },
      "status_breakdown": {
        "draft": 0,
        "pending": 8,
        "assigned": 6,
        "picked_up": 2,
        "in_transit": 4,
        "delivered": 90,
        "cancelled": 2,
        "returned": 2
      }
    },
    "cod": {
      "collected": 150,
      "remitted": 50,
      "outstanding": 100,
      "wallet_settled": 0
    }
  }
}
```

## GET /customer/delivery/reports/summary/export — Export Report Orders (CSV)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/reports#get-export-report-orders-csv)

Streams a CSV export of the underlying orders for the selected report range (same UTC boundary logic as summary).

### Response headers
- Content-Disposition
- X-Total-Rows
- X-Export-Tenant-Id
- X-Export-Customer-Id

text/csv

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/reports/summary/export?range=last_7_days&group_by=auto

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| range | query string |  | last_7_days \| last_30_days \| this_week \| this_month \| this_year \| till_now \| custom | last_7_days |
| from_date | query string |  | Required when range=custom (YYYY-MM-DD) |  |
| to_date | query string |  | Required when range=custom (YYYY-MM-DD) |  |
| group_by | query string |  | auto \| day \| week \| month (kept for parity with summary) | auto |



## POST /customer/delivery/price — Get Price

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/price#post-get-price)

Calculates delivery price for given coordinates and service. Optional `purpose=return` (body or query) requires an active customer-accessible service with `allow_returns=true`; otherwise HTTP 403 returns `return_delivery_service_not_allowed`. Unit size is derived from measured package items; when no usable measurements exist, the configured `general.unit_size.settings.missing_unit_size_fallback` is applied. Success also returns `unit_size` and `unit_size_source`. If the calculated unit is outside the service range, HTTP 400 returns code `can_not_use_this_delivery_for_this_unit_size`, a localized message, `field=unit_size`, `calculated_unit_size`, `allowed_unit_min`, `allowed_unit_max`, and `violation`. Frontend should display message and use code only for branching.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/customer/delivery/price

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_service_id | string |  |  |  |
| purpose | string |  |  |  |
| pickup_lat | number |  |  |  |
| pickup_lng | number |  |  |  |
| dropoff_lat | number |  |  |  |
| dropoff_lng | number |  |  |  |
| package | object |  |  |  |
| package.items | array<object> |  |  |  |
| package.items.quantity | number |  |  |  |
| package.items.weight | number |  |  |  |
| package.items.length | number |  |  |  |
| package.items.width | number |  |  |  |
| package.items.height | number |  |  |  |



### Request body (raw)

```json
{
  "delivery_service_id": "uuid",
  "purpose": "return",
  "pickup_lat": 40.7128,
  "pickup_lng": -74.006,
  "dropoff_lat": 40.7589,
  "dropoff_lng": -73.9851,
  "package": {
    "items": [
      {
        "quantity": 1,
        "weight": 1.5,
        "length": 30,
        "width": 20,
        "height": 15
      }
    ]
  }
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "price": 25.5
  }
}
```

### Response 400 Calculated Units Outside Service Range

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum"
}
```

## GET /customer/delivery/services — Get Services

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/services#get-get-services)

Lists available delivery services with customer and portal access filtering. Optional `purpose=return` restricts results to services with `allow_returns=true`. Each returned service includes `allow_returns`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/services?search=&delivery_type_code=&purpose=return

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| search | query string | No | Search by service name or code |  |
| delivery_type_code | query string | No | Filter by delivery type code |  |
| purpose | query string | No | Set to return to list only active customer-accessible services with allow_returns=true | return |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "services": [
      {
        "id": "uuid",
        "name": "Standard Delivery",
        "code": "STD",
        "unit_max": 5,
        "allow_returns": true,
        "delivery_type": {
          "id": "uuid",
          "name": "Delivery",
          "code": "delivery"
        }
      }
    ],
    "total": 1
  }
}
```

## POST /customer/delivery/returns — Request Return

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/returns#post-request-return)

Creates a shop-requested return for an order owned by the authenticated customer portal user. The original delivery must be `delivered`; there is no time limit and no approval step. A commercial reverse delivery is created with its own delivery code and `delivery_flow=commercial_return` on the logical return root. For a staged return, the parent carries this classification and its child legs keep their existing operational flow behavior. The original-to-return relationship remains in `return_requests`.

Payer roles are the return delivery's actual roles and are stored and returned unchanged: the customer is `sender` and the shop is `receiver`. Allowed request combinations:
- `payer_type=receiver`, `payment_method=wallet`: shop wallet
- `payer_type=receiver`, `payment_method=cash`: shop cash
- `payer_type=sender`, `payment_method=cash`: customer cash
- Sender wallet is rejected. Shop wallet is settled during commercial-return activation through `receiver_user_id`, and successful creation returns `payment_status=paid`. Goods COD/refund is not part of this endpoint; the created return always has `cod_amount=0`.

### Optional overrides
- `package_item_ids`: selected IDs from the original delivery. When omitted, all original package items are copied. An explicitly supplied array must be non-empty and all IDs must belong to the original delivery. The return unit size is calculated from the selected items. When the original delivery has no package items or package details, its existing `unit_size` is preserved; the return service's `unit_max` is never used as the return quantity fallback.
- `collection_location`: when omitted, uses the original customer/dropoff address.
- `return_location`: when omitted, uses the original shop/pickup address.
- `notes`

### Frontend integration contract
- Show the action only when order `return_eligible=true`.
- Load services with `GET /customer/delivery/services?purpose=return`.
- Calculate the displayed fee with `POST /customer/delivery/price` and body `purpose=return`, using collection coordinates as pickup and shop coordinates as dropoff.
- Ask for payer and payment method separately. Send `receiver` when the shop pays and `sender` when the customer pays. Offer Wallet or Cash for the shop and Cash only for the customer.
- On HTTP 201, use `data.return_delivery.delivery_code` and continue tracking from the original order detail. `data.return_delivery.payer_type` matches the submitted value, and `data.return_delivery.payment_status` confirms wallet settlement.
- Treat `delivery_flow=commercial_return` as the shop-requested return classification. Keep using `original_delivery_id` and `original_delivery_code` for the relationship to the original order.
- On 400/403/409, display localized `message` and use `code` only for branching.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/customer/delivery/returns

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | string | Yes |  |  |
| delivery_service_id | string | Yes | must be active, customer-accessible, portal-allowed, and `allow_returns=true` |  |
| return_reason | string | Yes |  |  |
| notes | string |  |  |  |
| payer_type | string | Yes |  |  |
| payment_method | string | Yes |  |  |
| package_item_ids | array<string> |  |  |  |
| collection_location | object |  |  |  |
| collection_location.full_address | string |  |  |  |
| collection_location.latitude | number |  |  |  |
| collection_location.longitude | number |  |  |  |
| collection_location.country | string |  |  |  |
| collection_location.contact_info | object |  |  |  |
| collection_location.contact_info.name | string |  |  |  |
| collection_location.contact_info.phone | string |  |  |  |
| return_location | object |  |  |  |
| return_location.full_address | string |  |  |  |
| return_location.latitude | number |  |  |  |
| return_location.longitude | number |  |  |  |
| return_location.country | string |  |  |  |
| return_location.contact_info | object |  |  |  |
| return_location.contact_info.name | string |  |  |  |
| return_location.contact_info.phone | string |  |  |  |



### Request body (raw)

```json
{
  "delivery_id": "original-delivery-uuid",
  "delivery_service_id": "return-enabled-service-uuid",
  "return_reason": "Customer requested return",
  "notes": "Call before collection",
  "payer_type": "receiver",
  "payment_method": "wallet",
  "package_item_ids": [
    "original-package-item-uuid"
  ],
  "collection_location": {
    "full_address": "Customer address",
    "latitude": 36.1911,
    "longitude": 44.0092,
    "country": "IQ",
    "contact_info": {
      "name": "Customer",
      "phone": "+9647000000000"
    }
  },
  "return_location": {
    "full_address": "Shop address",
    "latitude": 36.2,
    "longitude": 44.02,
    "country": "IQ",
    "contact_info": {
      "name": "Shop",
      "phone": "+9647500000000"
    }
  }
}
```

### Response 201 Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Return request created successfully",
  "data": {
    "return_request": {
      "id": "return-request-uuid",
      "status": "approved",
      "requested_at": "2026-08-21T10:00:00.000Z",
      "requested_by_user_id": "shop-user-uuid",
      "request_source": "customer_portal"
    },
    "original_delivery": {
      "id": "original-delivery-uuid",
      "delivery_code": "DEL1001",
      "return_status": "return_approved",
      "return_delivery_code": "DEL1050"
    },
    "return_delivery": {
      "id": "return-delivery-uuid",
      "delivery_code": "DEL1050",
      "status": "pending",
      "payment_status": "paid",
      "delivery_flow": "commercial_return",
      "delivery_fee": 5000,
      "payer_type": "receiver",
      "payment_method": "wallet"
    }
  }
}
```

### Response 400 Invalid Payment Combination

Bad Request

```json
{
  "success": false,
  "code": "invalid_return_payment_combination",
  "fields": [
    "payer_type",
    "payment_method"
  ],
  "message": "Choose shop wallet, shop cash, or customer cash for the return fee"
}
```

### Response 403 Return Service Not Allowed

Forbidden

```json
{
  "success": false,
  "code": "return_delivery_service_not_allowed",
  "field": "delivery_service_id",
  "message": "This delivery service is not available for returns"
}
```

### Response 409 Return Already Exists

Conflict

```json
{
  "success": false,
  "code": "return_already_exists_for_delivery",
  "message": "Return already exists for delivery"
}
```

## GET /customer/wallet/dashboard — Get Wallet Dashboard

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/wallet#get-get-wallet-dashboard)

Customer wallet dashboard data for UI cards + transaction history.

### Rules
- `wallet.balance` is currently usable funds; active draft-delivery holds have already been deducted.
- `wallet.reserved` is the total held for active wallet-paid draft deliveries.
- No currency field is returned; the frontend displays currency from tenant configuration.
- Cards: Total Top-ups (lifetime), Total Spent (lifetime), This Month (current month spent). Refunds do not inflate Total Spent or orders-paid counts.
- All history contains top-ups, delivery-fee payments, positive delivery-fee refunds, and payouts.
- Payment history contains delivery-fee charges and positive delivery-fee refunds. Top-ups and Withdraw exclude refunds.
- A refund is returned in the existing transaction shape with `type = payment`, a localized refund title/subtitle, positive `amount`, post-refund `running_balance`, and its delivery snapshot fields.
- `history.statuses` counts use the same optional `from_date` / `to_date` population as history pagination.
- Withdraw tab returns payout transactions (wallet_transactions.type = 'payout').
- Per transaction running balance is returned in running_balance (from wallet_transactions.balance_snapshot column).
- Every transaction includes nullable `delivery_id` and `delivery_code`. Delivery references use the stored wallet-transaction snapshot; non-delivery or unresolved historical rows return null.

### Frontend integration contract
- For a transaction linked to a delivery, use `delivery_id` for navigation and display `delivery_code` as the order identifier.
- Render a `type = payment` item with positive `amount` as incoming/refunded; do not assume every Payment item is a debit.
- Do not parse `subtitle` or use `reference_id` as a display code.
- Treat null delivery fields as a transaction with no resolvable delivery context.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/wallet/dashboard?page=1&limit=5&type=all

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Page size (frontend default: 5) | 5 |
| type | query string |  | Transaction tab: all \| topup \| payment \| withdraw. Payment includes delivery-fee charges and positive delivery-fee refunds. | all |
| from_date | query string | No | Optional ISO date lower bound |  |
| to_date | query string | No | Optional ISO date upper bound |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "wallet": {
      "balance": 100,
      "reserved": 25.5
    },
    "cards": {
      "total_topups": {
        "amount": 100,
        "transactions_count": 1
      },
      "total_spent": {
        "amount": 40.5,
        "orders_paid_count": 2
      },
      "this_month": {
        "amount": 0
      }
    },
    "history": {
      "type": "all",
      "statuses": [
        {
          "key": "all",
          "label": "All",
          "count": 4
        },
        {
          "key": "topup",
          "label": "Top-ups",
          "count": 1
        },
        {
          "key": "payment",
          "label": "Payment",
          "count": 3
        },
        {
          "key": "withdraw",
          "label": "Withdraw",
          "count": 0
        }
      ],
      "transactions": [
        {
          "id": "refund-uuid",
          "type": "payment",
          "title": "Delivery fee refund",
          "subtitle": "Delivery fee refunded for order DEL1949",
          "amount": 15.5,
          "running_balance": 100,
          "created_at": "2024-01-30T15:30:00Z",
          "reference_id": "delivery-uuid",
          "reference_type": "delivery",
          "delivery_id": "delivery-uuid",
          "delivery_code": "1949"
        },
        {
          "id": "uuid",
          "type": "payment",
          "title": "Order payment",
          "subtitle": "Delivery fee for order DEL1949",
          "amount": -15.5,
          "running_balance": 84.5,
          "created_at": "2024-01-29T15:30:00Z",
          "reference_id": "delivery-uuid",
          "reference_type": "delivery",
          "delivery_id": "delivery-uuid",
          "delivery_code": "1949"
        },
        {
          "id": "uuid",
          "type": "topup",
          "title": "Top-up",
          "subtitle": "Wallet top-up via card",
          "amount": 100,
          "running_balance": 100,
          "created_at": "2024-01-25T15:30:00Z",
          "reference_id": "uuid",
          "reference_type": "payment",
          "delivery_id": null,
          "delivery_code": null
        }
      ],
      "pagination": {
        "current_page": 1,
        "per_page": 5,
        "total_items": 3,
        "total_pages": 1,
        "has_next_page": false,
        "has_previous_page": false
      }
    }
  }
}
```

## POST /customer/wallet/payout-requests — Create Payout Request

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/wallet#post-create-payout-request)

Customer creates a payout request (allowed user types configured in customer portal settings: individual and/or business).

### Rules
- Workflow only. No wallet debit happens here.
- Wallet balance is checked (must be >= amount).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/customer/wallet/payout-requests

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| amount | number | Yes | (required) |  |
| reference | string | No | (optional) |  |
| notes | string | No | (optional) |  |



### Request body (raw)

```json
{
  "amount": 0,
  "reference": "",
  "notes": ""
}
```

## GET /customer/wallet/payout-requests — List Payout Requests

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/wallet#get-list-payout-requests)

Lists customer-owned payout requests. Access controlled by customer portal wallet_settings.allowed_payout_user_types (individual and/or business).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/wallet/payout-requests?page=1&limit=10&status=all

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (>=1) | 1 |
| limit | query string |  | Items per page (1-50) | 10 |
| status | query string |  | all \| pending \| approved \| rejected | all |



## GET /customer/wallet/payout-requests/:id — Get Payout Request

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/wallet#get-get-payout-request)

Gets customer-owned payout request detail. Access controlled by customer portal wallet_settings.allowed_payout_user_types (individual and/or business).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/wallet/payout-requests/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Payout request ID | payout-request-uuid |



## GET /customer/delivery/payments — Get Delivery Payments

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/payments#get-get-delivery-payments)

Customer payment page for delivery module.

- Summary cards: total/paid/unpaid
- Tabs with counts: all/paid/pending/unpaid
- Transaction history based on `payments` where payment_type=DELIVERY and user_id=customer

### Notes
- Read-only endpoint (no Pay Now).

COD_SETTLEMENT records represent driver settlements and are excluded from this customer payment list and its summary, including for accounts that are both drivers and customers.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/payments?page=1&limit=10&status=all

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (>=1) | 1 |
| limit | query string |  | Items per page (1-50) | 10 |
| status | query string |  | Tab filter: all \| paid \| pending \| unpaid | all |
| from_date | query string | No | Optional ISO date lower bound (created_at) |  |
| to_date | query string | No | Optional ISO date upper bound (created_at) |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "summary": {
      "total_amount": 700,
      "paid_amount": 600,
      "unpaid_amount": 100
    },
    "statuses": [
      {
        "key": "all",
        "label": "All",
        "count": 3
      },
      {
        "key": "paid",
        "label": "Paid",
        "count": 1
      },
      {
        "key": "pending",
        "label": "Pending",
        "count": 1
      },
      {
        "key": "unpaid",
        "label": "Unpaid",
        "count": 2
      }
    ],
    "payments": [
      {
        "id": "uuid",
        "amount": 100,
        "currency": "IQD",
        "status": "PENDING",
        "payment_method": "NASS",
        "payment_method_type_id": "uuid",
        "payment_purpose": null,
        "created_at": "2026-02-28T06:52:48.720513Z",
        "receipt_status": null,
        "receipt_generated_at": null,
        "has_receipt": false,
        "delivery": {
          "id": "uuid",
          "delivery_code": 1909,
          "from": "Pickup address",
          "to": "Dropoff address"
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 3,
      "total_pages": 1,
      "has_next_page": false,
      "has_previous_page": false
    }
  }
}
```

## POST /customer/delivery/orders — Create Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/orders#post-create-order)

Creates a delivery order from the authenticated customer portal. When tenant setting `delivery_external_reference.unique_per_customer=true` (default), a non-empty `external_reference_id` must be unique among this customer root orders; duplicates return HTTP 400 `external_reference_id_already_exists`. When false, duplicates are allowed. The backend enforces customer_id from the access token and calculates unit size from package dimensions.

### Package contract
- Single package: send package.weight, package.length, package.width, and package.height. These values are stored at the top level of deliveries.package_details and used to calculate unit_size.
- Multipart shipment: send package.items[]. Each item is persisted in delivery_package_items; deliveries.package_details is rebuilt as the item/totals/status-summary object.
- Package details are optional; when no usable measurements exist, `general.unit_size.settings.missing_unit_size_fallback` selects service minimum, half of service maximum, service maximum, or rejects the request.
- Optional `is_draft=true` creates the order as Draft; it may be sent in the JSON body or as `?is_draft=true`. Missing or false preserves the current Pending behavior. For a wallet-paid draft, the fee moves from usable `balance` to `reserved` without creating a wallet transaction or payment. Execute captures the reservation and creates the delivery-fee transaction; cancelling the draft releases it back to usable balance. Draft orders do not queue driver assignment until Execute.

### Warehouse staging
- The frontend does not send requires_warehouse_staging or staging_warehouse_id.
- The backend resolves the selected delivery service's effective warehouse policy.
- When warehouse_policies.pickup_delivery_required=true, the backend selects the configured default staging warehouse or applies staging_selection_strategy.
- A staged order creates a parent delivery and two child legs: pickup to warehouse, then warehouse to delivery.
- When package.items is absent or empty, the backend creates one fallback warehouse inventory item named from the delivery code and customer name; it uses package.weight when supplied.
- Staged P&D database persistence is atomic: the parent, A/B legs, package items, inventory items, receiving order, and sequence plan either all commit or all roll back. Automatically allocated inventory item codes skip codes already claimed manually.

### Payment

Use payment.method_id (UUID from GET /customer/delivery/payment-methods).
- Wallet: method_id = wallet UUID. Pending orders charge immediately. Draft orders reserve first and charge only when executed.
- COD/cash: method_id = cod UUID. `payment.cod_amount` is optional goods COD money; omit it or send 0 for fee-only orders. Positive values remain independent from the delivery fee and must fit tenant COD min/max settings.
- Gateway (e.g. Nass): method_id = gateway UUID, back_ref REQUIRED (return URL).

### Frontend integration contract
- For a single package, submit package dimensions directly and do not create an item row.
- For multipart, submit package.items; its item summary replaces the direct package-details shape.
- Do not require COD amount for fee-only orders. Omit `payment.cod_amount` or send numeric 0; do not submit an empty string.
- Do not prompt for or submit a staging warehouse.
- Use data.order.id as the parent order ID and data.children for staged leg details.
- Handle HTTP 400 for non-delivery service types, unresolved warehouse, ambiguous warehouse selection, unavailable route segments, invalid payment method, or invalid positive COD amount.
- Default the create-as-draft checkbox to false. The backend accepts either body `is_draft: true` or query `?is_draft=true`. Dispatcher Execute should call `POST /delivery/deliveries/:deliveryId/status` with `{ "target_delivery_status": "pending" }`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/customer/delivery/orders

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pickup | object |  |  |  |
| pickup.location | object |  |  |  |
| pickup.location.full_address | string |  |  |  |
| pickup.location.latitude | number |  |  |  |
| pickup.location.longitude | number |  |  |  |
| pickup.location.country | string |  |  |  |
| pickup.location.city | string |  |  |  |
| pickup.location.state | string |  |  |  |
| pickup.location.postal_code | string |  |  |  |
| pickup.location.contact_info | object |  |  |  |
| pickup.location.contact_info.name | string |  |  |  |
| pickup.location.contact_info.phone | string |  |  |  |
| pickup.scheduled_pickup_time | string |  |  |  |
| dropoff | object |  |  |  |
| dropoff.location | object |  |  |  |
| dropoff.location.full_address | string |  |  |  |
| dropoff.location.latitude | number |  |  |  |
| dropoff.location.longitude | number |  |  |  |
| dropoff.location.country | string |  |  |  |
| dropoff.location.city | string |  |  |  |
| dropoff.location.state | string |  |  |  |
| dropoff.location.postal_code | string |  |  |  |
| dropoff.location.contact_info | object |  |  |  |
| dropoff.location.contact_info.name | string |  |  |  |
| dropoff.location.contact_info.phone | string |  |  |  |
| dropoff.scheduled_delivery_time | string |  |  |  |
| delivery_service_id | string |  |  |  |
| payer_type | string |  |  |  |
| payment | object |  |  |  |
| payment.method_id | string |  |  |  |
| external_reference_id | string |  |  |  |
| is_draft | boolean |  |  |  |
| notes | string |  |  |  |
| special_instructions | string |  |  |  |
| package | object |  |  |  |
| package.items | array<object> |  |  |  |
| package.items.name | string |  |  |  |
| package.items.quantity | number |  |  |  |
| package.items.description | string |  |  |  |
| package.items.weight | number |  |  |  |
| package.items.length | number |  |  |  |
| package.items.width | number |  |  |  |
| package.items.height | number |  |  |  |



### Request body (raw)

```json
{
  "pickup": {
    "location": {
      "full_address": "123 Main St, New York, NY 10001, US",
      "latitude": 40.7128,
      "longitude": -74.006,
      "country": "US",
      "city": "New York",
      "state": "NY",
      "postal_code": "10001",
      "contact_info": {
        "name": "John Doe",
        "phone": "+1234567890"
      }
    },
    "scheduled_pickup_time": "2026-02-05T10:00:00Z"
  },
  "dropoff": {
    "location": {
      "full_address": "456 Oak Ave, Brooklyn, NY 11201, US",
      "latitude": 40.7589,
      "longitude": -73.9851,
      "country": "US",
      "city": "Brooklyn",
      "state": "NY",
      "postal_code": "11201",
      "contact_info": {
        "name": "Jane Smith",
        "phone": "+1987654321"
      }
    },
    "scheduled_delivery_time": "2026-02-05T14:00:00Z"
  },
  "delivery_service_id": "uuid",
  "payer_type": "sender",
  "payment": {
    "method_id": "0760f75f-4aee-4776-afc0-4057d7ad458f"
  },
  "external_reference_id": "ORD-12345",
  "is_draft": false,
  "notes": "Handle with care",
  "special_instructions": "Fragile items",
  "package": {
    "items": [
      {
        "name": "Product A",
        "quantity": 2,
        "description": "Electronics",
        "weight": 1.5,
        "length": 30,
        "width": 20,
        "height": 15
      },
      {
        "name": "Product B",
        "quantity": 1,
        "description": "Clothing",
        "weight": 0.5,
        "length": 25,
        "width": 15,
        "height": 10
      }
    ]
  }
}
```

### Response 400 Invalid Payer Type

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_payer_type",
  "message": "Invalid payer type",
  "field": "payer_type"
}
```

### Response 400 Receiver Requires Cash Payment

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "receiver_payer_requires_cash_payment",
  "message": "Payment method is not available for delivery",
  "field": "payer_type"
}
```

### Response 400 Insufficient Wallet Balance

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

### Response 201 Success

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "parent-delivery-uuid",
      "delivery_code": "DEL-12345",
      "status": "pending",
      "payment_status": "pending",
      "payment_method": "cod",
      "payer_type": "receiver",
      "cod_amount": 0,
      "delivery_fee": 25.5,
      "requires_warehouse_staging": true,
      "staging_warehouse_id": "warehouse-uuid",
      "created_at": "2026-02-04T10:00:00Z"
    },
    "children": [
      {
        "id": "inbound-leg-uuid",
        "direction": "inbound",
        "status": "pending"
      },
      {
        "id": "outbound-leg-uuid",
        "direction": "outbound",
        "status": "draft"
      }
    ],
    "redirectUrl": null
  }
}
```

### Response 201 Wallet Payment Example

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "uuid",
      "delivery_code": "DEL-12345",
      "status": "pending",
      "payment_status": "paid",
      "payment_method": "wallet",
      "payer_type": "sender",
      "cod_amount": null,
      "delivery_fee": 25.5,
      "requires_warehouse_staging": false,
      "staging_warehouse_id": null,
      "created_at": "2026-02-04T10:00:00Z"
    },
    "children": [],
    "redirectUrl": null
  }
}
```

### Response 201 Staged Order Without Package Items

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "parent-delivery-uuid",
      "requires_warehouse_staging": true
    },
    "children": [
      {
        "id": "inbound-leg-uuid",
        "direction": "inbound"
      },
      {
        "id": "outbound-leg-uuid",
        "direction": "outbound"
      }
    ],
    "redirectUrl": null
  }
}
```

### Response 400 External Reference Already Exists

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "External reference ID already exists for this customer"
}
```

### Response 400 Calculated Units Outside Service Range

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum"
}
```

### Response 400 Staging Warehouse Cannot Be Resolved

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "no_staging_warehouse_available"
}
```

## GET /customer/delivery/orders — List Orders

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/orders#get-list-orders)

Lists the authenticated customer's delivery orders with filters and pagination. Every row includes compact `pickup.location` and `dropoff.location` objects containing the nullable localized `title` map and `full_address`; existing `from` and `to` strings remain unchanged. Every row also includes the nullable relationship fields `return_delivery_id`, `return_delivery_code`, `original_delivery_id`, and `original_delivery_code`. An original delivery populates the `return_delivery_*` pair; a return delivery populates the `original_delivery_*` pair. The values come from the maintained delivery projection, so this list performs no per-row return lookup or join. `return_eligible` is true only for a delivered order without an existing return relationship. For a linked operational return, the original/P&D parent has literal `status=returning` only after physical pickup or in-transit starts; assignment alone keeps it `staged_at_warehouse`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/orders?page=1&limit=10&status=pending

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (>=1) | 1 |
| limit | query string |  | Items per page (1-50) | 10 |
| status | query string |  | Filter by status | pending |
| payment_method | query string | No | Filter by payment method (wallet/cod) |  |
| payment_status | query string | No | Filter by payment status: pending, paid, failed, or refunded |  |
| payer_type | query string | No | Filter by payer type: sender \| receiver |  |
| start_date | query string | No | Start date filter (ISO format) |  |
| end_date | query string | No | End date filter (ISO format) |  |
| search | query string | No | Search by delivery code or reference |  |



### Response 200 Success - Transfer manifest provenance

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": "delivery-uuid",
        "source_transfer_manifest_id": "manifest-uuid"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 1
    }
  }
}
```

### Response 200 Success - Location titles

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": "uuid",
        "from": "123 Main St",
        "to": "456 Oak Ave",
        "pickup": {
          "location": {
            "title": {
              "en": "Main warehouse"
            },
            "full_address": "123 Main St"
          }
        },
        "dropoff": {
          "location": {
            "title": {
              "en": "Customer address"
            },
            "full_address": "456 Oak Ave"
          }
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 1
    }
  }
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": "uuid",
        "delivery_code": "DEL-12345",
        "status": "pending",
        "payment_status": "paid",
        "payment_method": "wallet",
        "payer_type": "sender",
        "delivery_fee": 25.5,
        "return_delivery_id": null,
        "return_delivery_code": null,
        "original_delivery_id": null,
        "original_delivery_code": null,
        "from": "123 Main St",
        "to": "456 Oak Ave",
        "created_at": "2026-02-04T10:00:00Z",
        "updated_at": "2026-02-04T10:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 25
    }
  }
}
```

### Response 200 Success - Bidirectional return delivery links

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": "original-delivery-uuid",
        "delivery_code": "1001",
        "status": "delivered",
        "return_delivery_id": "return-delivery-uuid",
        "return_delivery_code": "1050",
        "original_delivery_id": null,
        "original_delivery_code": null
      },
      {
        "id": "return-delivery-uuid",
        "delivery_code": "1050",
        "status": "pending",
        "return_delivery_id": null,
        "return_delivery_code": null,
        "original_delivery_id": "original-delivery-uuid",
        "original_delivery_code": "1001"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 2
    }
  }
}
```

## GET /customer/delivery/orders/export — Export Orders (CSV)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/orders#get-export-orders-csv)

Streams a CSV export of the customer's orders using the same filters as List Orders. Supports realtime progress (stream) and abort by canceling the HTTP request.

### Response headers
- Content-Disposition (filename)
- X-Total-Rows (for progress)
- X-Export-Tenant-Id
- X-Export-Customer-Id

text/csv

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/orders/export?status=pending&start_date=2026-03-01&end_date=2026-03-03

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | query string | No | Filter by status | pending |
| debug | query string | No | Debug mode (local/dev only; returns JSON with counts instead of CSV) | 1 |
| payment_method | query string | No | Filter by payment method |  |
| payment_status | query string | No | Filter by payment status: pending, paid, failed, or refunded |  |
| payer_type | query string | No | Filter by payer type: sender \| receiver |  |
| start_date | query string | No | Start date (YYYY-MM-DD or ISO) | 2026-03-01 |
| end_date | query string | No | End date (YYYY-MM-DD or ISO) | 2026-03-03 |
| search | query string | No | Search by delivery code/reference |  |



## GET /customer/delivery/orders/:id — Get Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/orders#get-get-order)

Gets full order details for a delivery owned by the authenticated customer. `data.order.notes` and `data.order.special_instructions` are separate nullable fields; when a package snapshot is present, `data.order.package.special_instructions` contains the same handling instructions saved from the create request. `data.order.pickup.location.title` and `data.order.dropoff.location.title` contain the nullable localized title map captured in each historical delivery-location snapshot; clients should read the requested language key and fall back to `full_address` when it is missing or empty. `data.order` also includes the nullable relationship fields `return_delivery_id`, `return_delivery_code`, `original_delivery_id`, and `original_delivery_code`. For an original order, `data.return_delivery` contains the linked return's basic `id`, `delivery_code`, `status`, and customer-visible `driver`. For a return order, `data.original_delivery` has the same shape. A linked operational return changes the canonical original/P&D parent to literal `data.order.status=returning` at physical pickup/in-transit and to `returned` at completion; `data.order.return_status` remains the separate return lifecycle. Driver is null when customer portal driver visibility is disabled or no driver is assigned. Existing `return_request` metadata remains on the order. Returns 404 when the order is absent or not owned by the customer, and 500 when delivery or return-context lookup fails.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/orders/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Delivery order ID | uuid |



### Response 200 Success - Transfer manifest provenance

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "delivery-uuid",
      "source_transfer_manifest_id": "manifest-uuid",
      "pickup": {
        "location": {
          "title": {
            "en": "Processing point"
          },
          "full_address": "100 Meter Road, Erbil"
        }
      }
    }
  }
}
```

### Response 200 Success - Location titles

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "uuid",
      "pickup": {
        "location": {
          "title": {
            "en": "Main warehouse"
          },
          "full_address": "123 Main St"
        }
      },
      "dropoff": {
        "location": {
          "title": {
            "en": "Customer address"
          },
          "full_address": "456 Oak Ave"
        }
      }
    }
  }
}
```

### Response 200 Success - Independent notes and special instructions

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "uuid",
      "notes": "Handle with care",
      "special_instructions": "Fragile item",
      "package": {
        "special_instructions": "Fragile item"
      }
    }
  }
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "uuid",
      "delivery_code": "DEL-12345",
      "status": "in_transit",
      "payment_status": "paid",
      "payment_method": "wallet",
      "payer_type": "sender",
      "cod_amount": null,
      "delivery_fee": 25.5,
      "return_delivery_id": null,
      "return_delivery_code": null,
      "original_delivery_id": null,
      "original_delivery_code": null,
      "created_at": "2026-02-04T10:00:00Z",
      "pickup": {
        "location": {
          "full_address": "123 Main St, New York, NY 10001, US",
          "latitude": 40.7128,
          "longitude": -74.006,
          "country": "US",
          "city": "New York",
          "state": "NY",
          "postal_code": "10001",
          "contact_info": {
            "name": "John Doe",
            "phone": "+1234567890"
          }
        },
        "scheduled_time": "2026-02-05T10:00:00Z"
      },
      "dropoff": {
        "location": {
          "full_address": "456 Oak Ave, Brooklyn, NY 11201, US",
          "latitude": 40.7589,
          "longitude": -73.9851,
          "country": "US",
          "city": "Brooklyn",
          "state": "NY",
          "postal_code": "11201",
          "contact_info": {
            "name": "Jane Smith",
            "phone": "+1987654321"
          }
        },
        "scheduled_time": "2026-02-05T14:00:00Z"
      },
      "service": {
        "id": "uuid",
        "name": "Standard Delivery",
        "code": "STD",
        "delivery_type": {
          "id": "uuid",
          "name": "Delivery",
          "code": "delivery"
        }
      },
      "unit_size": 3,
      "package": {
        "items": [
          {
            "id": "uuid",
            "name": "Product A",
            "quantity": 2,
            "description": "Electronics",
            "weight": 1.5,
            "length": 30,
            "width": 20,
            "height": 15,
            "item_code": "DEL-12345-1",
            "label_url": null,
            "created_at": "2026-02-04T10:00:00Z",
            "updated_at": "2026-02-04T10:00:00Z"
          }
        ],
        "total_items": 1,
        "total_quantity": 2
      },
      "status_updates": [
        {
          "status": "pending",
          "timestamp": "2026-02-04T10:00:00Z"
        },
        {
          "status": "assigned",
          "timestamp": "2026-02-04T10:30:00Z",
          "location": {
            "latitude": 40.7128,
            "longitude": -74.006
          },
          "notes": "Assigned to driver"
        },
        {
          "status": "in_transit",
          "timestamp": "2026-02-04T11:00:00Z"
        }
      ],
      "tracking": {
        "driver": {
          "name": "John Doe",
          "phone": "+1234567890"
        },
        "last_location": {
          "latitude": 40.75,
          "longitude": -73.99,
          "updated_at": "2026-02-04T11:00:00Z"
        }
      }
    }
  }
}
```

### Response 200 Success - Original order with return delivery

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "original-delivery-uuid",
      "delivery_code": "1001",
      "status": "delivered",
      "return_delivery_id": "return-delivery-uuid",
      "return_delivery_code": "1050",
      "original_delivery_id": null,
      "original_delivery_code": null
    },
    "return_delivery": {
      "id": "return-delivery-uuid",
      "delivery_code": "1050",
      "status": "pending",
      "driver": null
    }
  }
}
```

### Response 200 Success - Return order with original delivery

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "return-delivery-uuid",
      "delivery_code": "1050",
      "status": "pending",
      "return_delivery_id": null,
      "return_delivery_code": null,
      "original_delivery_id": "original-delivery-uuid",
      "original_delivery_code": "1001"
    },
    "original_delivery": {
      "id": "original-delivery-uuid",
      "delivery_code": "1001",
      "status": "delivered",
      "driver": {
        "id": "driver-uuid",
        "full_name": "Driver Name"
      }
    }
  }
}
```

## POST /customer/delivery/orders/:id/cancel — Cancel Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/orders#post-cancel-order)

Cancels an order owned by the authenticated customer. Cancellation is blocked transactionally with HTTP 409 `cannot_cancel_delivery_after_driver_assignment` once any assignment in the logical order is active, and with HTTP 409 `cannot_cancel_delivery_in_terminal_status` after final delivery or another terminal logical state. For staged/P&D, the parent or any operational child ID resolves to the canonical parent; a completed inbound warehouse leg does not block cancellation while final delivery remains incomplete. Paid family delivery fees and collected COD are refunded once to the sender wallet, and the success response returns the canonical root order ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/customer/delivery/orders/:id/cancel

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Delivery order ID | uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "reason": "Customer requested cancellation"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "uuid",
      "status": "cancelled",
      "updated_at": "2026-02-04T12:00:00Z"
    }
  }
}
```

### Response 409 Error - Assigned Order

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot cancel delivery after driver assignment"
}
```

### Response 409 Error - Terminal Logical Order

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery cannot be cancelled"
}
```

## GET /customer/delivery/portal-settings — Get Portal Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/settings#get-get-portal-settings)

Returns the effective tenant configuration and configured resources for the customer portal delivery creation flow.

- Requires `Authorization: Bearer {{access_token}}`.

### Frontend integration contract
- Read delivery services from `data.resources.delivery_services`.
- Use each service `id` as `delivery_service_id` when creating an order.
- Use `delivery_type.code` for delivery-type-specific UI behavior; `delivery_type` can be `null` only when the service has no related type.
- Use `data.delivery_service_settings.default_delivery_service_id` to preselect the configured default service.

### Important errors
- HTTP 401 when the customer or tenant context is missing.
- HTTP 500 when portal settings cannot be loaded.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/portal-settings

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success



```json
{
  "success": true,
  "data": {
    "pickup_settings": {
      "mode": "predefined_only",
      "allow_change": true,
      "default_pickup_address_id": "uuid"
    },
    "delivery_service_settings": {
      "default_delivery_service_id": "service-uuid"
    },
    "cod_settings": {
      "enabled": true,
      "required": false,
      "allow_zero": true,
      "min": 0,
      "max": 500000
    },
    "delivery_creation_fields": {
      "reference_id_enabled": true,
      "invoicing_enabled": false,
      "custom_shipment_label_enabled": false
    },
    "package_settings": {
      "multipart_enabled": true
    },
    "visibility_settings": {
      "driver_info": {
        "show_driver_info": true,
        "show_driver_phone": false
      },
      "tracking": {
        "realtime_tracking_enabled": true
      },
      "chat": {
        "support_chat_enabled": true,
        "driver_chat_enabled": false
      }
    },
    "support_ticket_settings": {
      "enabled": true,
      "allow_customer_close": true,
      "default_priority": "medium"
    },
    "currency": {
      "currency_code": "IQD",
      "currency_symbol": "د.ع",
      "currency_name": "Iraqi Dinar",
      "exchange_rate_to_system": 1
    },
    "resources": {
      "pickup_addresses": [],
      "delivery_services": [
        {
          "id": "service-uuid",
          "name": "Standard Delivery",
          "code": "STD",
          "service_level": "standard",
          "delivery_type": {
            "id": "delivery-type-uuid",
            "name": "Delivery",
            "code": "delivery"
          }
        }
      ]
    }
  }
}
```

## GET /customer/delivery/payment-methods — Get Payment Methods

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/settings#get-get-payment-methods)

Lists payment methods available for the customer delivery portal (delivery module only).

- Filters by allowed_modules containing 'ALL' (excludes RESTAURANT-only methods like Pay at Counter)
- Respects tenant_payment_method_settings and portal cod_settings (COD hidden when disabled)
- For gateways: checks tenant_gateway_configs (tenant mode) or system_gateway_configs (system mode)
- For gateway_code `cod`, use the localized `display_name` (Cash) for the delivery-fee payment method. The canonical `name`, machine code, type, and payment behavior are unchanged.

### Response fields

id, gateway_code (use for payment.method in create order), name, type, display_name (jsonb), icon_url, description (jsonb), is_gateway

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/delivery/payment-methods

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "payment_methods": [
      {
        "id": "0760f75f-4aee-4776-afc0-4057d7ad458f",
        "gateway_code": "cod",
        "name": "Cash on Delivery",
        "type": "COD",
        "display_name": {
          "en": "Cash",
          "ar": "نقدًا",
          "fa": "نقدی",
          "ku": "نەقدی"
        },
        "icon_url": null,
        "description": {
          "en": "Pay when you receive your order"
        },
        "is_gateway": false
      },
      {
        "id": "c3164119-6fa4-4917-8021-b33a40ce7462",
        "gateway_code": "wallet",
        "name": "Wallet Payment",
        "type": "WALLET",
        "display_name": {
          "en": "Wallet",
          "ar": "المحفظة",
          "ku": "جزدان"
        },
        "icon_url": null,
        "description": {
          "en": "Pay using your wallet balance"
        },
        "is_gateway": false
      },
      {
        "id": "34814962-2627-43d2-a3ae-c1c473e1cf3d",
        "gateway_code": "NASS",
        "name": "Nass",
        "type": "GATEWAY",
        "display_name": {
          "en": "Nass Payment",
          "ar": "دفع ناس",
          "ku": "پارەدانی ناس"
        },
        "icon_url": "https://api.ondi.io/storage/v1/object/public/system/brand/nass.png",
        "description": {
          "en": "Nass payment gateway integration"
        },
        "is_gateway": true
      }
    ]
  }
}
```

## POST /customer/support/tickets — Create Support Ticket (Customer Portal)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/support-tickets#post-create-support-ticket-customer-portal)

Creates a customer-portal support ticket and its chat conversation. `source` defaults to `customer_portal`. Pass `related_delivery_id` to link a delivery (optional). A 409 is returned if an open ticket already exists for the same delivery.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/customer/support/tickets

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| subject | string |  |  |  |
| priority | string |  |  |  |
| source | string |  |  |  |
| related_delivery_id | string |  |  |  |
| message | string |  |  |  |



### Request body (raw)

```json
{
  "subject": "Delivery delayed",
  "priority": "medium",
  "source": "customer_portal",
  "related_delivery_id": "uuid",
  "message": "My delivery is delayed, please help"
}
```

## POST /customer/support/tickets — Create Support Ticket (Storefront / Market)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/support-tickets#post-create-support-ticket-storefront-market)

Creates a storefront-originated support ticket. `source` MUST be `storefront` and `related_order_id` is required. The order is verified to belong to the authenticated buyer and to the same tenant before the ticket is created. The conversation title and first message include the order number.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/customer/support/tickets

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| subject | string |  |  |  |
| priority | string |  |  |  |
| source | string |  |  |  |
| related_order_id | string |  |  |  |
| message | string |  |  |  |



### Request body (raw)

```json
{
  "subject": "Wrong item received",
  "priority": "high",
  "source": "storefront",
  "related_order_id": "uuid",
  "message": "My order had the wrong product, please help."
}
```

### Response 409 409 - Ticket already open for order

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "A support ticket is already open for this item",
  "error": "support_ticket_already_open_for_related",
  "data": {
    "existing_ticket_id": "ticket-uuid",
    "existing_conversation_id": "conversation-uuid"
  }
}
```

## GET /customer/support/tickets — List Support Tickets

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/support-tickets#get-list-support-tickets)

Lists the authenticated user's tickets with conversation summary. Use `source` to scope a portal-specific view (e.g. only storefront-order tickets).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/support/tickets?page=1&limit=20&status=open&source=storefront

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (>=1) | 1 |
| limit | query string |  | Items per page (1-50) | 20 |
| status | query string | No | open \| closed | open |
| source | query string | No | customer_portal \| storefront \| restaurant — filter by ticket origin | storefront |



## GET /customer/support/tickets/:id — Get Support Ticket

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/support-tickets#get-get-support-ticket)

Gets a single ticket (customer-scoped) with conversation summary, `assigned_to_user`, linked `delivery` (when source=customer_portal) or `order` (when source=storefront).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/customer/support/tickets/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Support ticket ID | uuid |



## POST /customer/support/tickets/:id/close — Close Ticket

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/customer-portal/delivery-management/support-tickets#post-close-ticket)

Closes the support ticket (only if allow_customer_close is enabled in portal settings).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/customer/support/tickets/:id/close

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Support ticket ID | uuid |



## GET /delivery/dispatcher-contacts — List Dispatcher Contacts

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-list-dispatcher-contacts)

Returns the active dispatchers the authenticated driver may contact.

### Authentication and business rules
- Driver JWT authentication is required.
- No delivery/order ID is required for this list.
- The driver and dispatchers must belong to the authenticated tenant.
- Only active users with the exact `dispatcher` role are returned.
- When both the driver and dispatcher have explicit team mappings, they must share at least one team. Existing all-team behavior is preserved: no driver mapping means the driver may see all dispatchers; no dispatcher mapping means that dispatcher is available to all teams.
- Team IDs and names are authorization-only and are never returned.

### Success (HTTP 200)

returns `success` and `dispatchers`. Each dispatcher contains only `user_id`, `full_name`, `avatar_url`, and `phone`. An empty eligible set returns HTTP 200 with `dispatchers: []`.

### Important errors
- HTTP 403 when the authenticated user is not a driver or the driver record is unavailable. The response `message` is localized; English for a non-driver is `Only drivers can perform this action`.

### Frontend integration contract

Load this endpoint when the driver opens the dispatcher contact picker. If one result is returned, the UI may use it directly; if multiple are returned, let the driver choose. Open `tel:<phone>` for calls. Disable or hide Call when `phone` is null. Show `No dispatcher available` for an empty array. To message a dispatcher from inside an order, call `POST /messaging/dispatcher-conversations` with the current `delivery_id` and selected `dispatcher_user_id`. Display the returned plain-language `message`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/dispatcher-contacts

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "dispatchers": [
    {
      "user_id": "dispatcher-user-uuid",
      "full_name": "Shivan Ahmed",
      "avatar_url": "https://example.com/avatar.png",
      "phone": "+9647500000000"
    }
  ]
}
```

### Response 403 Error - Driver Only

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Only drivers can perform this action"
}
```

## GET /delivery/deliveries — List Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-list-deliveries)

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

### Search
- `search` matches the delivery search fields, `barcode`, and `external_reference_id`.
- Delivery order codes match with or without the `DEL` prefix. Examples: `DEL1911`, `DEL19`, `1911`, and `19` can all match delivery_code `1911`.
- A non-empty `search` returns matching orders in ANY status: the default `draft`/`delivered` exclusions are skipped and any `status` or `column_filters.status` value is ignored for that request. Clearing `search` restores normal status filtering.

### Status filtering
- When no `search`, explicit delivery-status selector, return selector, or assignment-status filter is sent, the list keeps the existing default behavior and excludes `draft` and `delivered` rows.
- A non-empty standalone `assignment_status` or `column_filters.assignment_status` skips only those default exclusions, allowing matching assignments on terminal deliveries to remain visible. It does not override explicit `status`, `column_filters.status`, or `is_completed` constraints. Both assignment-filter forms are exact matches and are AND-combined when supplied together.
- Frontend integration: send the selected assignment state as `column_filters={"assignment_status":"<status>"}` (preferred for the grid) or standalone `assignment_status=<status>`; omit it when clearing the filter. No response fields or navigation behavior change.
- When `search` is non-empty, status filtering is bypassed and matching orders in any status (`pending`, `in_transit`, `returning`, `delivered`, `cancelled`, `draft`, etc.) are returned.
- When `column_filters.status` is a real status value (and no search), the list applies exact delivery-status matching.
- `returning` is stored on the canonical original/P&D parent only after its linked `operational_return` reaches `picked_up` or `in_transit`. Pending, Assigned, and En route to pickup return work leaves the parent Staged at warehouse. The failed outbound child remains Failed.
- When `column_filters.status` is the literal string `All` (and no search), the list applies no delivery-status equality filter and returns every status, including `draft`, `delivered`, `cancelled`, and other statuses. Pagination, sorting, search, and all other filters still apply.

### Zones

Each row includes `pickup_zone_id` and `dropoff_zone_id` (from `deliveries`, exposed on `delivery_list_view`). Query `zone_ids` matches if either column is in the list (same as driver zone scoping).

### Carrier fields
- carrier_type, direction, pickup_warehouse_id, dropoff_warehouse_id, sequence_plan (hybrid only).

### Parent assignment projection
- For staged/P&D parent rows, the existing `assignment` object is built from child delivery assignments instead of a parent assignment row.
- `assignment.source = "child_rollup"` identifies this response-only projection.
- `assignment.delivery_id`, `assignment.delivery_code`, `assignment.direction`, and `assignment.leg_sequence` identify the active/source child leg.
- `assignment.status_updates` is a merged child-assignment timeline. Each event includes `source = "child_assignment"`, `assignment_id`, `delivery_id`, `delivery_code`, `direction`, and `leg_sequence`.
- Cancelled/returned child assignments remain visible for history/audit, so terminal staged parents still expose the source child driver instead of `Not Assigned`.
- Frontend integration: keep reading `delivery.driver`, `delivery.assignment.status`, and `delivery.assignment.status_updates`; no separate parent-specific field is required.

### Exceptions
- Each delivery row includes `has_exception` and `exception`, plus compatibility fields `has_open_exception` and `open_exception`.
- `has_exception=true` means `exception` contains the compact snapshot of the latest open `delivery_exceptions` row.
- `column_filters.has_open_exception` accepts boolean `true`/`false` or the strings `"true"`/`"false"`. `true` returns only orders with a current open exception, `false` returns only orders without one, and omitting the key returns both. The filter is applied before pagination and counting.
- `exception` includes `id`, `exception_type`, `exception_label`, `option`, `status`, `reported_at`, and `updated_at`.
- `option` is the tenant/system exception option snapshot matched by `exception_type`; use it for list rendering when present.
- Frontend integration: continue sending `column_filters={"has_open_exception":"true"}` for the Has Exception selection and omit `has_open_exception` when the filter is cleared. Show the exception indicator only when `has_exception` is true. Use `exception.exception_label` or `exception.option.label` for the list label. Fetch full details from `GET /delivery/exceptions?delivery_id=<delivery_id>&status=open` only when the user opens the exception context.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/deliveries?page=1&limit=10&status=&search=&customer_id=&driver_id=&assignment_status=&bulk_group=&delivery_type=&tenant_id=&column_filters=&sort_by=&sort_order=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of items per page | 10 |
| status | query string |  | Filter by delivery status. `returning` is the canonical original/P&D-parent status after a linked operational return is physically picked up; assignment alone keeps that order `staged_at_warehouse`. |  |
| search | query string |  | Case-insensitive partial search across delivery search fields, `barcode`, `external_reference_id`, and the contact phone saved in both `pickup_location.contact_info.phone` and `delivery_location.contact_info.phone`. Numeric input ignores spaces, punctuation, and Arabic/Persian digit glyphs without assuming a country or dial code. Longer numeric input also matches by its final seven digits so local and international representations of the same saved number can resolve to the order. Delivery order codes match with or without the DEL prefix, so DEL1911, DEL19, 1911, and 19 can all match delivery_code 1911. A non-empty search term returns matching orders in ANY status: it bypasses the default draft/delivered exclusions and ignores `status` and `column_filters.status` for the request. Frontend integration: continue sending the user's value through the existing `search` query parameter; the response shape is unchanged. |  |
| customer_id | query string |  | Filter by customer ID |  |
| driver_id | query string |  | Filter by driver ID |  |
| assignment_status | query string | No | Optional effective assignment-status filter. Direct deliveries use their own current assignment. Parent/P&D orders use the same child-leg assignment projected in the returned row. Use `unassigned` for rows with no effective current/projected assignment. Filtering, exact count, and pagination are applied in the database before response enrichment. When non-empty, the list skips its default `draft`/`delivered` exclusions so matching assignments remain discoverable on terminal deliveries. Any explicit delivery `status`, `column_filters.status`, or `is_completed` constraint still applies. AND-combined with `column_filters.assignment_status` when both are supplied. |  |
| bulk_group | query string |  | Filter by bulk group identifier |  |
| delivery_type | query string |  | Filter by delivery type (Delivery, Pickup, OnDemand, etc.) |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| column_filters | query string | No | Optional JSON string for column-level filters. Keys are AND-combined and ANDed with existing standalone filters. Supported keys: delivery_code, customer, driver, barcode, pickup_zone, dropoff_zone, pickup_address, delivery_address, pickup_warehouse, vehicle, notes, external_reference_id (ILIKE); status, return_status, delivery_flow, readiness_status, assignment_status, carrier_type, payment_status, payer_type, payment_method, direction, priority, delivery_service_id, source_transfer_manifest_id (=); has_open_exception, requires_warehouse_staging, is_ready_to_deliver, has_return_order, is_return_order, g2g_only, is_parent (boolean fields); cod_amount, delivery_fee (number or {min,max}); created_at, updated_at ({from,to}). `delivery_service_id` and `source_transfer_manifest_id` accept exact string IDs, are AND-combined with other filters, and execute in the database before exact counting and pagination. A non-empty `assignment_status` matches the effective assignment displayed on the row: the direct assignment for ordinary rows and the projected child-leg assignment for parent/P&D rows. Use `unassigned` when the row has no effective assignment. The filter bypasses the default `draft`/`delivered` exclusions, so matching terminal delivery statuses remain included; explicit delivery-status constraints still apply. Use {"assignment_status":"assigned"} or {"assignment_status":"unassigned"}. Filtering, exact count, and pagination execute in the database. `g2g_only=true` returns only deliveries with a transfer-manifest source, `g2g_only=false` returns only deliveries without a transfer-manifest source, and omitting `g2g_only` returns both. The G2G predicate runs in the database before exact counting and pagination. `is_parent=true` returns parent orders, `is_parent=false` returns non-parent top-level orders, and omitting `is_parent` preserves the normal top-level-order behavior. The exact `is_parent` predicate runs before counting and pagination. `has_return_order=true` returns original root orders with a non-null `return_delivery_id`; use it for the P&D `Has return order` filter. `is_return_order=true` returns return-generated orders with a non-null `original_delivery_id`; use it for the pickup/return order list. Both predicates execute before count and pagination and use the maintained delivery projection. When either filter is `true`, the list's default delivered/draft exclusion is bypassed so historical linked rows are not hidden. Boolean false applies the inverse predicate while retaining normal status handling; omit the key to return both states. `return_status` accepts exactly `return_requested`, `return_approved`, `return_denied`, `return_in_transit`, `return_received`, or `return_completed`; it returns matching original orders only and is applied before counting and pagination. Use {"return_status":"return_in_transit"}; omit `return_status` when clearing the filter. `delivery_flow` accepts exactly `replacement_forward`, `replacement_return`, `operational_return`, or `commercial_return`; use {"delivery_flow":"commercial_return"} and omit the key when clearing it. `readiness_status` accepts exactly `ready_to_pick_up` or `ready_to_deliver`. `ready_to_pick_up` means the next pending work collects from a non-warehouse origin, including a normal delivery or P&D inbound Leg A. `ready_to_deliver` means pending work starts from a warehouse, including a released P&D outbound Leg B; a staged parent is ready only when it is `staged_at_warehouse` and its outbound child is `pending`. Received-but-unreleased orders have no readiness value. The legacy `is_ready_to_deliver` boolean remains temporarily for deployed-client compatibility and is true only when `readiness_status=ready_to_deliver`. `has_open_exception`, `is_ready_to_deliver`, `has_return_order`, `is_return_order`, and `is_parent` accept boolean `true`/`false` or the strings `true`/`false`; omit the key to return both states. Use {"readiness_status":"ready_to_deliver"} for the delivery-ready list and omit `readiness_status` to clear the filter. Use {"status":"All"} to explicitly include every delivery status and bypass the default draft/delivered exclusions without applying a status equality filter. A non-empty `search` overrides only delivery-status handling; return status, return relationship, delivery flow, readiness, and other filters still apply. Example: {"has_return_order":true}. |  |
| column_filters.is_parent | query string | No | Documentation-only example for the nested `column_filters` JSON object. Send `is_parent` as boolean `true` or `false`; `true` returns parent orders and `false` returns non-parent top-level orders. The exact predicate is AND-combined with other filters and runs before counting and pagination. Omit the key to return both types under the endpoint's normal top-level-order behavior. | true |
| column_filters.delivery_service_id | query string | No | Documentation-only example for the nested `column_filters` JSON object. Send an exact string ID to return deliveries using that delivery service. The predicate is AND-combined with other filters and runs in the database before counting and pagination. | delivery-service-uuid |
| column_filters.source_transfer_manifest_id | query string | No | Documentation-only example for the nested `column_filters` JSON object. Send an exact string ID to return deliveries originating from that Transfer Manifest. The predicate is AND-combined with other filters and runs in the database before counting and pagination. | transfer-manifest-uuid |
| sort_by | query string | No | Optional column to sort by. Allowed values: delivery_code, status, delivery_status, return_status, priority, created_at, updated_at, cod_amount, delivery_fee, customer, driver. `status` and `delivery_status` both sort by the delivery status column. Default: created_at. |  |
| sort_order | query string | No | Optional sort order: asc or desc. Default: desc. |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success - Has return order filter

OK

```json
{
  "success": true,
  "message": "Deliveries retrieved successfully",
  "deliveries": [
    {
      "id": "original-delivery-uuid",
      "delivery_code": "2501",
      "return_delivery_id": "return-delivery-uuid",
      "return_delivery_code": "2601",
      "original_delivery_id": null
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 200 Success - Return-generated orders filter

OK

```json
{
  "success": true,
  "message": "Deliveries retrieved successfully",
  "deliveries": [
    {
      "id": "return-delivery-uuid",
      "delivery_code": "2601",
      "original_delivery_id": "original-delivery-uuid",
      "original_delivery_code": "2501"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 200 Success - Delivery Flow filter

OK

```json
{
  "success": true,
  "message": "Deliveries retrieved successfully",
  "deliveries": [
    {
      "id": "return-delivery-uuid",
      "status": "pending",
      "delivery_flow": "commercial_return"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 200 Success - Return Status filter

OK

```json
{
  "success": true,
  "message": "Deliveries retrieved successfully",
  "deliveries": [
    {
      "id": "original-delivery-uuid",
      "status": "delivered",
      "return_status": "return_in_transit",
      "return_delivery_id": "return-delivery-uuid",
      "original_delivery_id": null
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 200 Success - Readiness filter

OK

```json
{
  "success": true,
  "message": "Deliveries retrieved successfully",
  "deliveries": [
    {
      "id": "warehouse-origin-direct-uuid",
      "status": "pending",
      "readiness_status": "ready_to_deliver",
      "is_ready_to_deliver": true
    },
    {
      "id": "staged-parent-uuid",
      "status": "staged_at_warehouse",
      "readiness_status": "ready_to_deliver",
      "is_ready_to_deliver": true
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 2
}
```

### Response 200 Success - Bidirectional return delivery links

OK

```json
{
  "success": true,
  "deliveries": [
    {
      "id": "original-delivery-uuid",
      "delivery_code": "DEL1001",
      "status": "delivered",
      "delivery_flow": null,
      "return_status": "return_approved",
      "return_delivery_id": "return-delivery-uuid",
      "return_delivery_code": "DEL1050",
      "original_delivery_id": null,
      "original_delivery_code": null
    },
    {
      "id": "return-delivery-uuid",
      "delivery_code": "DEL1050",
      "status": "pending",
      "delivery_flow": "commercial_return",
      "return_delivery_id": null,
      "return_delivery_code": null,
      "original_delivery_id": "original-delivery-uuid",
      "original_delivery_code": "DEL1001"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 2
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Deliveries retrieved successfully",
  "deliveries": [
    {
      "id": "uuid",
      "tenant_id": "uuid",
      "external_reference_id": "ORD-12345",
      "pickup_location_id": "uuid",
      "delivery_location_id": "uuid",
      "carrier_type": "internal",
      "direction": null,
      "pickup_warehouse_id": null,
      "dropoff_warehouse_id": null,
      "sequence_plan": null,
      "special_instructions": "Handle with care",
      "package_details": {
        "items": [
          {
            "id": "uuid",
            "item_code": "DEL1324-1",
            "name": "{{item_name}}",
            "description": "{{item_description}}",
            "quantity": 1,
            "weight": null,
            "length": null,
            "width": null,
            "height": null,
            "label_url": null,
            "created_at": "2026-01-14T00:00:00Z",
            "updated_at": "2026-01-14T00:00:00Z"
          }
        ]
      },
      "customer_id": "uuid",
      "status": "pending",
      "exception": {
        "id": "exception-uuid",
        "exception_type": "wrong_address",
        "exception_label": "Wrong Address",
        "option": {
          "code": "wrong_address",
          "label": "Wrong Address",
          "allowed_statuses": [
            "picked_up",
            "in_transit"
          ]
        },
        "status": "open",
        "reported_at": "2026-06-24T08:00:00Z",
        "updated_at": "2026-06-24T08:00:00Z"
      },
      "has_exception": true,
      "open_exception": {
        "id": "exception-uuid",
        "exception_type": "wrong_address",
        "exception_label": "Wrong Address",
        "option": {
          "code": "wrong_address",
          "label": "Wrong Address",
          "allowed_statuses": [
            "picked_up",
            "in_transit"
          ]
        },
        "status": "open",
        "reported_at": "2026-06-24T08:00:00Z",
        "updated_at": "2026-06-24T08:00:00Z"
      },
      "has_open_exception": true,
      "created_at": "2023-08-01T10:00:00Z",
      "scheduled_pickup_time": "2023-08-02T14:00:00Z",
      "scheduled_delivery_time": "2023-08-02T16:00:00Z",
      "priority": 1,
      "notes": "Customer prefers delivery after 3pm",
      "bulk_group": "batch-2023-08-01",
      "delivery_type": "Delivery"
    },
    {
      "id": "uuid-hybrid",
      "tenant_id": "uuid",
      "carrier_type": "hybrid",
      "direction": null,
      "pickup_warehouse_id": "wh-uuid",
      "dropoff_warehouse_id": null,
      "sequence_plan": [
        {
          "seq": 1,
          "delivery_id": "uuid-child-internal",
          "type": "internal",
          "direction": "inbound"
        },
        {
          "seq": 2,
          "delivery_id": "uuid-child-prime",
          "type": "prime",
          "direction": "outbound"
        }
      ],
      "status": "pending",
      "exception": null,
      "has_exception": false,
      "open_exception": null,
      "has_open_exception": false
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 25,
  "stats": {
    "totalDeliveries": 150,
    "pendingDeliveries": 32,
    "deliveredDeliveries": 87,
    "cancelledDeliveries": 11
  }
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /delivery/driver-deliveries — Get Driver Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-get-driver-deliveries)

Driver delivery cards expose the existing parcel contract plus `returnStatus`. `returnStatus=null` means normal actions. `return_approved` or `return_in_transit` is the persistent instruction to hide Deliver, show Mark Returned, and use the card's existing pickup location as the return destination. This field is read directly from the delivery; the driver app never needs to query a resolved exception. After a failed operational return is received back at its pickup warehouse, the delivery becomes Pending and is discoverable in `status=pending` when the driver is eligible to self-assign. Its previous returned assignment remains audit history and is not returned as current ownership: the card has `assignmentId=null` and `assignmentStatus=null` until a fresh assignment is created. The frontend must show Assign, not Change Driver or Unassign, while those current-assignment fields are null. Every driver delivery row includes nullable `sourceTransferManifestId`; a non-null value identifies the originating Transfer Manifest. For a staged child, the value is inherited from its parent order. `pickupLocation` and `deliveryLocation` preserve their stored `title` value when present. Frontend integration: when `sourceTransferManifestId` is non-null and the relevant location title exists, show that title before the zone pill.

### G2G origin display

each delivery includes nullable `g2gOrigin` with `{ "branchName": string|null, "branchAddress": string|null }`. It is populated only for Transfer Manifest orders from a valid stored `manifest_origin_v1` snapshot, with the staged parent snapshot used when the child has no usable origin and its manifest ID is absent or matches the parent. Normal orders and older orders without usable saved origin return null. When `sourceTransferManifestId` is non-null, show a read-only Original G2G location line separately from pickup, using the available branch name and address; if `g2gOrigin` is null, show Origin unavailable. Hide the line only when the manifest ID is null. Keep `pickupLocation`, `deliveryLocation`, maps, navigation, route calculations, and driver actions unchanged. Origin metadata is returned by the existing batched list read or single detail read; do not request one detail per card.

Driver deliveries list (driver-only).

### Pagination order
- Rows keep the existing business priority and `updatedAt` descending order. Rows with the same `updatedAt` are ordered by `deliveryId` descending, so offset pages do not overlap because of timestamp ties.

### Default behavior (no status filter)
- Returns normal deliveries assigned to the driver
- Excludes delivered deliveries
- Includes a cancelled delivery only when its latest assignment for this driver is still `picked_up` or `en_route_to_delivery`; this represents physical parcel custody, not permission to continue delivery
- Excludes child deliveries whose Waybill is active (`drafted`, `sealed`, `assigned`, `picked_up`, `in_transit`, or `awaiting_receipt`). Those packages are represented by the Waybill card from `GET /delivery/driver-waybills` and must not be independently startable from the normal delivery list.

### Exception decision contract
- Every row includes `returnStatus`: `null`, `return_approved`, `return_in_transit`, or `return_completed`.
- While `status=exception`, an open exception blocks driver actions.
- After dispatcher Retry, `returnStatus` is null and the driver continues the normal delivery action.
- After dispatcher Return, `returnStatus=return_approved`; hide Delivered and show one whole-delivery Mark Returned action using `POST /delivery/deliveries/:deliveryId/status`.

### Cancelled custody contract
- Every row exposes `assignmentId` and `assignmentStatus`
- A cancelled custody row includes `returnInstruction` with `action=complete_return_handoff`, a localized `message`, and `destination.warehouseId` plus the existing location snapshot
- For an inbound warehouse leg, the destination is the drop-off warehouse; for an outbound warehouse leg, it is the pickup warehouse; a non-warehouse delivery returns to its pickup location
- Cancelled custody rows are ordered before normal active work and count in the `all` total until handoff completion

### Filtering
- `status=delivered`: returns only delivered deliveries
- `status=pending`: returns pending deliveries. If tenant `driver_config.driver_can_self_assign` is true, it also includes unassigned pending deliveries. Pending deliveries that are already assigned to this driver (pending acceptance) are returned first and include `pendingAcceptance: true`.
- `status=en_route_to_pickup`: returns deliveries whose delivery status has moved beyond assigned and the driver is travelling to pickup.
- Active-Waybill children are excluded from every status and search filter. Children of `received`, `closed`, or `cancelled` Waybills follow normal delivery filtering.

### Proof Requirements
- Each delivery card includes `proofRequirements.pickup` and `proofRequirements.delivery` when a delivery service policy can be resolved.
- If the pickup endpoint is a warehouse, pickup uses `proof_settings.warehouse_pickup`.
- If the pickup endpoint is not a warehouse, pickup uses `proof_settings.driver_pickup`.
- If the delivery endpoint is a warehouse, delivery uses `proof_settings.warehouse_delivery`.
- If the delivery endpoint is not a warehouse, delivery uses `proof_settings.driver_delivery`.

### Frontend contract
- When `status=cancelled` and `returnInstruction` is present, show the instruction and destination, hide every normal delivery action, and offer only the literal `returnInstruction.action`.
- Submit the row's child `deliveryId` to `POST /delivery/action/:deliveryId`; do not use a staged parent ID.
- Render status tabs in the backend-provided order: `all`, `pending` (label `Available`), `assigned`, `en_route_to_pickup`, `picked_up`, `in_transit`, `delivered`, `cancelled`, `returned`.
- Render and navigate active Waybill work from `/delivery/driver-waybills`, not from a child delivery card.
- The backend also suppresses `delivery_realtime_assignments` rows for active-Waybill children; use `waybill_realtime` to refresh Waybill cards.
- Use each card's `proofRequirements` to decide whether to prompt for pickup/delivery photo or signature.
- If a card has `deliveryFlow = "replacement_forward"`, show the replacement badge and the replacement completion flow. For staged replacements, use `replacementParentDeliveryId` only as the parent linkage id; continue sending the child `deliveryId` for driver actions.

### Replacement staging
- For the final outbound child of a warehouse-staged replacement parent, each delivery row exposes `deliveryFlow: "replacement_forward"` even though the child leg itself remains an operational child.
- That row also includes `replacementParentDeliveryId` with the staged parent id.

### Response includes
- `deliveries`: simplified UI shape
- `statuses`: list of status counts for the driver UI tabs/filters, ordered as `all`, `pending`, `assigned`, `en_route_to_pickup`, `picked_up`, `in_transit`, `delivered`, `cancelled`

### G2G origin display

each delivery includes nullable `g2gOrigin` with `{ "branchName": string|null, "branchAddress": string|null }`. It is populated only for Transfer Manifest orders from a valid stored `manifest_origin_v1` snapshot, with the staged parent snapshot used when the child has no usable origin and its manifest ID is absent or matches the parent. Normal orders and older orders without usable saved origin return null. When `sourceTransferManifestId` is non-null, show a read-only Original G2G location line separately from pickup, using the available branch name and address; if `g2gOrigin` is null, show Origin unavailable. Hide the line only when the manifest ID is null. Keep `pickupLocation`, `deliveryLocation`, maps, navigation, route calculations, and driver actions unchanged. Origin metadata is returned by the existing batched list read or single detail read; do not request one detail per card.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver-deliveries?page=1&limit=10&status=&search=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination (default: 1) | 1 |
| limit | query string |  | Number of items per page (default: 10) | 10 |
| status | query string | No | Filter by delivery status (optional) |  |
| search | query string | No | Search term for filtering deliveries (optional) |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success - Multipart staged delivery

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "deliveries": [
    {
      "deliveryId": "outbound-child-uuid",
      "deliveryCode": "1500B",
      "sourceTransferManifestId": "manifest-uuid",
      "status": "in_transit",
      "returnStatus": "return_approved",
      "pickupLocation": {
        "title": {
          "en": "Processing point"
        },
        "full_address": "Warehouse origin"
      },
      "quantity": 3,
      "parcelCount": 2,
      "packageDetails": {
        "total_items": 2,
        "total_quantity": 2,
        "items": [
          {
            "id": "parcel-1",
            "status": "pending",
            "parent_status": "pending",
            "actionable": true
          },
          {
            "id": "parcel-2",
            "status": "pending",
            "parent_status": "pending",
            "actionable": true
          }
        ]
      },
      "g2gOrigin": {
        "branchName": "Original branch",
        "branchAddress": "Original branch address"
      }
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 10
}
```

### Response 200 Success - Default (Assigned, excluding delivered)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "deliveries": [
    {
      "deliveryId": "active-child-leg-uuid",
      "status": "assigned",
      "returnStatus": null,
      "assignmentId": "assignment-uuid",
      "assignmentStatus": "assigned",
      "sourceTransferManifestId": null,
      "g2gOrigin": null
    }
  ],
  "statuses": [
    {
      "status": "all",
      "label": "All",
      "count": 1
    },
    {
      "status": "pending",
      "label": "Available",
      "count": 0
    },
    {
      "status": "assigned",
      "label": "Assigned",
      "count": 1
    },
    {
      "status": "cancelled",
      "label": "Cancelled",
      "count": 0
    },
    {
      "status": "returned",
      "label": "Returned",
      "count": 0
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 10
}
```

### Response 200 Success - Cancelled filter

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "deliveries": [
    {
      "deliveryId": "cancelled-child-leg-uuid",
      "status": "cancelled",
      "assignmentId": "assignment-uuid",
      "assignmentStatus": "picked_up",
      "sourceTransferManifestId": null,
      "g2gOrigin": null
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 10
}
```

### Response 200 Success - Pending (includes pending acceptance + unassigned if enabled)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Deliveries retrieved successfully",
  "deliveries": [
    {
      "deliveryId": "uuid-accept",
      "deliveryCode": 1290,
      "barcode": "DEL1290",
      "externalReferenceId": null,
      "status": "pending",
      "pendingAcceptance": true,
      "pending_acceptance": true,
      "sourceTransferManifestId": null,
      "g2gOrigin": null
    },
    {
      "deliveryId": "uuid-unassigned",
      "deliveryCode": 1890,
      "barcode": "DEL1890",
      "externalReferenceId": null,
      "status": "pending",
      "pendingAcceptance": false,
      "pending_acceptance": false,
      "sourceTransferManifestId": null,
      "g2gOrigin": null
    }
  ],
  "statuses": [
    {
      "status": "pending",
      "label": "Available",
      "count": 8
    }
  ],
  "total": 8,
  "page": 1,
  "limit": 10
}
```

### Response 200 Success - Delivered filter

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Deliveries retrieved successfully",
  "deliveries": [
    {
      "deliveryId": "uuid",
      "deliveryCode": 999,
      "barcode": "DEL999",
      "externalReferenceId": null,
      "status": "delivered",
      "pending_acceptance": false,
      "sourceTransferManifestId": null,
      "g2gOrigin": null
    }
  ],
  "statuses": [
    {
      "status": "delivered",
      "label": "Delivered",
      "count": 10
    }
  ],
  "total": 10,
  "page": 1,
  "limit": 10
}
```

### Response 403 Error - Not a Driver

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 404 Error - Driver Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not found"
}
```

## GET /delivery/driver/deliveries/{{delivery_id}} — Get Driver Delivery Detail V1

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-get-driver-delivery-detail-v1)

`delivery.sourceTransferManifestId` contains the originating Transfer Manifest ID or null. For a staged child, the value is inherited from its parent order. `pickupLocation.title` and `deliveryLocation.title` preserve the stored multilingual title value or return null. Frontend integration: when the manifest ID is non-null and the relevant title exists, show the localized title before the zone pill.

### G2G origin display

each delivery includes nullable `g2gOrigin` with `{ "branchName": string|null, "branchAddress": string|null }`. It is populated only for Transfer Manifest orders from a valid stored `manifest_origin_v1` snapshot, with the staged parent snapshot used when the child has no usable origin and its manifest ID is absent or matches the parent. Normal orders and older orders without usable saved origin return null. When `sourceTransferManifestId` is non-null, show a read-only Original G2G location line separately from pickup, using the available branch name and address; if `g2gOrigin` is null, show Origin unavailable. Hide the line only when the manifest ID is null. Keep `pickupLocation`, `deliveryLocation`, maps, navigation, route calculations, and driver actions unchanged. Origin metadata is returned by the existing batched list read or single detail read; do not request one detail per card.

Fast driver-only delivery detail projection. A valid user Bearer JWT is required; API keys are not accepted. The shared Delivery Edge middleware is the only authentication owner: it validates the credential and resolves the active user's tenant, roles, permissions, and team context. The handler rejects non-JWT and non-driver actors and passes only the middleware-owned tenant and user IDs to one service-role-only RPC. Any authenticated user with a current driver record may read an internal, non-parent delivery in the same tenant when delivery-service and customer team restrictions permit it, even when that delivery is unassigned or assigned to another driver. Assignment is returned only when it belongs to the authenticated driver; otherwise `assignment` is null and `pendingAcceptance` is false. Scanner and action APIs keep their existing assignment, route, Waybill, zone, and action-state authorization. A client-supplied tenant or driver ID is never accepted. Missing, cross-tenant, team-restricted, and unsupported delivery identifiers return HTTP 404 `delivery_not_found`.

The response is authoritative and uses camelCase with explicit nulls, numeric zeroes, and empty arrays. It contains current assignment/route data for the authenticated driver, customer contact data, pickup and delivery locations, endpoint warehouses, service/type, fees and collection amounts, proof requirements, leg-aware parcel state (including pickupStatus and actionable), current/parent exception history with labels, and compact `relationships`. `quantity` and `packageDetails.totalQuantity` preserve the legacy product/package count from `package_details.total_quantity`, defaulting to `1` when it is absent or invalid. `unitSize` remains the separate vehicle-capacity value and is never used as product quantity. The response intentionally excludes driver/vehicle objects, carrier/Prime data, shipment labels, proof history, order/status timelines, parent/child trees, search strings, and generic admin enrichment.

For a staged outbound child, `packageDetails.items` excludes source packages whose `return_status` is `return_approved`, `return_in_transit`, or `return_completed`. `packageDetails.totalItems`, its item-status summary, `parcelCount`, and `actionableParcelCount` describe the remaining projected packages. This matches the existing generic detail and driver-list package projection. The exclusion does not apply to inbound history or the separate return delivery. A missing pickup status does not hide an otherwise eligible package; only explicit `not_received` does. Clients use the returned items and `actionable` flags for package actions; no new request field or mobile adapter change is required.

`relationships.original`, `relationships.return`, and `relationships.replacementParent` contain only deliveryId, deliveryCode, status, and deliveryFlow. They are display/navigation context, never mutation authorization. Detail visibility also never authorizes a mutation; every scanner and action endpoint independently applies its existing stricter rules.

### Important errors

HTTP 400 `invalid_filter_type` or `invalid_delivery_identifier`; HTTP 401 `unauthorized` when authentication is missing or the authenticated mechanism is not JWT; HTTP 403 `unauthorized` for an invalid/expired credential from the shared middleware, or `insufficient_permissions` / `user_account_is_inactive`; HTTP 404 `delivery_not_found`; HTTP 500 `internal_server_error`. Every handler response is `private, no-store` and includes `X-Request-ID`.

### Frontend integration contract

add one mobile service adapter from this camelCase V1 object to the existing `DeliveryItem` shape. Use `quantity` for product/package count and `unitSize` only for capacity. Use this endpoint for driver detail reads only; warehouse detail screens stay on the generic endpoint. Do not infer action eligibility from successful detail access; keep using the existing scanner and action responses. Keep the old endpoint behind a temporary rollout flag until staging field-parity and latency comparison pass.

### G2G origin display

each delivery includes nullable `g2gOrigin` with `{ "branchName": string|null, "branchAddress": string|null }`. It is populated only for Transfer Manifest orders from a valid stored `manifest_origin_v1` snapshot, with the staged parent snapshot used when the child has no usable origin and its manifest ID is absent or matches the parent. Normal orders and older orders without usable saved origin return null. When `sourceTransferManifestId` is non-null, show a read-only Original G2G location line separately from pickup, using the available branch name and address; if `g2gOrigin` is null, show Origin unavailable. Hide the line only when the manifest ID is null. Keep `pickupLocation`, `deliveryLocation`, maps, navigation, route calculations, and driver actions unchanged. Origin metadata is returned by the existing batched list read or single detail read; do not request one detail per card.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver/deliveries/{{delivery_id}}?filter_type=id&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| filter_type | query string | Yes | Required lookup mode. Allowed values are exactly `id` and `delivery_code`; defaults to `id`. Barcode lookups must use the scanner resolver. | id |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | private, no-store |



```json
{
  "success": true,
  "message": "Delivery retrieved successfully",
  "delivery": {
    "deliveryId": "delivery-uuid",
    "deliveryCode": "3501",
    "barcode": "DEL3501",
    "externalReferenceId": null,
    "sourceTransferManifestId": "manifest-uuid",
    "status": "assigned",
    "returnStatus": null,
    "deliveryFlow": null,
    "pendingAcceptance": false,
    "assignment": {
      "id": "assignment-uuid",
      "status": "assigned",
      "sequence": 1,
      "routeSessionId": null,
      "routeDetails": null
    },
    "customer": {
      "id": "customer-uuid",
      "fullName": "Test Customer",
      "avatarUrl": null,
      "phone": null,
      "email": null
    },
    "pickupLocation": {
      "id": null,
      "title": {
        "en": "Processing point"
      },
      "fullAddress": "Pickup address",
      "latitude": 33.3,
      "longitude": 44.4,
      "city": null,
      "state": null,
      "country": null,
      "zoneName": "Pickup Zone",
      "zoneCode": "PZ",
      "contactInfo": null
    },
    "deliveryLocation": {
      "id": null,
      "title": {
        "en": "Destination branch"
      },
      "fullAddress": "Delivery address",
      "latitude": 33.4,
      "longitude": 44.5,
      "city": null,
      "state": null,
      "country": null,
      "zoneName": "Delivery Zone",
      "zoneCode": "DZ",
      "contactInfo": null
    },
    "pickupWarehouse": null,
    "dropoffWarehouse": null,
    "deliveryService": null,
    "priority": 1,
    "unitSize": 1,
    "quantity": 1,
    "notes": null,
    "scheduledPickupTime": null,
    "scheduledDeliveryTime": null,
    "paymentMethod": "wallet",
    "codAmount": 0,
    "payerType": "sender",
    "deliveryFee": 0,
    "feeToCollectAtPickup": 0,
    "feeToCollectAtDelivery": 0,
    "proofRequirements": {
      "pickup": {
        "photoRequired": false,
        "signatureRequired": false
      },
      "delivery": {
        "photoRequired": true,
        "signatureRequired": true
      }
    },
    "packageDetails": {
      "totalItems": 1,
      "totalQuantity": 1,
      "itemStatusSummary": {
        "pending": 1,
        "delivered": 0,
        "returned": 0,
        "isPartiallyDelivered": false,
        "isPartiallyReturned": false
      },
      "pickupStatusSummary": null,
      "items": []
    },
    "parcelCount": 1,
    "actionableParcelCount": 0,
    "packageOutcomeStage": "final_delivery",
    "packageOutcomeActionsAllowed": true,
    "exceptions": [],
    "relationships": {
      "original": null,
      "return": null,
      "replacementParent": null
    },
    "createdAt": "2026-08-30T00:00:00Z",
    "updatedAt": "2026-08-30T00:00:00Z",
    "g2gOrigin": {
      "branchName": "Original branch",
      "branchAddress": "Original branch address"
    }
  }
}
```

### Response 404 Error - Missing or Outside Driver Read Scope

Not Found

```json
{
  "success": false,
  "code": "delivery_not_found",
  "message": "Delivery not found"
}
```

## GET /delivery/driver/deliveries/scan/{{barcode}} — Resolve Driver Delivery Barcode V1

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-resolve-driver-delivery-barcode-v1)

Lightweight driver-only barcode resolver. A valid user Bearer JWT is required; API keys are not accepted. The shared Delivery Edge middleware is the only authentication owner and resolves the active user's trusted tenant and role context. The handler rejects non-JWT and non-driver actors, then one service-role-only RPC applies driver visibility and performs the indexed `(tenant_id, barcode)` lookup. Missing and unauthorized barcodes both return HTTP 404.

A normal successful scan returns the one actionable `deliveryId`. When an original or P&D barcode has no active forward leg because its outbound delivery failed, but it has an authorized active linked return, the same response also contains compact `delivery` and `return_delivery` records. `delivery` is the failed outbound with `is_active=false` and `next_action=null`. `return_delivery` is the linked return with its current `is_active` and driver `next_action`. Top-level `deliveryId` is the actionable return delivery ID. The active return prevents `no_actionable_delivery_leg`. Direct return-barcode scans and orders without active returns keep the normal response.

If more than one independently actionable authorized delivery exists, the endpoint returns HTTP 409 `ambiguous_delivery_barcode`. A recognized parent with no actionable child and no active linked return returns HTTP 409 `no_actionable_delivery_leg`. Important authentication errors are HTTP 401 `unauthorized` when authentication is missing or the authenticated mechanism is not JWT; HTTP 403 `unauthorized` for an invalid/expired credential from the shared middleware, or `insufficient_permissions` / `user_account_is_inactive`.

### Frontend integration contract

call this endpoint first. For a normal response, navigate using `deliveryId` and fetch detail only when the destination screen needs it. When `return_delivery` is present, show the existing active-return presentation, render `delivery` only as inactive context, and use top-level `deliveryId` for the actionable return navigation. Display localized messages for HTTP 409 conflicts. Do not use this route for the warehouse scanner. Keep a temporary feature flag to the legacy barcode detail until staging scanner parity passes.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver/deliveries/scan/{{barcode}}?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcode | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success - One Actionable Delivery

OK

```json
{
  "success": true,
  "message": "Delivery barcode resolved successfully",
  "scannedBarcode": "DEL3501",
  "deliveryId": "actionable-delivery-uuid"
}
```

### Response 200 Success - Failed Outbound With Active Return

OK

```json
{
  "success": true,
  "message": "Delivery barcode resolved successfully",
  "scannedBarcode": "DEL2863",
  "deliveryId": "actionable-return-delivery-uuid",
  "delivery": {
    "id": "failed-outbound-uuid",
    "delivery_code": "2863B",
    "barcode": "DEL2863B",
    "status": "failed",
    "delivery_flow": null,
    "is_active": false,
    "next_action": null
  },
  "return_delivery": {
    "id": "return-root-uuid",
    "delivery_code": "2865",
    "barcode": "DEL2865",
    "status": "pending",
    "delivery_flow": "operational_return",
    "is_active": true,
    "next_action": null
  }
}
```

### Response 409 Error - No Actionable Leg

Conflict

```json
{
  "success": false,
  "code": "no_actionable_delivery_leg",
  "message": "This barcode does not have a delivery that can be actioned"
}
```

## GET /delivery/driver-deliveries/status-summary — Get Driver Delivery Status Summary

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-get-driver-delivery-status-summary)

The Available count uses the same current-assignment rule as the Pending driver list. A Pending operational-return retry with only a previous returned assignment counts as Available when self-assignment, zone, team, and service eligibility pass.

Get driver delivery status summary with counts and labels (driver-only).

### Purpose
- Lightweight endpoint that returns only status counts and labels
- Useful for displaying status tabs/cards in the driver UI
- Does not include delivery list data
- Uses the same visibility rule as `GET /delivery/driver-deliveries`: children of active Waybills (`drafted`, `sealed`, `assigned`, `picked_up`, `in_transit`, `awaiting_receipt`) are excluded from all counts.

### Response includes
- `statuses`: Array of status objects with `status`, `label`, and `count` fields
- Status order: `all`, `pending` (label `Available`), `assigned`, `en_route_to_pickup`, `picked_up`, `in_transit`, `delivered`, `cancelled`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver-deliveries/status-summary?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Status summary retrieved successfully",
  "statuses": [
    {
      "status": "all",
      "label": "All",
      "count": 5
    },
    {
      "status": "pending",
      "label": "Available",
      "count": 5
    },
    {
      "status": "assigned",
      "label": "Assigned",
      "count": 2
    },
    {
      "status": "en_route_to_pickup",
      "label": "En Route To Pickup",
      "count": 1
    },
    {
      "status": "picked_up",
      "label": "Picked Up",
      "count": 0
    },
    {
      "status": "in_transit",
      "label": "In Transit",
      "count": 2
    },
    {
      "status": "delivered",
      "label": "Delivered",
      "count": 10
    },
    {
      "status": "cancelled",
      "label": "Cancelled",
      "count": 0
    }
  ]
}
```

### Response 403 Error - Not a Driver

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 404 Error - Driver Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not found"
}
```

## GET /delivery/deliveries/:deliveryId — Get Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-get-delivery)

Delivery detail includes `delivery.return_status` directly from the delivery read model. Its value is `null | return_approved | return_in_transit | return_completed`. For a linked operational return, the canonical original/P&D parent remains `status=staged_at_warehouse` while the return is Pending, Assigned, or En route to pickup; physical `picked_up` or `in_transit` changes it to `status=returning` and `return_status=return_in_transit`; return delivery completion changes it to `status=returned` and `return_status=return_completed`. The failed outbound leg remains Failed. This is stored database state, not response-only mapping. For tenant admin/dispatcher detail, canonical `order_events` records return lifecycle changes as `return_to_sender_approved`, `returning_to_sender`, and `returned_to_sender`. Each return event contains `timestamp`, optional actor/reason/notes`, `previous_return_status`, `new_return_status`, and metadata with `source=delivery_return_status`, `delivery_id`, and `delivery_code` plus available return-operation IDs. Operational P&D child custody changes are not projected as parent sender-return events. When a primary status changes to `returning` or `returned` together with its return status, the primary status event remains authoritative and a duplicate return-status event is not appended. Historical parents affected by the former hybrid RTS cancellation repair retain their original audit entries and receive one additive `failed_delivery_status_corrected` event. Its metadata has `source=hybrid_outbound_rts_parent_event_correction`, the corrected child and return IDs, and `parent_status_unchanged=true`; it corrects the audit meaning and does not represent parcel movement or a parent status transition. Canonical `order_events` also includes `warehouse_transfer_completed` as soon as a present/damaged member of a warehouse-to-warehouse Waybill is marked received. This is a custody/location event independent of delivery status: a staged P&D parent can remain `staged_at_warehouse` while the transfer event is appended. The transfer event contains the receipt `timestamp`, `actor_id`/`actor_name`/`actor_role`, a system-generated `notes` value (`Transfer received from {origin} at {destination}`), and metadata with `waybill_id`, `waybill_number`, `driver_id`, origin/destination warehouse IDs and names, `received_at`, and verification counts. Missing members and location-origin/destination Waybills do not add this event. A later Waybill close does not duplicate it. For tenant admin/dispatcher detail, `order_events` always belongs to the requested delivery row. A parent request returns the parent's aggregate history; a P&D child request returns only that inbound/outbound leg's status, driver/handoff, exception, and custody events. No separate `delivery_events` field is returned. The history is loaded by one primary-key query. Customer and driver `status_updates` remain unchanged.

### G2G origin display

detail includes nullable `source_transfer_manifest_id`, `source_transfer_manifest_item_id`, and `source_transfer_manifest_meta`, consistent with the delivery list. For a Transfer Manifest order whose metadata has `snapshot_version=manifest_origin_v1`, show the non-empty `source_branch_name` and `source_branch_address` as a separate read-only Original G2G location line. For a known manifest-linked order with no usable saved origin, show Origin unavailable. Hide the line only when `source_transfer_manifest_id` is null; display any genuine partial name or address that is available. The operational `pickup_location` and `delivery_location` remain unchanged; use those for maps, navigation, and routing. These origin fields come from the existing saved order snapshot; no branch lookup is required.

Get delivery details. Includes carrier fields (carrier_type, direction, pickup_warehouse_id, dropoff_warehouse_id). If staged/hybrid parent, also includes sequence_plan, children_summary, and full children; if prime, includes prime object.

### Internal event history (tenant admin/dispatcher)
- `delivery.order_events` belongs to the requested delivery row.
- For a P&D parent request, it contains the parent's complete aggregate journey. For a P&D inbound/outbound child request, it contains only that operational leg's status, driver/handoff, exception, and Waybill custody events.
- No separate `delivery_events` field is returned. The requested history is loaded with one bounded primary-key database query. Customer and driver tracking continues to use `status_updates`.

### Children
- `children_summary` is a compact operational-leg list with `id`, `type`, `direction`, `status`, and optional `prime`.
- `children` is included for staged/hybrid parent detail responses when child rows are available from `delivery_list_view`.
- Each child includes the normal delivery list view fields, including `delivery_flow` when present. Replacement-return children are excluded from this parent operational children list.

### Parent assignment projection
- For staged/P&D parent details, the existing `assignment` object is built from child delivery assignments instead of a parent assignment row.
- `assignment.source = "child_rollup"` identifies this response-only projection.
- `assignment.delivery_id`, `assignment.delivery_code`, `assignment.direction`, and `assignment.leg_sequence` identify the active/source child leg.
- `assignment.status_updates` is a merged child-assignment timeline. Each event includes `source = "child_assignment"`, `assignment_id`, `delivery_id`, `delivery_code`, `direction`, and `leg_sequence`.
- `driver` and `vehicle` follow the same source child leg as the projected assignment.
- Cancelled/returned child assignments remain visible for history/audit, so terminal staged parents still expose the source child driver instead of `Not Assigned`.
- Frontend integration: keep reading `delivery.driver`, `delivery.assignment.status`, and `delivery.assignment.status_updates`; no separate parent-specific field is required.

### Proofs
- The optional `proofs` array contains one row per submitted asset. Each row has `actor_type`, `actor_id`, `stage` (`pickup` or `delivery`), `proof_type` (`photo` or `signature`), and `storage_url`.
- `proof_of_pickup` and `proof_of_delivery` are delivery-level aggregate proof summaries when captured, shaped as `{ signature, photos }`.

### Exceptions
- `delivery.has_exception` and `delivery.exception` (with compatibility aliases `has_open_exception` and `open_exception`) expose the compact current-open-exception snapshot. During the warehouse decision stage the exception is stored directly on the P&D parent; once an outbound is assigned or moving, an exception is stored on that outbound and the existing parent projection remains compatible.
- While the parent exception is open, the nested outbound child response remains unchanged. Frontend derives its blocked display from the parent `has_exception` and `exception` fields. Starting or progressing that outbound returns HTTP 409 `parent_exception_open`.
- The top-level `exceptions` array contains the delivery's exception history.
- Each item includes the canonical `exception_type` and the user-facing `exception_label` from the tenant `exception_types` setting. Render `exception_label`; do not send it when creating or updating an exception.

### Replacement orders
- `delivery_flow` indicates if this is a `replacement_forward` or `replacement_return` (null for normal deliveries).
- If `delivery_flow = 'replacement_forward'` and the return delivery has been auto-created, response includes `replacement_delivery` with the full return delivery details and its current status.
- If `delivery_flow = 'replacement_return'`, response includes `replacement_delivery` with the forward delivery details.
- `replacement_delivery` is absent if not a replacement, or if the return has not yet been created (forward not yet completed).
- For the final outbound child of a warehouse-staged replacement parent, the detail response exposes effective `delivery_flow = "replacement_forward"` and `replacement_parent_delivery_id` with the parent id so driver UI can render the replacement stop while still acting on the child delivery id. Once created, its linked return is also exposed through replacement_delivery.

Actor ID contract for new driver-performed delivery and assignment status events: `actor_role=driver` means `actor_id` is the tenant-scoped `drivers.id`, resolvable with `GET /delivery/drivers/:driverId`. A dispatcher-performed event uses the dispatcher user ID. Historical order events may contain a driver user ID instead; resolve a legacy value with `GET /users/:userId` when it is not a driver ID. Existing event JSON is not rewritten.

### G2G origin display

detail includes nullable `source_transfer_manifest_id`, `source_transfer_manifest_item_id`, and `source_transfer_manifest_meta`, consistent with the delivery list. For a Transfer Manifest order whose metadata has `snapshot_version=manifest_origin_v1`, show the non-empty `source_branch_name` and `source_branch_address` as a separate read-only Original G2G location line. For a known manifest-linked order with no usable saved origin, show Origin unavailable. Hide the line only when `source_transfer_manifest_id` is null; display any genuine partial name or address that is available. The operational `pickup_location` and `delivery_location` remain unchanged; use those for maps, navigation, and routing. These origin fields come from the existing saved order snapshot; no branch lookup is required.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/deliveries/:deliveryId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveryId | path string | Yes | ID of the delivery to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success - Transfer manifest provenance

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "delivery": {
    "id": "delivery-uuid",
    "source_transfer_manifest_id": "manifest-uuid",
    "pickup_location": {
      "title": {
        "en": "Processing point"
      },
      "full_address": "100 Meter Road, Erbil"
    },
    "delivery_location": {
      "title": {
        "en": "Destination branch"
      },
      "full_address": "Main Street, Erbil"
    },
    "source_transfer_manifest_item_id": "manifest-item-uuid",
    "source_transfer_manifest_meta": {
      "snapshot_version": "manifest_origin_v1",
      "source_branch_name": "Original branch",
      "source_branch_address": "Original branch address"
    }
  }
}
```

### Response 200 Success - P&D child order events

OK

```json
{
  "success": true,
  "message": "Delivery retrieved successfully",
  "delivery": {
    "id": "outbound-child-uuid",
    "parent_delivery_id": "parent-order-uuid",
    "direction": "outbound",
    "order_events": [
      {
        "event_type": "assigned",
        "timestamp": "2026-08-27T09:30:00Z"
      },
      {
        "event_type": "driver_handoff_completed",
        "timestamp": "2026-08-27T10:00:00Z"
      }
    ]
  },
  "exceptions": []
}
```

### Response 200 Success - Shop-requested return context

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery retrieved successfully",
  "delivery": {
    "id": "original-delivery-uuid",
    "delivery_code": "DEL1001",
    "status": "delivered",
    "return_status": "return_approved",
    "return_delivery_code": "DEL1050"
  },
  "return_delivery": {
    "id": "return-delivery-uuid",
    "delivery_code": "DEL1050",
    "status": "pending",
    "is_active": true,
    "next_action": null
  },
  "return_request": {
    "id": "return-request-uuid",
    "status": "approved",
    "return_reason": "Customer requested return",
    "notes": null,
    "requested_at": "2026-08-21T10:00:00.000Z",
    "requested_by_user_id": "shop-user-uuid",
    "request_source": "customer_portal",
    "requested_by": {
      "id": "shop-user-uuid",
      "full_name": "Shop User"
    },
    "return_delivery_id": "return-delivery-uuid"
  },
  "exceptions": []
}
```

### Response 200 Success - Return delivery with original context

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery retrieved successfully",
  "delivery": {
    "id": "return-delivery-uuid",
    "delivery_code": "DEL1050",
    "status": "pending",
    "delivery_flow": "commercial_return",
    "original_delivery_id": "original-delivery-uuid"
  },
  "original_delivery": {
    "id": "original-delivery-uuid",
    "delivery_code": "DEL1001",
    "status": "delivered",
    "driver": {
      "id": "driver-uuid",
      "full_name": "Driver Name"
    }
  },
  "return_request": null,
  "exceptions": []
}
```

### Response 200 Success - Driver child with multipart parcels

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery retrieved successfully",
  "delivery": {
    "id": "outbound-child-uuid",
    "parent_delivery_id": "parent-order-uuid",
    "delivery_code": "1500B",
    "status": "assigned",
    "return_status": "return_approved",
    "quantity": 3,
    "parcelCount": 2,
    "parcels": [
      {
        "id": "parcel-1",
        "itemCode": "DEL1500-1",
        "name": "Box 1",
        "quantity": 1,
        "status": "pending"
      },
      {
        "id": "parcel-2",
        "itemCode": "DEL1500-2",
        "name": "Box 2",
        "quantity": 1,
        "status": "pending"
      }
    ]
  },
  "exceptions": []
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery retrieved successfully",
  "delivery": {
    "id": "uuid-parent",
    "tenant_id": "uuid-tenant",
    "external_reference_id": "ORD-12345",
    "carrier_type": "hybrid",
    "delivery_flow": null,
    "status": "exception",
    "sequence_plan": [
      {
        "seq": 1,
        "delivery_id": "uuid-child-inbound",
        "type": "internal",
        "direction": "inbound"
      },
      {
        "seq": 2,
        "delivery_id": "uuid-child-outbound",
        "type": "internal",
        "direction": "outbound"
      }
    ],
    "children_summary": [
      {
        "id": "uuid-child-inbound",
        "type": "internal",
        "direction": "inbound",
        "status": "delivered"
      },
      {
        "id": "uuid-child-outbound",
        "type": "internal",
        "direction": "outbound",
        "status": "exception"
      }
    ],
    "children": [
      {
        "id": "uuid-child-inbound",
        "parent_delivery_id": "uuid-parent",
        "carrier_type": "internal",
        "delivery_flow": null,
        "direction": "inbound",
        "status": "delivered"
      },
      {
        "id": "uuid-child-outbound",
        "parent_delivery_id": "uuid-parent",
        "carrier_type": "internal",
        "delivery_flow": null,
        "direction": "outbound",
        "status": "exception"
      }
    ],
    "has_exception": true,
    "exception": {
      "id": "uuid-exception",
      "exception_type": "vehicle_issue",
      "exception_label": "Vehicle Issue",
      "status": "open",
      "reported_at": "2026-07-24T10:30:00Z",
      "updated_at": "2026-07-24T10:30:00Z"
    },
    "created_at": "2026-07-24T09:00:00Z"
  },
  "exceptions": [
    {
      "id": "uuid-exception",
      "delivery_id": "uuid-child-outbound",
      "exception_type": "vehicle_issue",
      "exception_label": "Vehicle Issue",
      "status": "open",
      "exception_details": {
        "delivery_status_at_exception": "en_route_to_pickup",
        "parent_delivery_status_at_exception": "staged_at_warehouse"
      },
      "reported_at": "2026-07-24T10:30:00Z"
    }
  ]
}
```

### Response 200 Success - Location titles

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery retrieved successfully",
  "delivery": {
    "id": "delivery-uuid",
    "pickup_location": {
      "id": "pickup-address-uuid",
      "title": {
        "en": "Pickup address"
      },
      "full_address": "Pickup full address"
    },
    "delivery_location": {
      "id": "delivery-address-uuid",
      "title": {
        "en": "Delivery address"
      },
      "full_address": "Delivery full address"
    }
  },
  "exceptions": []
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## GET /delivery/deliveries/{{delivery_barcode}} — Scan Delivery Barcode With Linked Return

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-scan-delivery-barcode-with-linked-return)

Read-only mobile scan contract for the existing Get Delivery handler. The backend preserves an eligible active linked return before resolving a P&D original child and returns the scanned delivery and linked return as separate choices. Every displayed `delivery` and `return_delivery` has its own non-null navigation target with `{action,entity_id,reason}`. Driver targets use `action=open_delivery`; `entity_id` is the ID of that exact displayed delivery and the fallback reason is `delivery_details_available`. Linked summaries include `id`, `delivery_code`, `barcode`, `status`, `delivery_flow`, `is_active`, and the actor's operational `next_action`. If a P&D original has no active forward child but has an active linked return, the scan still succeeds with the original context and return choice. If no eligible active return exists, `return_delivery` and `return_target` are null and the normal scan flow is unchanged. A direct return-barcode scan keeps the return in `delivery`, uses `delivery_target`, and includes `original_delivery`. Mobile shows Delivery and Return choices when `return_delivery` is present; selecting a choice uses only that choice's delivery ID and corresponding target. Dismissing the choice does not navigate or process either order. No assignment, receipt, handoff, or status change occurs during this GET. The return remains a separate order with its own stored barcode and shipment label.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/deliveries/{{delivery_barcode}}?filter_type=barcode&tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_barcode | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| filter_type | query string |  |  | barcode |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 Original barcode resolves active return

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery retrieved successfully",
  "scanned_barcode": "DEL1001",
  "delivery": {
    "id": "original-delivery-uuid",
    "delivery_code": "1001",
    "barcode": "DEL1001",
    "status": "cancelled",
    "delivery_flow": "standard",
    "is_active": false,
    "next_action": null
  },
  "delivery_target": {
    "action": "open_delivery",
    "entity_id": "original-delivery-uuid",
    "reason": "delivery_details_available"
  },
  "return_delivery": {
    "id": "return-delivery-uuid",
    "delivery_code": "1050",
    "barcode": "DEL1050",
    "status": "pending",
    "delivery_flow": "operational_return",
    "is_active": true,
    "next_action": "en_route_to_pickup"
  },
  "return_target": {
    "action": "open_delivery",
    "entity_id": "return-delivery-uuid",
    "reason": "delivery_details_available"
  },
  "exceptions": []
}
```

## POST /delivery/deliveries — Create Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#post-create-delivery)

### Unit pricing contract

send both the user's `unit_size` input and current `package_details`. Fully measurable package details require weight, length, width, and height values above zero for every item, and the backend-calculated unit overrides `unit_size`. If any item has a zero, missing, or empty measurement, the package is treated as unmeasurable and the backend uses the submitted `unit_size`. Negative or non-numeric measurements remain invalid. If neither usable package measurements nor a submitted unit is present, creation uses `general.unit_size.settings.missing_unit_size_fallback`: service minimum, half of service maximum, service maximum, or HTTP 400 `unit_size_or_dimensions_required`. The calculated/selected unit is validated against the service `unit_min`/`unit_max` before routing, pricing, wallet handling, or persistence. An out-of-range unit returns HTTP 400 with code `can_not_use_this_delivery_for_this_unit_size`, a localized cause, and calculated/allowed unit fields. The success delivery includes `unit_size`. Frontend must preview the same payload through POST /delivery/price and show its returned unit and price. Payment lifecycle: zero fee is immediately paid; a positive wallet fee on a draft is deducted from usable wallet balance into `reserved`, without creating a wallet transaction or payment. Execute captures that hold and creates the delivery-fee transaction; cancelling the draft releases it. Non-draft sender wallet activation still charges immediately; receiver wallet remains pending until final completion; cash remains pending until its pickup/dropoff collection point.

Create a new delivery.

### Key rules
- `external_reference_id` is optional. When tenant setting `delivery_external_reference.unique_per_customer=true` (default), a non-empty value must be unique among root orders for the same tenant customer. Staged child legs reuse the root reference. A duplicate returns HTTP 400 `external_reference_id_already_exists`; when false, duplicates are accepted.
- Provide `pickup_location` and `delivery_location` as JSON objects with lat/long and country for non-warehouse endpoints.
- If `pickup_warehouse_id` is provided, the API resolves pickup from that warehouse address and ignores any submitted `pickup_location`/`pickup_address` JSON for pickup.
- If `dropoff_warehouse_id` is provided, the API resolves delivery from that warehouse address and ignores any submitted `delivery_location`/`delivery_address` JSON for delivery.
- If only address IDs are provided by legacy callers, the API resolves them once and embeds JSON.
- Contact info for labels/notifications is taken from the resolved address JSON `contact_info`.
- For a single package, place `weight`, `length`, `width`, `height`, and optional `quantity` directly in `package_details`. For multipart, use `package_details.items`; the backend rebuilds that field from normalized item rows.
- Optional `is_draft=true` creates the order with `status=draft`; it may be sent in the JSON body or as `?is_draft=true`. Missing or false keeps the existing `pending` behavior. A positive wallet fee is reserved from usable balance without creating a payment or wallet transaction. Execute captures the reservation and creates the delivery-fee transaction; cancelling the draft releases the hold. Draft orders do not queue auto-assignment and are not visible to driver workflows until executed.

### Warehouse staging
- If `requires_warehouse_staging=true`, the API creates a HYBRID parent with 2 legs (pickup→warehouse, warehouse→dropoff).
- `staging_warehouse_id` is optional; if null, system auto-selects.
- `package_details.items` is optional. When omitted or empty, backend creates one warehouse inventory item automatically with name `{delivery_code} - {customer_name}`, quantity 1, and uses `package_details.weight` when supplied.
- Staged P&D database persistence is atomic: the parent, A/B legs, package items, inventory items, receiving order, sequence plan, and PRIME extension rows either all commit or all roll back.
- Automatically allocated inventory item codes skip codes already claimed manually.
- Guard: staging is supported for delivery types "Pickup and Delivery" and "Delivery"; otherwise API returns `warehouse_staging_only_supported_for_pickup_and_delivery`.
- When `is_draft=true`, the parent and child legs are held in draft. Execute the parent with `target_delivery_status=pending` to release the parent and first leg.

### Frontend integration contract
- For warehouse toggles, send the matching `pickup_warehouse_id` or `dropoff_warehouse_id`; the backend will use the warehouse address even if the matching location JSON is present or contains null coordinates.
- For non-warehouse endpoints, continue sending location JSON with valid coordinates.
- For a single package, send direct dimensions in `package_details`; for multipart, send `package_details.items`.
- Frontend does not need to block submission when item rows are empty.
- If item rows are omitted, backend creates the receiving-order inventory item automatically; handle only existing HTTP errors such as invalid service, warehouse selection failure, wallet balance (`insufficient_available_balance`), missing payer wallet, or unsupported service type.
- Use optional checkbox value as `is_draft`; default it to false. The backend accepts either body `is_draft: true` or query `?is_draft=true`.
- For wallet draft rows, show the held fee from the wallet's aggregate `reserved` value; currency comes from tenant configuration. Show Execute and call `POST /delivery/deliveries/:deliveryId/status` with `{ "target_delivery_status": "pending" }`.

### Replacement orders
- Set `is_replacement=true` to create a replacement forward delivery.
- For non-staged replacements, the created delivery is marked `delivery_flow = 'replacement_forward'`.
- For warehouse-staged replacements, the HYBRID parent is marked `delivery_flow = 'replacement_forward'`; child legs remain normal operational legs.
- When the replacement forward reaches the customer, the system auto-creates a zero-fee return using the same `delivery_service_id`.
- For staged replacements, the return is created only after the final outbound child is completed; inbound/warehouse staging events do not create the return.
- If that service requires warehouse staging, the return creates a second HYBRID parent with customer→warehouse inbound and warehouse→original-pickup outbound legs. Otherwise it remains one direct return delivery.
- The return parent links to the replacement forward through `replacement_forward_delivery_id`; `parent_delivery_id` is reserved for HYBRID operational children.
- The driver is assigned to the return inbound child for staged returns, or the direct return for non-staged returns. Every return parent and leg has `delivery_fee = 0` because the replacement forward fee covers the round trip.
- No need to reference any original order — replacements are standalone.

### Required
- `delivery_service_id`, `customer_id`, `payer_type`, `payment_method`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/deliveries?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| requires_warehouse_staging | boolean |  |  |  |
| staging_warehouse_id | null |  |  |  |
| is_replacement | boolean |  |  |  |
| is_draft | boolean |  |  |  |
| pickup_location | object |  |  |  |
| pickup_location.full_address | string |  |  |  |
| pickup_location.lat | number |  |  |  |
| pickup_location.long | number |  |  |  |
| pickup_location.country | string |  |  |  |
| pickup_location.contact_info | object |  |  |  |
| pickup_location.contact_info.phone | string |  |  |  |
| pickup_location.contact_info.email | string |  |  |  |
| delivery_location | object |  |  |  |
| delivery_location.full_address | string |  |  |  |
| delivery_location.lat | number |  |  |  |
| delivery_location.long | number |  |  |  |
| delivery_location.country | string |  |  |  |
| delivery_location.contact_info | object |  |  |  |
| delivery_location.contact_info.phone | string |  |  |  |
| delivery_location.contact_info.name | string |  |  |  |
| pickup_warehouse_id | null |  |  |  |
| dropoff_warehouse_id | null |  |  |  |
| delivery_service_id | string |  |  |  |
| package_details | object |  |  |  |
| package_details.special_instructions | string |  |  |  |
| package_details.items | array<object> |  |  |  |
| package_details.items.name | string |  |  |  |
| package_details.items.description | string |  |  |  |
| package_details.items.quantity | number |  |  |  |
| package_details.items.weight | number |  |  |  |
| package_details.items.length | number |  |  |  |
| package_details.items.width | number |  |  |  |
| package_details.items.height | number |  |  |  |
| customer_id | string |  |  |  |
| external_reference_id | string |  |  |  |
| status | string |  |  |  |
| scheduled_pickup_time | string |  |  |  |
| scheduled_delivery_time | string |  |  |  |
| priority | number |  |  |  |
| notes | string |  |  |  |
| payer_type | string |  |  |  |
| payment_method | string |  |  |  |
| delivery_fee | number |  |  |  |



### Request body (raw)

```json
{
  "requires_warehouse_staging": false,
  "staging_warehouse_id": null,
  "is_replacement": false,
  "is_draft": false,
  "pickup_location": {
    "full_address": "Baghdad HQ",
    "lat": 33.3152,
    "long": 44.3661,
    "country": "IQ",
    "contact_info": {
      "phone": "+964700000000",
      "email": "sender@example.com"
    }
  },
  "delivery_location": {
    "full_address": "Basra Customer",
    "lat": 30.5081,
    "long": 47.7835,
    "country": "IQ",
    "contact_info": {
      "phone": "+964711111111",
      "name": "Customer Name"
    }
  },
  "pickup_warehouse_id": null,
  "dropoff_warehouse_id": null,
  "delivery_service_id": "uuid",
  "package_details": {
    "special_instructions": "Handle with care",
    "items": [
      {
        "name": "Mobile phone",
        "description": "Mobile phone",
        "quantity": 2,
        "weight": 2.5,
        "length": 30,
        "width": 20,
        "height": 15
      }
    ]
  },
  "customer_id": "uuid",
  "external_reference_id": "ORD-12345",
  "status": "pending",
  "scheduled_pickup_time": "2023-08-02T14:00:00Z",
  "scheduled_delivery_time": "2023-08-02T16:00:00Z",
  "priority": 1,
  "notes": "Customer prefers delivery after 3pm",
  "payer_type": "sender",
  "payment_method": "wallet",
  "delivery_fee": 3000
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery created successfully",
  "delivery": {
    "id": "uuid",
    "tenant_id": "uuid",
    "customer_id": "uuid",
    "delivery_service_id": "uuid",
    "pickup_location_id": "uuid",
    "delivery_location_id": "uuid",
    "status": "pending",
    "payment_status": "paid",
    "delivery_fee": 25.99,
    "payer_type": "sender",
    "payment_method": "wallet",
    "created_at": "2023-08-02T14:00:00Z"
  }
}
```

### Response 403 Error - Delivery Fee Override Not Allowed

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_fee_override_not_allowed",
  "field": "delivery_fee",
  "message": "Only dispatchers and tenant administrators can set the delivery fee"
}
```

### Response 400 Error - Invalid Delivery Fee Override

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_delivery_fee",
  "field": "delivery_fee",
  "message": "Delivery fee must be a finite non-negative number"
}
```

### Response 409 Error - Invalid Package Measurements

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid package details",
  "field": "package_details"
}
```

### Response 400 Error - Calculated Units Outside Service Range

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum"
}
```

### Response 400 Error - External Reference Already Exists

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "External reference ID already exists for this customer"
}
```

### Response 400 Error - Invalid Payment Method

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid payment method"
}
```

### Response 400 Error - Insufficient Wallet Balance

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

### Response 400 Error - Receiver User ID Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Receiver user ID required"
}
```

## POST /delivery/deliveries — Create Delivery (Carrier Routing)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#post-create-delivery-carrier-routing)

Create a delivery using the carrier-aware routing engine.

### Behavior
- `external_reference_id` follows `delivery_external_reference.unique_per_customer` (default `true`) for the same tenant customer at the root-order level. Hybrid child legs reuse the root reference.
- Provide `pickup_location` and `delivery_location` as JSON with lat/long/country for non-warehouse endpoints.
- If `pickup_warehouse_id` is provided, the pickup endpoint is resolved from that warehouse address and any submitted pickup JSON is ignored.
- If `dropoff_warehouse_id` is provided, the delivery endpoint is resolved from that warehouse address and any submitted delivery JSON is ignored.
- For a single package, place dimensions directly in `package_details`; for multipart, use `package_details.items`, which is rebuilt from normalized item rows.

### Warehouse staging
- If `requires_warehouse_staging=true`, the API creates a HYBRID parent with 2 legs.
- `staging_warehouse_id` is optional; if null, system auto-selects.
- `package_details.items` is optional. When omitted or empty, backend creates one warehouse inventory item automatically with name `{delivery_code} - {customer_name}`, quantity 1, and uses `package_details.weight` when supplied.
- Staged P&D database persistence is atomic: the parent, A/B legs, package items, inventory items, receiving order, sequence plan, and PRIME extension rows either all commit or all roll back.
- Automatically allocated inventory item codes skip codes already claimed manually.
- Guard: staging is supported for delivery types "Pickup and Delivery" and "Delivery"; otherwise API returns `warehouse_staging_only_supported_for_pickup_and_delivery`.

### Frontend integration contract
- For warehouse toggles, send the matching `pickup_warehouse_id` or `dropoff_warehouse_id`; the backend resolves coordinates from the warehouse address.
- For non-warehouse endpoints, continue sending location JSON with valid coordinates.
- For a single package, send direct dimensions in `package_details`; for multipart, send `package_details.items`.
- Frontend does not need to require at least one item row before submission.
- If item rows are omitted, backend creates the receiving-order inventory item automatically.

### Notes
- Pure PRIME eligibility requires carrier-enabled zones and at least one endpoint being a warehouse in a zone with configured warehouse.
- HYBRID creates parent + two children and sequence plan.
- Non-carrier flow unchanged when `carrier_routing` is false.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/deliveries?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| carrier_routing | boolean |  |  |  |
| requires_warehouse_staging | boolean |  |  |  |
| staging_warehouse_id | null |  |  |  |
| pickup_location | object |  |  |  |
| pickup_location.full_address | string |  |  |  |
| pickup_location.lat | number |  |  |  |
| pickup_location.long | number |  |  |  |
| pickup_location.country | string |  |  |  |
| delivery_location | object |  |  |  |
| delivery_location.full_address | string |  |  |  |
| delivery_location.lat | number |  |  |  |
| delivery_location.long | number |  |  |  |
| delivery_location.country | string |  |  |  |
| pickup_warehouse_id | null |  |  |  |
| dropoff_warehouse_id | null |  |  |  |
| delivery_service_id | string |  |  |  |
| customer_id | string |  |  |  |
| payer_type | string |  |  |  |
| payment_method | string |  |  |  |
| unit_size | number |  |  |  |
| notes | string |  |  |  |
| cod_amount | number |  |  |  |
| external_reference_id | string |  |  |  |
| scheduled_pickup_time | string |  |  |  |
| scheduled_delivery_time | string |  |  |  |
| receiver_user_id | string |  |  |  |
| priority | number |  |  |  |
| package_details | object |  |  |  |
| package_details.special_instructions | string |  |  |  |
| package_details.items | array<object> |  |  |  |
| package_details.items.name | string |  |  |  |
| package_details.items.description | string |  |  |  |
| package_details.items.quantity | number |  |  |  |
| package_details.items.weight | number |  |  |  |
| package_details.items.length | number |  |  |  |
| package_details.items.width | number |  |  |  |
| package_details.items.height | number |  |  |  |



### Request body (raw)

```json
{
  "carrier_routing": true,
  "requires_warehouse_staging": false,
  "staging_warehouse_id": null,
  "pickup_location": {
    "full_address": "Baghdad HQ",
    "lat": 33.3152,
    "long": 44.3661,
    "country": "IQ"
  },
  "delivery_location": {
    "full_address": "Basra Customer",
    "lat": 30.5081,
    "long": 47.7835,
    "country": "IQ"
  },
  "pickup_warehouse_id": null,
  "dropoff_warehouse_id": null,
  "delivery_service_id": "uuid",
  "customer_id": "uuid",
  "payer_type": "sender",
  "payment_method": "wallet",
  "unit_size": 1,
  "notes": "Optional notes",
  "cod_amount": 0,
  "external_reference_id": "ORD-12345",
  "scheduled_pickup_time": "2025-09-30T10:00:00Z",
  "scheduled_delivery_time": "2025-09-30T12:00:00Z",
  "receiver_user_id": "uuid",
  "priority": 1,
  "package_details": {
    "special_instructions": "Handle with care",
    "items": [
      {
        "name": "Mobile phone",
        "description": "Mobile phone",
        "quantity": 1,
        "weight": 2.5,
        "length": 30,
        "width": 20,
        "height": 15
      }
    ]
  }
}
```

### Response 201 Success - Internal Only

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery created successfully",
  "delivery": {
    "id": "uuid",
    "carrier_type": "internal",
    "status": "pending"
  }
}
```

### Response 201 Success - Pure Prime

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery created successfully",
  "delivery": {
    "id": "uuid",
    "carrier_type": "prime",
    "status": "pending"
  }
}
```

### Response 201 Success - Hybrid (Parent)

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery created successfully",
  "delivery": {
    "id": "uuid-parent",
    "carrier_type": "hybrid",
    "sequence_plan": [
      {
        "seq": 1,
        "delivery_id": "uuid-child-internal",
        "type": "internal",
        "direction": "inbound"
      },
      {
        "seq": 2,
        "delivery_id": "uuid-child-prime",
        "type": "prime",
        "direction": "outbound"
      }
    ]
  }
}
```

### Response 400 Error - Carrier Routing Insufficient Wallet Balance

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

### Response 400 Error - Prime Price Missing

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "prime_price_missing"
}
```

## GET /delivery/deliveries/:deliveryId/receipt — Get Delivery Receipt

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-get-delivery-receipt)

Returns the primary signed receipt URL for a delivery.

### Access
- tenant admin / dispatcher with delivery module access
- delivery owner customer

### Resolution priority
- COD_COLLECTION receipt
- DELIVERY_FEE receipt

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/deliveries/:deliveryId/receipt?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveryId | path string | Yes | ID of the delivery whose receipt should be retrieved | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "delivery_id": "uuid",
    "payment_purpose": "COD_COLLECTION",
    "receipt_status": "success",
    "receipt_generated_at": "2026-04-04T12:00:00.000Z",
    "expires_in": 3600,
    "url": "https://example.supabase.co/storage/v1/object/sign/documents/delivery-receipts/uuid.pdf?..."
  }
}
```

## PUT /delivery/deliveries/:deliveryId — Update Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#put-update-delivery)

### Unit pricing contract

fully measurable `package_details` override a simultaneously submitted `unit_size`; automatic fee recalculation runs when the resolved unit size changes or an endpoint actually changes. If any item has a zero, missing, or empty weight/length/width/height value, the package is treated as unmeasurable and the submitted unit is used. Negative or non-numeric measurements remain invalid. If neither measurable package details nor unit size is submitted, the existing saved unit is retained. A successful real edit appends one `order_edited` entry to the canonical delivery's `order_events`; `status_updates` remains status-only. When a promoted order's destination changes, its linked Transfer Manifest item is updated in the same database transaction so the Manifest Items table and order stay aligned.

Update the editable fields of a delivery order.

### Authorization
- Requires `manage:operations:tenant`, `manage:module:delivery`, or system-admin access.
- Customer ownership, delivery service, status, and internal `delivery_flow` are immutable.
- Use the public boolean `is_replacement` to enable or disable replacement handling.
- Status is not accepted here; use `POST /delivery/deliveries/:deliveryId/status`.

### Editable until final delivery
- `pickup_warehouse_id` or `dropoff_warehouse_id`
- `pickup_location_id` or `pickup_location`
- `delivery_location_id` or `delivery_location`
- `external_reference_id` (string or `null`; trimmed, and an empty string or `null` clears it)
- `cod_amount` (minimum 0)
- `payer_type` (`sender | receiver`)
- `payment_method` (`wallet | cash | cod`; `cod` is stored as `cash`)
- `delivery_fee` (minimum 0)
- `unit_size` (integer, minimum 1)
- `is_replacement` (boolean)
- `notes` (string or `null`; send an empty string or `null` to clear it)
- `package_details`, including `package_details.special_instructions` (string handling instructions) and replacement semantics for `items[]`; each item requires a name, a positive integer quantity, non-negative numeric dimensions, and a valid UUID when `id` is supplied

External references keep the tenant `delivery_external_reference.unique_per_customer` rule. When enabled (default), another root order for the same customer cannot use the same non-empty value; the API returns HTTP 400 with a localized duplicate-reference message and `field=external_reference_id`.

A non-null warehouse ID takes priority over the matching address ID/JSON, resolves the tenant warehouse address, and becomes the endpoint. Send the warehouse field as `null` with a replacement address to switch back to a normal address.

If an address, warehouse, or unit size changes, the backend validates the fixed delivery service and its zones, recalculates the fee, and uses an explicitly supplied `delivery_fee` as the override. Saved address IDs must belong to the order customer or tenant. Address JSON must contain valid coordinates and the tenant domestic country. A missing, cross-tenant, or address-less warehouse fails before the atomic update.

Unchanged endpoint submissions and contact-only location updates do not trigger automatic fee recalculation when the effective address and warehouse IDs and every normalized non-contact location field match the saved endpoint. Any extra submitted location field must also match its saved value. These saves preserve the saved non-contact snapshot, including omitted legacy fields, and still run the existing address resolution, coordinate/country validation, atomic assignment checks, and finance rules. Real endpoint or resolved unit-size changes still run service/zone pricing. An explicitly supplied `delivery_fee` remains supported under the existing rules. Transfer Manifest destination changes continue to run pricing.

### Order and P&D behavior
- A child delivery ID resolves to its logical P&D parent.
- Pickup address/warehouse changes propagate to the inbound leg; drop-off address/warehouse changes propagate to the outbound leg.
- COD stays on the parent/outbound leg. Delivery fee stays on the parent and moves to the sender-paid inbound leg or receiver-paid outbound leg.
- Unit size, payment terms, notes, special instructions, external reference, and package snapshot propagate to both operational legs.
- `is_replacement` updates the logical parent only; operational children read the parent's replacement state.
- An inbound child that is already delivered to the warehouse does not lock the order. The final parent/outbound `delivered` state locks every editable field.
- If an active assigned driver cannot serve either new zone, the whole update fails with HTTP 409 and nothing changes.
- If a larger unit size exceeds the assigned vehicle capacity, the whole update fails with HTTP 409.

### Replacement behavior
- `is_replacement: true` maps the logical order to `delivery_flow = replacement_forward`; `false` removes that classification.
- The toggle is editable until final delivery. Final completion and return creation recheck the same state atomically.
- Enabling replacement while the final payment method is cash remains subject to `allow_cod_on_replacement`; a forbidden change returns HTTP 400 `cod_not_allowed_for_replacement_orders`.
- Disabling replacement after its return exists returns HTTP 409 `replacement_return_already_exists`.
- A replacement return is a separate logical order and cannot be converted; submitting `is_replacement` on it returns HTTP 400 `replacement_return_type_not_editable`.
- A replacement forward follows normal direct/P&D update rules. Its latest addresses, package, unit size, and fixed service are used when the return is generated.
- Return addresses, package details, notes, special instructions, unit size, payer type, and payment method remain editable until the return is delivered. Direct and staged return assignments receive the same zone and vehicle-capacity validation as normal orders.
- Every replacement return remains zero-fee and zero-COD. Non-zero values return HTTP 400 `replacement_return_delivery_fee_must_be_zero` or `replacement_return_cod_must_be_zero`.

### Finance
- Draft wallet orders resize or move their reservation atomically as fee, payer, or method changes. No wallet transaction or payment is created until Execute; changing away from wallet releases the hold.
- Non-draft sender wallet: refund the full previously settled fee first, then charge the full updated fee. These are two visible wallet transaction lines when both amounts are non-zero.
- Pending sender cash to wallet: charge the full current delivery fee.
- Pending sender wallet to cash: refund the full settled wallet fee and leave the fee pending for pickup collection.
- Sender cash before pickup: leave the fee pending for pickup collection.
- Sender terms changed after pickup: refund any full previous wallet settlement, then charge the full corrected fee through the sender wallet while preserving historical physical-cash rows.
- Receiver cash/wallet: refund any settled sender fee and create no receiver payment during this update; the current fee becomes the future final-delivery obligation.
- COD updates only change the future goods-collection amount; no COD collection, payment, or wallet entry is created before final delivery.
- Successful delivery-fee payment rows are the settled-money source of truth; the delivery payment-status flag does not fabricate a wallet settlement.
- Refund, replacement charge, payment audit, and delivery changes commit or roll back together.
- Replacement-return payer or method changes move no money because its fee remains zero.
- `insufficient_available_balance` or insufficient wallet balance returns HTTP 409 and rolls back the entire update. A missing customer wallet returns HTTP 500.

### Atomicity and webhook
- Parent external reference, notes, special instructions, P&D legs, driver capacity, payment/wallet adjustments, package items, and replacement toggle commit or roll back together.
- A concurrent write returns HTTP 409 `delivery_update_conflict`.
- External-reference-only, notes-only, `package_details.special_instructions`-only, and replacement-only updates do not emit `order_update`; existing webhook tracking remains status, COD, price, payer side, and payment method.
- One request emits at most one logical `order_update` webhook when tracked fields change. Child synchronization does not emit duplicates.

### Order history (`order_events`)
- `status_updates` remains the delivery tracking/status timeline used by customer and driver experiences; edit events are never written there.
- The canonical/root delivery stores `order_events`, a complete internal history containing the same created/status events as its canonical `status_updates`, `order_edited` events, and projected operational-child driver activity (`driver_unassigned`, `driver_switched`, `driver_handoff_pending`, and `driver_handoff_completed`). Child driver events retain their original payload and add `delivery_id`, `delivery_code`, `direction`, `leg_sequence`, and `source=active_leg`. Existing canonical orders are backfilled from `status_updates`; future canonical status events are mirrored without changing their payload.
- Every successful edit appends at most one `order_edited` event inside the same transaction; a failed update or no-op save records nothing. A child inbound/outbound ID resolves to the logical parent, so P&D propagation never creates duplicate events.
- The event compares persisted before/after values, capturing normalized addresses and backend-recalculated fees. Shape: `{"event_type":"order_edited","timestamp":"2026-08-06T17:30:00Z","actor_id":"user-uuid","actor_name":"Dispatcher Name","actor_role":"dispatcher","changes":[{"field":"pickup_address","old_value":"Old Street","new_value":"New Street"},{"field":"delivery_fee","old_value":2500,"new_value":5000}],"metadata":{"source":"delivery_update_api"}}`.
- `changes[]` uses this stable field order: `pickup_address`, `delivery_address`, `delivery_fee`, `cod_amount`, `payer_type`, `payment_method`, `unit_size`, `package_details`, `special_instructions`, `notes`, `external_reference_id`, `is_replacement`. Address values are display-friendly text, not internal location JSON.
- The internal update response returns the canonical parent delivery with `delivery.order_events`. Existing `system_events` webhook behavior remains separate and unchanged.

### Frontend integration contract
- When a warehouse is selected, send its non-null `pickup_warehouse_id` or `dropoff_warehouse_id`; matching address input is ignored. To deselect it, send the warehouse field as `null` together with the replacement address.
- Send only fields the user actually changed and use `is_replacement` for the toggle; never send `delivery_flow`. Send special instructions only as `package_details.special_instructions`; top-level `special_instructions` is rejected.
- Use the returned canonical parent `delivery`; `delivery_flow = replacement_forward` means the toggle is on.
- Show the toggle until final delivery, except for `delivery_flow = replacement_return`, where it is read-only.
- For `delivery_flow = replacement_return`, keep `delivery_fee` and `cod_amount` read-only at zero.
- Handle HTTP 400 with `field=external_reference_id` as a duplicate external reference, plus `invalid_package_details`, `cod_not_allowed_for_replacement_orders`, and `replacement_return_type_not_editable`.
- Handle HTTP 409 `delivery_already_delivered`, `replacement_return_already_exists`, driver-zone/capacity conflicts, `insufficient_available_balance`, `insufficient_balance`, and `delivery_update_conflict`.
- On a conflict, keep the previous local order and reload before retrying.

### Conditional edit reason
- The tenant setting `delivery.delivery_order.settings.edit.reason_required` defaults to `false`.
- `reason` is accepted as an optional string, trimmed by the backend, and limited to 500 characters.
- When the setting is enabled, tenant-admin/dispatcher edits require a non-blank `reason`; missing or blank input returns HTTP 400 `delivery_edit_reason_required`.
- External customer edits remain unchanged.
- A successful real edit stores the normalized reason on the same canonical `order_edited` event. Failed and no-op updates do not create an event.

### Frontend integration contract
- Read `tenantSettings.delivery.delivery_order.settings.edit.reason_required` from Tenant Settings.
- When true, require Reason before enabling Save and send it as top-level `reason`.
- Handle HTTP 400 codes `delivery_edit_reason_required`, `delivery_edit_reason_must_be_text`, and `delivery_edit_reason_too_long`; display the localized `message`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/deliveries/:deliveryId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveryId | path string | Yes | ID of the delivery to update | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pickup_warehouse_id | string |  |  |  |
| pickup_location_id | string |  |  |  |
| dropoff_warehouse_id | null |  |  |  |
| delivery_location | object |  |  |  |
| delivery_location.full_address | string |  |  |  |
| delivery_location.latitude | number |  |  |  |
| delivery_location.longitude | number |  |  |  |
| delivery_location.city | string |  |  |  |
| delivery_location.country | string |  |  |  |
| external_reference_id | string |  |  |  |
| cod_amount | number |  |  |  |
| payer_type | string |  |  |  |
| payment_method | string |  |  |  |
| delivery_fee | number |  |  |  |
| unit_size | number |  |  |  |
| is_replacement | boolean |  |  |  |
| package_details | object |  |  |  |
| package_details.special_instructions | string |  |  |  |
| package_details.items | array<object> |  |  |  |
| package_details.items.id | string |  |  |  |
| package_details.items.name | string |  |  |  |
| package_details.items.description | string |  |  |  |
| package_details.items.quantity | number |  |  |  |
| package_details.items.weight | number |  |  |  |
| package_details.items.length | number |  |  |  |
| package_details.items.width | number |  |  |  |
| package_details.items.height | number |  |  |  |
| notes | string |  |  |  |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "pickup_warehouse_id": "pickup-warehouse-uuid",
  "pickup_location_id": "pickup-address-uuid",
  "dropoff_warehouse_id": null,
  "delivery_location": {
    "full_address": "Updated receiver address",
    "latitude": 36.1901,
    "longitude": 44.0091,
    "city": "Erbil",
    "country": "Iraq"
  },
  "external_reference_id": "CUSTOMER-ORDER-456",
  "cod_amount": 25000,
  "payer_type": "receiver",
  "payment_method": "cod",
  "delivery_fee": 7500,
  "unit_size": 3,
  "is_replacement": true,
  "package_details": {
    "special_instructions": "Keep upright and avoid heat",
    "items": [
      {
        "id": "package-item-uuid",
        "name": "Updated item name",
        "description": "Updated description",
        "quantity": 2,
        "weight": 3,
        "length": 35,
        "width": 25,
        "height": 20
      }
    ]
  },
  "notes": "Call the receiver before delivery",
  "reason": "Receiver requested an address correction"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery updated successfully",
  "delivery": {
    "id": "parent-delivery-uuid",
    "status": "in_transit",
    "delivery_flow": "replacement_forward",
    "external_reference_id": "CUSTOMER-ORDER-456",
    "pickup_warehouse_id": "pickup-warehouse-uuid",
    "dropoff_warehouse_id": null,
    "pickup_location_id": "pickup-address-uuid",
    "delivery_location": {
      "full_address": "Updated receiver address",
      "latitude": 36.1901,
      "longitude": 44.0091
    },
    "cod_amount": 25000,
    "payer_type": "receiver",
    "payment_method": "cash",
    "delivery_fee": 7500,
    "unit_size": 3,
    "package_details": {
      "items": [
        {
          "id": "package-item-uuid",
          "name": "Updated item name",
          "quantity": 2
        }
      ]
    },
    "updated_at": "2026-07-21T12:00:00Z",
    "notes": "Call the receiver before delivery"
  }
}
```

### Response 200 Success - Refunded Payment Status Preserved

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery updated successfully",
  "delivery": {
    "id": "parent-delivery-uuid",
    "payment_status": "refunded",
    "cod_amount": 25000
  }
}
```

### Response 400 Error - Invalid Update Fields

Bad Request

```json
{
  "success": false,
  "message": "Invalid delivery update fields: customer_id. Customer, delivery service, status, and internal delivery flow cannot be changed here",
  "field": "customer_id",
  "invalid_fields": [
    "customer_id"
  ]
}
```

### Response 400 Error - Duplicate External Reference

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "External reference ID already exists for this customer",
  "field": "external_reference_id"
}
```

### Response 409 Error - Delivered Order Locked

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivered orders cannot be changed"
}
```

### Response 409 Error - Returned Order Locked

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This delivery is complete and cannot be changed"
}
```

### Response 409 Error - Assigned Driver Not Eligible For New Zone

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "The assigned driver is not eligible for the updated pickup or drop-off zone"
}
```

### Response 409 Error - Concurrent Update

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "The order changed while it was being updated. Reload it and try again"
}
```

### Response 409 Error - Minimum Wallet Balance Reached

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 400 Error - Edit Reason Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_edit_reason_required",
  "field": "reason",
  "message": "A reason is required to edit this delivery order"
}
```

## POST /delivery/deliveries/:deliveryId/status — Update Delivery Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#post-update-delivery-status)

Update delivery status. Dispatchers retain all existing manual targets. The currently assigned driver may call this same endpoint only with `target_delivery_status=returned`, a nonblank `reason`, and optional `notes`. An open delivery exception or Waybill `missing_on_receive` discrepancy returns HTTP 409 `delivery_has_open_exception`; a missing receipt must be resolved as `found` or `lost` through the dedicated Waybill discrepancy endpoint. Direct deliveries finish Returned at their pickup. When a P&D inbound is returned to its pickup origin, the inbound leg and parent become Returned and every unfinished outbound leg becomes Cancelled because it never carried the parcel. Any active outbound assignment is cancelled, its capacity is released, and its driver receives the normal cancellation notification. P&D outbound and operational-return deliveries create a warehouse receiving handoff and return `return_handoff`; they are received through the existing receiving endpoint. A P&D outbound returned without an exception becomes `returned` and cannot be assigned until normal warehouse receipt changes it to `pending`; its parent becomes `arrived_at_warehouse`. That driver return appends the next `delivery_attempt_failed` event using the submitted `reason`; operational returns are not counted as customer-delivery attempts. If the direct delivery or any child of a P&D parent has `return_status=return_approved|return_in_transit`, a Delivered attempt returns HTTP 409 with `code=return_required` and `error=return_required`. When a customer delivery becomes `delivered`, every linked parcel leaves warehouse stock in the same transaction: positive stock becomes zero/depleted, each removal records a `pick` movement, and parcel custody becomes `customer`. Warehouse-dropoff deliveries and non-delivered parcels are unchanged.

Update delivery status manually from the dispatcher/admin panel.

### Allowed target statuses

`draft`, `pending`, `assigned`, `en_route_to_pickup`, `picked_up`, `in_transit`, `delivered`, `staged_at_warehouse`, `cancelled`, and `returned`.

### Request fields
- `target_delivery_status`: the status selected by the operator. `target_delivery_status=assigned` respects `require_accept_or_reject_for_assignments`: when enabled, the active assignment and delivery remain `pending` until the driver accepts, and the success response includes `driver_acceptance_required=true`. Later operational targets (`en_route_to_pickup`, `picked_up`, and `in_transit`) deliberately bypass acceptance and proceed immediately. `delivered` is accepted only when the direct delivery, or the resolved active child of a staged/P&D parent, is currently `picked_up` or `in_transit`.
- `reason`: optional when a dispatcher sends `target_delivery_status=pending`. It is required and nonblank for every other dispatcher/admin target and when the assigned driver sends `target_delivery_status=returned`. Missing or blank values return HTTP 400 `reason_required` with `field=reason`. Other driver-action endpoints, automatic transitions, warehouse receiving, Waybills, and external draft confirmation keep their existing contracts.

### Draft wallet behavior
- `draft` to `pending` captures the reserved fee and creates the single delivery-fee wallet transaction/payment in the same database transaction as the status change.
- Cancelling or returning an unpaid draft releases the reserved fee to usable wallet balance without creating a refund transaction.
- A positive-fee wallet delivery already paid cannot be moved back to draft; HTTP 409 returns `paid_wallet_delivery_cannot_return_to_draft`.
- Reservation conflicts return localized `insufficient_available_balance`, `payer_wallet_not_found`, or reservation consistency errors. Currency is not part of the reservation response.

### Terminal status behavior
- `cancelled` and `returned` resolve a staged parent or operational child ID to the same logical delivery family.
- Both statuses use the original service effective payment policy. `refund_delivery_fee_on_cancel` controls cancellation and `refund_delivery_fee_on_return` controls return; defaults are `true` and `false` respectively. When allowed, the collected fee is credited to the sender wallet once and matching fee payments become `REFUNDED`. When disallowed, the settled fee is retained and remains paid. Collected COD keeps its existing independent terminal credit behavior.
- `cancelled` writes every active assignment to `cancelled`, releases driver capacity, removes realtime assignment cards, and blocks every later driver action. No return handoff is created.
- `returned` writes the parent plus every non-final operational leg and active assignment to `returned`, releases driver capacity, removes realtime assignment cards, and closes unfinished receiving, picking, and package-handoff work. Already `delivered`, `cancelled`, or `returned` child legs are preserved as final history; for example, a delivered P&D inbound leg is not rewritten when the parent/outbound leg is returned.
- Cancelled and returned deliveries are history-only for drivers and are available only through `GET /delivery/driver-deliveries?status=cancelled` and `status=returned` respectively.
- A successful cancelled or returned response includes `reversal` with fee/COD amounts, refund flags, wallet transaction IDs, and the sender user ID.

### Manual status behavior
- A successful response normally guarantees `delivery.status` equals `target_delivery_status`, except an `assigned` request that requires driver acceptance returns `delivery.status=pending` with `driver_acceptance_required=true`. For a staged/P&D parent whose active child is outbound, manual `assigned`, `en_route_to_pickup`, and `picked_up` advance only that child; the returned parent `delivery.status` keeps its current warehouse lifecycle status (normally `staged_at_warehouse`).
- For a staged/P&D parent, manual `delivered` first resolves the active child and requires that child to be `picked_up` or `in_transit`. Once eligible, it completes every unfinished child in `sequence_plan` order before finalizing the parent. Children already delivered remain unchanged; cancelled or returned children fail with HTTP 409 `staged_child_not_completable`.
- Each child keeps its existing active assignment. When a child has no active assignment, the backend uses request `driver_id`; when it is omitted, it reuses the first available driver from the ordered child assignments. If neither is available, the request fails with HTTP 400 `driver_id_required_for_delivery_status`.
- Missing child assignments reuse the existing driver, zone, team, vehicle, and capacity validation. A draft future child can be assigned only through this protected dispatcher completion path and is completed immediately.
- Every unfinished child follows the existing proof, payment, COD, earning, notification, package, capacity, and replacement workflow. Request proof and returned-item outcomes apply to the final unfinished child.
- Child-to-parent warehouse projection is suppressed only for tagged dispatcher completion events. The parent is finalized with the last unfinished child, so skipped warehouse states are not recorded as the selected parent result. A retry also repairs an unfinished child when the parent is already delivered.
- For other assignment-required staged targets (`assigned`, `en_route_to_pickup`, `picked_up`, or `in_transit`), the backend continues resolving only the first active non-terminal child. Outbound `assigned`, `en_route_to_pickup`, and `picked_up` are child-only preparation states; outbound `in_transit` advances both child and parent.
- When a manual status changes both parent and child, both Events include the dispatcher actor and reason and metadata identifies `source=dispatcher_manual_status_override`. For outbound child-only preparation states, only the child status event is written; no parent status event or manual parent override is created.

### Warehouse staging action
- `target_delivery_status=staged_at_warehouse` remains the dedicated P&D warehouse-receive operation. When the inbound child lacks an active assignment, request `driver_id` is used first. If it is omitted, a parent created from a Transfer Manifest (both `source_transfer_manifest_id` and `source_transfer_manifest_item_id`) may use `assignment_change_settings.default_driver_id` only when `transfer_manifest.allow_warehouse_staging_without_assigned_driver=true`. Toggle Off, a missing fallback, or a normal/non-G2G order returns `driver_id_required_for_unassigned_delivery`. Deleted or cross-tenant fallback drivers return `driver_not_found`; inactive or unavailable drivers return `driver_not_active`; zone mismatches keep the existing zone errors. Optional `bin_id` must be active and belong to the inbound warehouse; otherwise the default receiving location is used. Assignment creation, inbound completion, inventory receipt, and parent staging remain atomic and idempotent.

### Other business rules
- Dispatcher/admin targets require `manage:operations:tenant` or `manage:module:delivery`. The currently assigned driver is also authorized, but only for `target_delivery_status=returned`; any other target returns HTTP 403 `insufficient_permissions`.
- For non-Delivered assignment-required targets with no active assignment, `driver_id` is used first, then `assignment_change_settings.default_driver_id`; otherwise the API returns `driver_id_required_for_delivery_status`.
- `proof_of_delivery` remains optional for dispatcher Delivered.
- A new Delivered request from `draft`, `pending`, `assigned`, `en_route_to_pickup`, `arrived_at_warehouse`, `staged_at_warehouse`, or another status outside `picked_up` and `in_transit` returns HTTP 409 with `code=invalid_status_transition` and the localized plain-language `message` `Order status does not allow this action` in English. Eligibility is checked before driver assignment, capacity, payment, audit, or completion side effects.
- Delivered keeps the existing idempotent COD/payment and driver-earning behavior for every eligible child completed by the request.
- Open delivery exceptions, active Waybills, driver-zone rules, and non-internal-delivery guards remain unchanged.

### Frontend integration contract
- Driver app: call this endpoint only for whole-delivery Return with `{ "target_delivery_status": "returned", "reason": "...", "notes": "..." }`; do not send parcel IDs or `returned_items`.
- Dispatcher panel: when the selected target is `pending`, Reason may be omitted or blank. For every other target, require and trim Reason and do not enable Save when it is blank.
- Send the visible parent delivery ID for staged orders. For manual Delivered, send the operator-selected `driver_id` when available; the backend uses it only after the delivery passes the `picked_up`/`in_transit` eligibility check and only for child legs missing an active assignment.
- Enable or show Delivered only when the direct delivery or resolved active P&D child is `picked_up` or `in_transit`; backend validation remains authoritative.
- Handle HTTP 400 `reason_required` and `driver_id_required_for_delivery_status`, plus HTTP 409 `invalid_status_transition`, `insufficient_available_balance`, `paid_wallet_delivery_cannot_return_to_draft`, `staged_child_not_completable` and existing driver eligibility/capacity errors. For `invalid_status_transition`, display the returned localized `message` and keep the current order status.
- On success, replace the local parent order with `delivery`. When a staged parent's active child is outbound and the target is `assigned`, `en_route_to_pickup`, or `picked_up`, keep the returned parent warehouse status (normally `staged_at_warehouse`) instead of displaying the child target on the parent. For `target_delivery_status=assigned`, keep the returned status when `driver_acceptance_required=true`; do not display the child as Assigned until the driver accepts. Outbound `in_transit` and `delivered` advance the parent normally. On terminal success, use `reversal` for audit/display. No navigation action is returned.

### P&D Pickup

For `target_delivery_status=picked_up` on a lifecycle-managed P&D/operational-return child, Pickup is one atomic custody boundary. Existing completed picking, current assignment/handoff, clear delivery and warehouse-item exceptions, and reserved warehouse stock are required. Success records one `pick` movement, removes stock/reservation, marks item custody `in_transit`, completes the handoff, marks the picking order `handed_off`, and advances delivery/assignment together. Missing preparation is not created here. Waybill and ordinary pickup behavior is unchanged. Frontend sends the existing status payload and must not separately complete the P&D handoff or mark its picking order handed off.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/deliveries/:deliveryId/status?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveryId | path string | Yes | ID of the delivery to update status for | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| target_delivery_status | string |  |  |  |
| reason | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "target_delivery_status": "returned",
  "reason": "Dispatcher instructed return",
  "notes": "Parcel handed back at the active leg origin"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery status updated successfully",
  "delivery": {
    "id": "uuid",
    "status": "delivered",
    "delivery_code": 1533
  },
  "driver_acceptance_considered": true,
  "driver_acceptance_message": "Driver acceptance was considered automatically for this status update"
}
```

### Response 200 Success - P&D Outbound Prepared

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery status updated successfully",
  "delivery": {
    "id": "outbound-delivery-uuid",
    "status": "pending",
    "delivery_code": 1533
  }
}
```

### Response 200 Success - P&D Outbound Returned to Warehouse

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery status updated successfully",
  "delivery": {
    "id": "outbound-delivery-uuid",
    "status": "returned",
    "delivery_code": 1533
  },
  "return_handoff": {
    "mode": "warehouse",
    "custody_state": "awaiting_warehouse_receipt",
    "receiving_order_id": "receiving-order-uuid",
    "warehouse_id": "warehouse-uuid",
    "parent_delivery_id": "pnd-parent-uuid",
    "parent_status": "arrived_at_warehouse",
    "return_status": null,
    "idempotent_replay": false
  }
}
```

### Response 400 Error - Reason Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "reason_required",
  "field": "reason",
  "message": "Reason is required"
}
```

### Response 409 Error - Delivered Requires In Transit

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_status_transition",
  "error": "invalid_status_transition",
  "message": "Order status does not allow this action"
}
```

### Response 409 Error - Approved Return Must Be Completed

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "return_required",
  "error": "return_required",
  "message": "This parcel must be returned and can no longer be marked delivered"
}
```

### Response 409 Error - Return Before Pickup (Use Cancel)

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_not_picked_up_for_return",
  "message": "This order has not been picked up yet. Cancel it instead."
}
```

### Response 409 Error - Open Delivery Exception or Missing Receipt

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_has_open_exception",
  "error": "delivery_has_open_exception",
  "message": "Delivery has an open exception. Resolve it before moving the delivery forward.",
  "exception": {
    "id": "exception-or-discrepancy-uuid",
    "exception_type": "missing_on_receive"
  }
}
```

### Response 409 Error - Active Waybill

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery is part of an active waybill. Use waybill actions instead"
}
```

### Response 500 Wallet Error Contract - Draft Execution

Internal Server Error

```json
{
  "success": false,
  "code": "insufficient_available_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

## POST /delivery/deliveries/:deliveryId/cancel — Cancel Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#post-cancel-delivery)

Cancel a delivery order. This endpoint:
- Updates the logical delivery order status to `cancelled`
- Reverses delivery financials to the sender wallet
- Cancels assignments that do not hold the parcel
- Preserves picked-up driver/assignment history for audit
- Sends notifications to the customer and any affected driver

### Business Rules
- Allowed: tenant admin (`manage:operations:tenant`), delivery module manager (`manage:module:delivery`), or order owner (`customer_id = authenticated user`)
- A delivered, returned, failed, rejected, or already-cancelled logical order cannot be cancelled; final outbound delivery also blocks cancellation with HTTP 409 `cannot_cancel_delivery_in_terminal_status`
- Merchant/customer cancellation is transactionally blocked after any logical-order assignment becomes active with HTTP 409 `cannot_cancel_delivery_after_driver_assignment`
- Tenant admin/dispatcher can cancel assigned, picked-up, or in-transit deliveries
- Assignments not in `picked_up` or `en_route_to_delivery` become `cancelled` and release capacity immediately
- A `picked_up` or `en_route_to_delivery` assignment keeps its custody status, driver, and capacity for audit; the delivery and every leg are still cancelled and all normal actions are blocked
- Cancelled and returned deliveries are hidden from the default driver task list and are returned only from explicit `status=cancelled` or `status=returned` filters
- Cancellation credits the collected delivery fee to the sender wallet only when the original service effective `refund_delivery_fee_on_cancel` policy allows it; the default is `true`
- Successful `DELIVERY_FEE` payments are resolved across the logical parent and operational children. The first policy decision is persisted per logical root so retries cannot change the result after a policy edit
- Collected COD is resolved across the same logical family and refunded using `cod_collections.allocations.cod`; driver cash liability/remittance state is not voided or reversed
- Matching family `DELIVERY_FEE` payments are updated to `REFUNDED` only when the cancel policy allows a fee refund. A no-refund fee remains successful and paid. COD remains independent, and a COD-only credit never marks the delivery fee as refunded
- A wallet-paid draft has no collected delivery fee yet: cancellation releases its reservation to usable balance without a refund wallet transaction. Other unpaid and uncollected amounts are not credited, and an unpaid cancellation does not require a sender wallet
- Replacement-return orders are separate logical orders and are never included as staged children
- **Staged deliveries:** submitting the parent or any operational child resolves to the same logical parent. A completed inbound warehouse leg does not block cancellation while the parent/final outbound leg remains cancellable. The transaction cancels:
  - The parent plus every non-terminal operational child; terminal child history is preserved
  - Receiving order (`receiving_orders` with `external_reference = 'DEL' || delivery_code`, status not in `received`, `partially_received`, `received_with_exceptions`, `cancelled`)
  - Package handoffs (`package_handoffs` with `delivery_id` matching an operational child, status not in `completed`, `cancelled`)
  - Picking orders linked to those handoffs (status not in `handed_off`, `cancelled`)

### Driver notification

The assigned/custody driver is captured before cancellation changes assignment data and receives one `delivery_cancelled` notification per affected operational delivery. It uses `priority=critical`, `display_mode=modal`, `requires_acknowledgement=true`, and `portal_context=driver_app`. In-app availability remains `status=sent` even when push fails; the push result is recorded in `channel_status.push`. Notification insertion, rendering, and push delivery run outside this API response path.

### Success contract
- `delivery.id` is the canonical logical root ID, including when the request used a staged child ID
- `reversal.delivery_fee_amount` is the actual successful family fee refunded

### Frontend integration contract
- Request body is optional; both `failure_reason` and `notes` are optional.
- Handle HTTP 409 `cannot_cancel_delivery_after_driver_assignment` and `cannot_cancel_delivery_in_terminal_status`.
- Admin/dispatcher does not need to unassign first.
- Use returned `delivery.id` as the canonical order ID.
- For staged deliveries, receiving, handoff, and eligible picking work is cancelled atomically.
- Cancelled/returned parent and child rows keep the historical assigned driver in `driver` and `assignment` for dispatcher/admin list and detail display.
- The driver app retrieves the persistent cancellation through the messaging notification API and acknowledges it only from the explicit “I understand” action.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/deliveries/:deliveryId/cancel?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveryId | path string | Yes | ID of the delivery to cancel | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| failure_reason | string | No | (string): stored in `delivery_failure_reason` and the status timeline |  |
| notes | string | No | (string): stored in the status timeline |  |



### Request body (raw)

```json
{
  "failure_reason": "Customer changed mind",
  "notes": "Order cancelled per customer request"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery cancelled successfully",
  "delivery": {
    "id": "uuid",
    "status": "cancelled"
  },
  "reversal": {
    "delivery_fee_refunded": true,
    "cod_refunded": true,
    "delivery_fee_amount": 5000,
    "cod_amount": 25000,
    "delivery_fee_wallet_transaction_id": "wallet-transaction-uuid",
    "cod_wallet_transaction_id": "wallet-transaction-uuid",
    "refund_recipient": "sender",
    "sender_user_id": "sender-user-uuid"
  }
}
```

### Response 404 Error - Delivery Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "delivery_not_found"
}
```

### Response 400 Error - Cannot Cancel Terminal Status

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "cannot_cancel_delivery_in_terminal_status"
}
```

### Response 409 Error - Customer Cannot Cancel Assigned Delivery

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "cannot_cancel_delivery_after_driver_assignment"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "insufficient_permissions"
}
```

## DELETE /delivery/deliveries/:deliveryId — Delete Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#delete-delete-delivery)

Legacy admin cancellation endpoint. The delivery record is retained; this endpoint now delegates to the same atomic logical-order cancellation flow as `POST /delivery/deliveries/{deliveryId}/cancel` instead of updating status directly. The parent or an operational child identifier resolves to the canonical root. Parent and eligible children are cancelled together and active work is closed. Collected delivery fees follow the original service cancel-refund policy; collected COD keeps its existing independent terminal credit behavior. Separate replacement-return orders are excluded. Tenant-admin permission is required.

### Responses

`200` success, `403` unauthorized, `404` delivery not found, `409` terminal logical-order conflict. Tenant admins may cancel an assigned order through this endpoint.

### Frontend integration contract

treat this as cancellation, not record deletion. Remove no record solely because the request succeeded; use the normal cancelled-order presentation. Prefer the explicit POST cancellation endpoint for new integrations.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/deliveries/:deliveryId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveryId | path string | Yes | ID of the delivery to delete | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery deleted successfully"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## POST /delivery/deliveries/bulk — Create Bulk Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#post-create-bulk-deliveries)

Create multiple deliveries in bulk. Only admin users can create bulk deliveries. All deliveries in the batch must have the same bulk_group value. Each row uses measurable item data or its submitted `unit_size`; otherwise the effective `general.unit_size.settings.missing_unit_size_fallback` is applied. The setting is loaded once for the batch.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/deliveries/bulk?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pickup_location | object |  |  |  |
| pickup_location.full_address | string |  |  |  |
| pickup_location.lat | number |  |  |  |
| pickup_location.long | number |  |  |  |
| pickup_location.country | string |  |  |  |
| delivery_location | object |  |  |  |
| delivery_location.full_address | string |  |  |  |
| delivery_location.lat | number |  |  |  |
| delivery_location.long | number |  |  |  |
| delivery_location.country | string |  |  |  |
| package_details | object |  |  |  |
| package_details.special_instructions | string |  |  |  |
| package_details.items | array<object> |  |  |  |
| package_details.items.name | string |  |  |  |
| package_details.items.description | string |  |  |  |
| package_details.items.quantity | number |  |  |  |
| package_details.items.weight | number |  |  |  |
| package_details.items.length | number |  |  |  |
| package_details.items.width | number |  |  |  |
| package_details.items.height | number |  |  |  |
| customer_id | string |  |  |  |
| status | string |  |  |  |
| scheduled_pickup_time | string |  |  |  |
| scheduled_delivery_time | string |  |  |  |
| priority | number |  |  |  |
| bulk_group | string |  |  |  |



### Request body (raw)

```json
[
  {
    "pickup_location": {
      "full_address": "Baghdad HQ",
      "lat": 33.3152,
      "long": 44.3661,
      "country": "IQ"
    },
    "delivery_location": {
      "full_address": "Basra Customer",
      "lat": 30.5081,
      "long": 47.7835,
      "country": "IQ"
    },
    "package_details": {
      "special_instructions": "Handle with care",
      "items": [
        {
          "name": "Book",
          "description": "Book",
          "quantity": 1,
          "weight": 1.5,
          "length": 20,
          "width": 15,
          "height": 10
        }
      ]
    },
    "customer_id": "uuid",
    "status": "pending",
    "scheduled_pickup_time": "2023-08-02T10:00:00Z",
    "scheduled_delivery_time": "2023-08-02T14:00:00Z",
    "priority": 1,
    "bulk_group": "batch-2023-08-01"
  },
  {
    "pickup_location": {
      "full_address": "Baghdad HQ 2",
      "lat": 33.3152,
      "long": 44.3661,
      "country": "IQ"
    },
    "delivery_location": {
      "full_address": "Basra Customer 2",
      "lat": 30.5081,
      "long": 47.7835,
      "country": "IQ"
    },
    "package_details": {
      "special_instructions": "Handle with care",
      "items": [
        {
          "name": "Electronics",
          "description": "Electronics",
          "quantity": 2,
          "weight": 2.5,
          "length": 30,
          "width": 20,
          "height": 15
        }
      ]
    },
    "customer_id": "uuid",
    "status": "pending",
    "scheduled_pickup_time": "2023-08-02T11:00:00Z",
    "scheduled_delivery_time": "2023-08-02T15:00:00Z",
    "priority": 1,
    "bulk_group": "batch-2023-08-01"
  }
]
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk deliveries created successfully",
  "bulk_group_id": "uuid",
  "delivery_ids": [
    "uuid-1",
    "uuid-2"
  ],
  "tracking_numbers": [
    "TRK123456",
    "TRK789012"
  ]
}
```

### Response 400 Error - Invalid Request Body

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Request body must be an array of delivery objects"
}
```

### Response 400 Error - Inconsistent Bulk Group

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Bulk group text must be same for all deliveries"
}
```

### Response 500 Wallet Error Contract - Legacy Bulk Creation

Internal Server Error

```json
{
  "success": false,
  "code": "insufficient_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

## POST /delivery/action/:deliveryId — Handle Delivery Action

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#post-handle-delivery-action)

Handle ordinary forward driver transitions, parcel pickup confirmation, and existing partial-package outcomes. For pickup, send `assignment_status=en_route_to_pickup` and `not_received_item_ids`; an empty array records a full pickup. The inbound child records leg outcomes while accepted parcel parent status remains `pending`; partial package returns never refund, and an all-package return uses the existing whole-delivery Return authority. Conflicting retries return `package_item_outcome_conflict`. An open delivery exception or Waybill `missing_on_receive` discrepancy returns HTTP 409 `delivery_has_open_exception`. A missing receipt remains blocked until destination staff resolve it as `found` or `lost` through the dedicated Waybill discrepancy endpoint. After Retry, this endpoint works normally. After Return, the delivery card has `returnStatus=return_approved`; this endpoint returns HTTP 409 `return_required` and cannot mark the parcel Delivered. The driver must use `POST /delivery/deliveries/:deliveryId/status` with `target_delivery_status=returned`. For a returned P&D inbound, the backend returns the inbound and parent and cancels the unused outbound. This endpoint does not query resolved exceptions. A successful customer-delivery completion removes every linked parcel from warehouse stock and records the movement in the same transaction; no additional frontend stock mutation is required.

Handle delivery status transitions by drivers. Only drivers can perform actions on their assigned deliveries.

### Terminal guards
- A returned delivery, or any leg whose logical parent is returned, rejects every driver transition with HTTP 409 `error=delivery_terminal_no_actions` and a localized plain-language `message`.
- A cancelled delivery, or any leg whose logical parent is cancelled, rejects every driver transition with HTTP 409 `error=cannot_perform_action_on_cancelled_delivery`. No return-handoff action is available.
- A driver switch transfers operational custody to the replacement driver immediately, leaves `handoff_status` null, and does not block the replacement driver's next status action.
- These rules are enforced in the database transaction, so stale screens cannot bypass terminal or custody changes.

### Supported Assignment Statuses
- `assigned` / `accepted` → `en_route_to_pickup` (delivery status also transitions to `en_route_to_pickup`)
- `en_route_to_pickup` → `picked_up`
- `picked_up` → `en_route_to_delivery` (delivery status transitions to `in_transit`)
- `en_route_to_delivery` → `completed` (requires proof only when policy flags require it, delivery status transitions to `delivered`)

### Replacement Orders
- When completing a direct delivery with `delivery_flow = 'replacement_forward'`, the response includes a `replacement_return` object.
- For warehouse-staged replacements, the parent has `delivery_flow = 'replacement_forward'`; completing the final outbound child creates the return for that parent and includes `replacement_return` in the child completion response.
- Inbound/staging leg completion does not create the return.
- The return reuses the replacement forward service policy. If that policy requires staging, the backend creates a return HYBRID parent and assigns its inbound customer→warehouse child to the same driver; otherwise it assigns the direct return delivery.
- The return parent links to the forward through `replacement_forward_delivery_id`; its operational children use `parent_delivery_id`.
- The return parent and all return legs have `delivery_fee = 0` and `cod_amount = 0`; replacement returns never collect goods COD because the forward fee covers the round trip.
- The driver should display `replacement_return` immediately. It is the assigned inbound child for staged returns and the direct return for non-staged returns.
- The replacement return is a completion prerequisite. If its creation fails, the action returns `409` with `error = replacement_return_creation_failed`; the forward delivery and assignment remain uncompleted so the driver can retry.

### Failed-delivery exception workflow
- An open exception blocks this endpoint with HTTP 409 `code=delivery_has_open_exception`; the assignment, parcel custody, and reserved capacity remain with the driver.
- Dispatcher `retry` resolves the exception, restores the prior operational status, clears any return instruction, and lets the same assigned driver continue through this ordinary action endpoint.
- Dispatcher `return` resolves the exception and sets persistent `returnStatus=return_approved`. The driver app must hide Delivered and show one whole-delivery Mark Returned action.
- Mark Returned calls `POST /delivery/deliveries/{deliveryId}/status` with `{ "target_delivery_status": "returned", "reason": "...", "notes": "..." }`; it does not use `returned_items` and does not require parcel-by-parcel selection.
- Normal direct and P&D inbound deliveries become Returned at the active leg's original pickup location.
- P&D outbound and operational-return legs go back to their pickup warehouse and the status response contains `return_handoff`. A manually returned P&D outbound becomes Returned while its parent becomes Arrived at warehouse; normal warehouse receipt later changes that same outbound to Pending. Operational-return retries keep their existing warehouse-receipt behavior.
- An assigned driver may also choose the same whole-delivery Returned status outside an exception.
- A stale ordinary delivery action after return approval receives HTTP 409 `code=return_required`; it must not mark the delivery Delivered.

### COD Payment Record (after completion)
- If the delivery payment method is COD, the system records a successful `payments` row for the **COD amount only** after the delivery is marked `delivered`.
- The delivery customer/merchant wallet is credited immediately at delivery completion. Driver remittance later only clears the driver cash liability and does not credit that COD payout a second time.

### Driver earning
- When a delivery is completed, the backend credits one idempotent `wallet_transactions` earning row for the assigned driver using `tenant_settings.driver_compensation`.
- For staged/P&D child legs, earning is calculated from the parent delivery fee and split equally across the sequenced child legs. With the normal two-leg P&D flow, the pickup driver gets half and the delivery driver gets half.
- The earning row uses `type=earning`, `reference_type=delivery`, and `reference_id=<deliveryId>`.

### Note

If a delivery is still in `picked_up` status when completing (assignment status `en_route_to_delivery`), the system will automatically transition the delivery to `in_transit` first before marking it as `delivered` to ensure proper status flow.

### Proof Enforcement
- `signature_url` is required only when the active leg's `proofRequirements` has `signature_required=true`.
- `photo_urls` is required only when the active leg's `proofRequirements` has `photo_required=true`.
- Non-warehouse pickup legs use `proof_settings.driver_pickup`.
- Warehouse pickup legs use `proof_settings.warehouse_pickup`.
- Non-warehouse delivery legs use `proof_settings.driver_delivery`.
- Warehouse delivery legs use `proof_settings.warehouse_delivery`.
- `notes` is optional.

### Location Tracking
- `current_lat` (optional): Current latitude for location tracking
- `current_lng` (optional): Current longitude for location tracking
- Route calculation is queued after the status transition so Google Directions latency does not delay the action response.
- `delivery_assignments.route_details` may still contain the previous route immediately after this response and is updated asynchronously when calculation succeeds.

### Hybrid Parent Status
- Inbound leg status drives the parent through `assigned`, `en_route_to_pickup`, `picked_up`, `in_transit`, and `arrived_at_warehouse`.
- Warehouse receiving sets the parent to `staged_at_warehouse`; assigning the outbound leg does not regress that stage.
- Outbound `en_route_to_pickup` and `picked_up` remain child-leg-only statuses and do not change the parent from `staged_at_warehouse`. Outbound `in_transit` sets the parent to `in_transit`, and outbound completion sets it to `delivered`.
- Parent `status_updates` inherit `actor_id` and `actor_role` from the child status event that caused the parent transition.

### Response
- Returns the updated delivery object with its current status. Pickup proof is returned as `delivery.proof_of_pickup`; delivery proof is returned as `delivery.proof_of_delivery`.
- Always includes `proofRequirements`, `feeToCollectAtPickup`, and `feeToCollectAtDelivery`, calculated from the resolved effective policy for the active delivery leg.
- Includes `warehouseEndpoint` when the active leg starts or ends at a warehouse.
- For direct replacement forwards and final outbound children of staged replacements, also returns `replacement_return` with the auto-created active return delivery.
- For final outbound children of staged replacements, `delivery.delivery_flow` is returned as `replacement_forward` and `delivery.replacement_parent_delivery_id` contains the staged parent id.

### Frontend integration contract
- Replace the local delivery with `delivery` from this response after each successful action.
- On HTTP 409 `cannot_perform_action_on_cancelled_delivery`, remove the item from active task state and show it only in the Cancelled tab; display the localized `message` and expose no action.
- On HTTP 409 `delivery_terminal_no_actions`, remove the item from active task state and show it only in the Returned tab; display the localized `message`.
- Read `delivery.proofRequirements` before deciding whether to ask for signature or photo proof.
- If `replacement_return` is present, insert/show it immediately as the driver's next return pickup; it is already assigned to the same driver.
- On `409` with `error = replacement_return_creation_failed`, keep the forward delivery in its current local state and allow the driver to retry the completion action; do not mark it complete locally.
- Continue sending the child `deliveryId` to this endpoint for staged replacement actions; use `replacement_parent_delivery_id` only for linkage/display.

### Event actor contract

for a driver action, new delivery and assignment status events record the authenticated driver record ID (`drivers.id`) as `actor_id`; the backend derives it from the authenticated user and validated assignment. The request must not supply an actor ID. The authenticated user ID remains the identity used for authorization and user-owned package records.

### Opt-in durable completion receipt
For `assignment_status=en_route_to_delivery` only, send `Idempotency-Key: <UUID>` with the existing completion body. JWT driver authentication is required. The server atomically claims this tenant/delivery/driver operation before calling the existing action owner; this is not a server job queue. The request body is fingerprinted with object keys sorted and array order preserved. Keep uploaded proof URLs and parcel outcomes unchanged for the operation. A changed body with the same key returns HTTP 409 `idempotency_conflict`.

Response adds `operation_id`, `delivery_id`, and `operation_state`:
- `completed` (HTTP 200): the action succeeded, required payment work and applicable driver earning succeeded, and the complete response was durably saved. Existing delivery/replacement/return fields remain present. A matching replay returns the original receipt without executing writes. Receipts describe that operation, not the current state after later staff changes.
- `processing` (HTTP 202, `success=false`, `code=completion_operation_processing`): this exact operation is already owned by an earlier request. No second action is executed.
- `rejected` (original error HTTP status): validation failed before a completion write could begin; the saved error replays for the same key/body. A corrected submission must use a new UUID.
- `needs_attention` (HTTP 409, `success=false`, `code=completion_operation_needs_attention`): the strategy may have applied some changes or a durable receipt could not be confirmed. Do not automatically repeat the completion. Preserve all evidence and check the operation status. When available, `cause.message` preserves the original localized action error and `cause.code` its safe machine code; show this detail for the affected order.

An unresolved operation also blocks a different key for the same delivery (`completion_operation_already_exists`, HTTP 409). Completed receipts do not prevent a legitimately reopened delivery from following normal status validation under a new key. The server never steals an old processing claim or re-executes uncertain work. When a processing receipt is older than 180 seconds, reads/replays conservatively show `needs_attention`; this does not cancel an original request that is still running. If that original request later saves its final receipt, subsequent reads show the final state.

### Mobile integration

Keep one completion POST in flight while the driver prepares the next order. Mark success only from `operation_state=completed`; HTTP 202, missing/empty responses, and transport errors are not success. After a timeout or app restart, use the operation status GET. A delivery status of delivered alone does not prove every completion side effect finished. After an explicit 404 operation lookup, resending the same UUID with the exact immutable body is safe: the atomic claim grants only one owner even if the first request is still arriving. Never use a new UUID to recover an uncertain submission. For processing or needs_attention, only check status; do not repeat completion. Retain the same UUID and uploaded URLs. Legacy callers without the header retain existing behavior.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/action/:deliveryId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveryId | path string | Yes | ID of the delivery to perform action on | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| assignment_status | string |  |  |  |
| signature_url | string |  |  |  |
| photo_urls | array<string> |  |  |  |
| notes | string |  |  |  |
| returned_items | array<object> |  |  |  |
| returned_items.id | string |  |  |  |
| returned_items.reason | string |  |  |  |
| returned_items.notes | string |  |  |  |
| returned_items.photo_urls | array<string> |  |  |  |
| current_lat | string |  |  |  |
| current_lng | string |  |  |  |



### Request body (raw)

```json
{
  "assignment_status": "en_route_to_delivery",
  "signature_url": "https://example.com/signatures/signature-123.png",
  "photo_urls": [
    "https://example.com/photos/photo-1.jpg"
  ],
  "notes": "Optional delivery notes",
  "returned_items": [
    {
      "id": "parcel-3-uuid",
      "reason": "damaged",
      "notes": "Outer box was crushed",
      "photo_urls": [
        "https://example.com/photos/parcel-3-damage.jpg"
      ]
    }
  ],
  "current_lat": "33.3152",
  "current_lng": "44.3661"
}
```

### Response 200 Success - Short Pickup Confirmed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Package picked up successfully",
  "delivery": {
    "id": "delivery-uuid",
    "status": "picked_up",
    "package_details": {
      "pickup_status_summary": {
        "expected": 3,
        "received": 2,
        "not_received": 1,
        "unconfirmed": 0,
        "is_confirmed": true,
        "is_short": true
      },
      "items": [
        {
          "id": "parcel-1-uuid",
          "pickup_status": "received"
        },
        {
          "id": "parcel-2-uuid",
          "pickup_status": "received"
        },
        {
          "id": "parcel-3-uuid",
          "pickup_status": "not_received"
        }
      ]
    }
  }
}
```

### Response 409 Error - Return Instruction Requires Mark Returned

Conflict

```json
{
  "success": false,
  "code": "return_required",
  "error": "return_required",
  "message": "This parcel must be returned and can no longer be marked delivered"
}
```

### Response 200 Success - Multipart delivery completed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery completed successfully",
  "delivery": {
    "id": "inbound-child-uuid",
    "status": "delivered",
    "parcelCount": 2,
    "actionableParcelCount": 0,
    "packageOutcomeStage": "inbound",
    "packageOutcomeActionsAllowed": true,
    "parcels": [
      {
        "id": "parcel-1",
        "itemCode": "DEL1500-1",
        "name": "Box 1",
        "quantity": 1,
        "status": "delivered",
        "parentStatus": "pending",
        "actionable": false
      },
      {
        "id": "parcel-2",
        "itemCode": "DEL1500-2",
        "name": "Box 2",
        "quantity": 1,
        "status": "returned",
        "parentStatus": "returned",
        "actionable": false,
        "returnReason": "damaged"
      }
    ],
    "package_details": {
      "items": [
        {
          "id": "parcel-1",
          "status": "delivered",
          "parent_status": "pending",
          "actionable": false
        },
        {
          "id": "parcel-2",
          "status": "returned",
          "parent_status": "returned",
          "actionable": false,
          "return_reason": "damaged"
        }
      ],
      "item_status_summary": {
        "pending": 0,
        "delivered": 1,
        "returned": 1,
        "is_partially_delivered": true,
        "is_partially_returned": true
      }
    }
  }
}
```

### Response 200 Success - Delivery Completed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery completed successfully",
  "delivery": {
    "id": "uuid",
    "status": "delivered",
    "delivery_flow": "replacement_forward",
    "proofRequirements": {
      "pickup": {
        "photo_required": true,
        "signature_required": true
      },
      "delivery": {
        "photo_required": false,
        "signature_required": false
      }
    },
    "feeToCollectAtPickup": 0,
    "feeToCollectAtDelivery": 0,
    "warehouseEndpoint": {
      "type": "dropoff",
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Central Warehouse"
      }
    },
    "proof_of_delivery": {
      "signature": "https://example.com/signatures/signature-123.png",
      "photos": [
        "https://example.com/photos/photo-1.jpg",
        "https://example.com/photos/photo-2.jpg"
      ]
    },
    "updated_at": "2023-08-02T16:30:00Z"
  },
  "replacement_return": {
    "id": "uuid",
    "parent_delivery_id": "uuid",
    "delivery_flow": "replacement_return",
    "status": "assigned",
    "delivery_code": 10042,
    "pickup_location": {
      "full_address": "Basra Customer",
      "lat": 30.5081,
      "long": 47.7835
    },
    "delivery_location": {
      "full_address": "Baghdad HQ",
      "lat": 33.3152,
      "long": 44.3661
    },
    "assignment": {
      "id": "uuid",
      "status": "en_route_to_pickup"
    },
    "notes": "Return for replacement DEL10041"
  }
}
```

### Response 400 Error - Missing Signature

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "signature_url_required"
}
```

### Response 400 Error - Missing Photos

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "photo_urls_required"
}
```

### Response 409 Error - Delivery Cancelled

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This delivery was cancelled and can no longer be updated",
  "error": "cancelled_delivery_return_only"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /delivery/action/{{delivery_id}}/operations/{{completion_operation_id}} — Get Driver Completion Operation

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#get-get-driver-completion-operation)

Read a driver completion receipt after a lost response or app restart. JWT driver only; tenant, user, and driver ownership are checked. This read never re-runs completion and never changes delivery or money state.

### HTTP 200

`{success:true,data:{operation_id,delivery_id,state,http_status,result}}`. `state` is `processing`, `completed`, `rejected`, or `needs_attention`. `result` is the saved action JSON, including operation fields, or null while the original request has no final receipt. `http_status` is the saved action HTTP status or null. Processing older than 180 seconds is presented as `needs_attention` without releasing the claim; a late original result can still finalize it. For completed use `result` as the original action response. For rejected show the saved error; for needs_attention preserve evidence and request review rather than retrying blindly.

### Errors

400 invalid UUID; 401 missing JWT; 403 non-driver; 404 `completion_operation_not_found` for missing or unauthorized receipt; 503 `completion_operation_unavailable` on database failure. After an explicit 404, the mobile worker may resend the exact original completion body under the same UUID; concurrent arrivals still have one owner. Never use a different UUID for this recovery. For processing or needs_attention, only check status. Operation result is historical and is never inferred from delivery status.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/action/{{delivery_id}}/operations/{{completion_operation_id}}?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | path string | Yes | Variable used inside the request path. |  |
| completion_operation_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  |  | en |



### Response 200 Completed receipt

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "operation_id": "11111111-1111-4111-8111-111111111111",
    "delivery_id": "22222222-2222-4222-8222-222222222222",
    "state": "completed",
    "http_status": 200,
    "result": {
      "success": true,
      "operation_id": "11111111-1111-4111-8111-111111111111",
      "operation_state": "completed",
      "delivery_id": "22222222-2222-4222-8222-222222222222",
      "delivery": {
        "id": "22222222-2222-4222-8222-222222222222",
        "status": "delivered"
      }
    }
  }
}
```

### Response 200 Still processing

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "operation_id": "11111111-1111-4111-8111-111111111111",
    "delivery_id": "22222222-2222-4222-8222-222222222222",
    "state": "processing",
    "http_status": null,
    "result": null
  }
}
```

## POST /delivery/action/:deliveryId — Complete Cancelled Delivery Return Handoff

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/deliveries#post-complete-cancelled-delivery-return-handoff)

Completes the physical return handoff for a cancelled delivery whose assignment is still `picked_up` or `en_route_to_delivery`. Driver authentication and ownership of the assignment are required. The request must contain the literal `action=complete_return_handoff`; `assignment_status` is not required for this action. The backend atomically changes the assignment to `returned`, releases driver capacity exactly once, records return audit context, and leaves the logical delivery/order status `cancelled`. Retries after a successful handoff are idempotent. A normal driver action on the cancelled delivery returns HTTP 409 `cancelled_delivery_return_only`; this action on a non-cancelled delivery returns HTTP 409 `return_handoff_requires_cancelled_delivery`. Frontend integration: this action is not surfaced by the current driver task-list contract. The default driver list does not return `returnInstruction`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/action/:deliveryId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveryId | path string | Yes | The cancelled operational child delivery ID returned by GET /delivery/driver-deliveries | active-child-leg-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| action | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "action": "complete_return_handoff",
  "notes": "Parcel handed to warehouse staff"
}
```

### Response 200 Success

OK

```json
{
  "success": true,
  "message": "The delivery was returned successfully",
  "delivery": {
    "id": "active-child-leg-uuid",
    "status": "cancelled"
  }
}
```

### Response 409 Error - Return only

Conflict

```json
{
  "success": false,
  "message": "This delivery was cancelled and can no longer be updated",
  "error": "cancelled_delivery_return_only"
}
```

## PUT /delivery/assignments/driver/:driverId/sequence — Update Driver Assignment Sequence

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#put-update-driver-assignment-sequence)

Updates a driver's assignment sequence. Dispatcher, tenant operations manager, system admin, or the matching authenticated driver is required. Body `assignments` must be a non-empty array. Success returns `updated_count`. HTTP 400, 403, and 500 response `message` values are localized using `language` or `Accept-Language`; the frontend must display the returned plain-language message and must not expect internal keys such as `missing_required_params`, `unauthorized`, or `internal_error`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/assignments/driver/:driverId/sequence?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes | Driver ID | driver-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| assignments | array<object> |  |  |  |
| assignments.assignment_id | string |  |  |  |
| assignments.sequence_number | number |  |  |  |



### Request body (raw)

```json
{
  "assignments": [
    {
      "assignment_id": "assignment-uuid",
      "sequence_number": 1
    }
  ]
}
```

## GET /delivery/assignments/:driverId/sequence — Get Driver Assignment Sequence

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#get-get-driver-assignment-sequence)

Returns the driver's active assignments ordered by `sequence_number`. Authorization matches the update endpoint. Success returns `assignments` and `total`. HTTP 400, 403, and 500 response `message` values are localized using `language` or `Accept-Language`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/assignments/:driverId/sequence?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes | Driver ID | driver-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



## GET /delivery/assignments — List Assignments

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#get-list-assignments)

Get a list of delivery assignments with pagination, filtering, sorting and stats. Admin users can view all assignments; drivers can only view their own. Use the optional `include` query parameter for `customer`, `delivery_details`, and `route` expansions. Expanded pickup and delivery locations are resolved through their respective address relationships.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/assignments?page=1&limit=10&status=&driver_id=&delivery_id=&bulk_assignment_id=&sort_by=&sort_order=desc&column_filters=&include=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of items per page | 10 |
| status | query string |  | Filter by assignment status (pending, accepted, rejected, completed) |  |
| driver_id | query string |  | Filter by driver ID (Admin only; drivers always see their own assignments) |  |
| delivery_id | query string |  | Filter by delivery ID |  |
| bulk_assignment_id | query string |  | Filter by bulk assignment ID |  |
| sort_by | query string |  | Column to sort by. Allowed: assignment_time. Defaults to assignment_time desc when not provided. |  |
| sort_order | query string |  | Sort direction: asc or desc (default: desc) | desc |
| column_filters | query string |  | JSON string of column filters. Supported keys: status, driver_id, vehicle_id, delivery_id, assigned_by, bulk_assignment_id, external_reference_id (string ILIKE), delivery_code (integer exact), assignment_time ({from?, to?} date range). Example: {"driver_id":"5c5cd7f0-2b31-402f-872d-81cf17058fe2","assigned_by":"e40f4705-361e-4fcf-a221-49857de4fa82"} |  |
| include | query string | No | Optional comma-separated expansions: customer, delivery_details, route. customer adds delivery customer details; delivery_details adds pickup/delivery addresses and service details; route adds pickup/delivery address coordinates. |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignments retrieved successfully",
  "assignments": [
    {
      "id": "uuid",
      "tenant_id": "uuid",
      "delivery_id": "uuid",
      "assignment_time": "2025-01-15T08:00:00Z",
      "status": "pending",
      "sequence_number": 1,
      "estimated_pickup_time": "2025-01-15T09:00:00Z",
      "delivery": {
        "id": "uuid",
        "delivery_code": "DEL-001",
        "external_reference_id": "EXT-001",
        "customer": {
          "id": "uuid",
          "full_name": "Jane Doe",
          "phone": "+1234567890"
        }
      },
      "drivers": {
        "id": "uuid",
        "user": "John Driver"
      },
      "vehicle": {
        "id": "uuid",
        "name": "Toyota Hilux",
        "image_url": null
      },
      "assigned_by": {
        "id": "uuid",
        "full_name": "Admin User"
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 42,
  "stats": {
    "totalAssignments": 150,
    "pendingAssignments": 30,
    "completedAssignments": 100,
    "recentlyAddedCount": 12
  }
}
```

### Response 400 Error - Invalid column_filters JSON

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "column_filters",
  "message": "invalid_json"
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized to access this resource"
}
```

## POST /delivery/assignments — Create Assignment

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#post-create-assignment)

`POST /delivery/assignments` accepts a Pending P&D outbound or warehouse-origin operational return only after Execute has created one completed picking order and reserved its full positive warehouse stock. Open delivery or warehouse-item exceptions block assignment with HTTP 409 (`delivery_has_open_exception`, `parent_exception_open`, `warehouse_item_exception_open`, or `warehouse_preparation_inconsistent`). Assignment and driver capacity commit atomically; eligible P&D work also creates one pending package handoff linked to the delivery, completed pick, assignment, and driver. Warehouse stock remains positive and reserved because assignment is responsibility, not custody. Before receipt, a manually returned P&D outbound is `returned` and cannot be assigned; its parent is `arrived_at_warehouse`. After receipt, the backend creates a fresh assignment row; the returned assignment remains immutable history and is never reused or rewritten. Frontend sends the Pending delivery ID in `delivery_id` and the selected active driver in `driver_id` using the normal request body. Realtime mobile contract: the existing `delivery_realtime_assignments` row includes `assignment_status` and `requires_driver_response`. The boolean is true only when the authoritative assignment status is exactly `pending`; `assigned`, later lifecycle states, missing status, and unknown status are informational. Mobile keeps the existing notification UI and shows Accept/Reject only when `requires_driver_response === true`.

Create a new delivery assignment. Only users with 'manage:operations:tenant' permission can create assignments. The assignment row, driver capacity, and directly-assigned delivery status are committed atomically. Google route calculation runs after creation and stores `route_details` asynchronously, so route availability does not delay or determine assignment success.

### P&D parent resolution

A staged/P&D parent delivery ID is accepted in `delivery_id`. The backend resolves the first non-terminal, non-draft child by parent `sequence_plan` order and creates or reuses the assignment on that active child, never on the parent. Parent-resolved success includes `resolved_delivery_id`. If no operational child is active, the request fails with localized `no_active_child_leg`.

### Open exception rule

An active assignment cannot be created while the resolved operational delivery has an open delivery exception or an open Waybill `missing_on_receive` discrepancy. The API returns HTTP 409 `delivery_has_open_exception`. A normal delivery exception must be resolved through its exception action; a missing receipt must be resolved as `found` or `lost` through the dedicated Waybill discrepancy endpoint before assignment becomes available.

### Zone Validation
- Uses `pickup_zone_id` / `dropoff_zone_id` from `delivery_list_view` (persisted on `deliveries`) and always validates driver zone access (no silent skip)
- If driver has `all_zones = true`, they can be assigned to any delivery
- If driver has `all_zones = false`, they must have the pickup zone and/or delivery zone assigned via `delivery_zone_drivers` table
- Errors: `driver_not_allowed_in_pickup_zone`, `driver_not_allowed_in_delivery_zone`, `driver_not_allowed_in_zones`, `delivery_zone_validation_unavailable` (row missing from view). On view query failure: `internal_server_error` (500)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/assignments?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | string |  |  |  |
| driver_id | string |  |  |  |
| vehicle_id | string |  |  |  |
| estimated_pickup_time | string |  |  |  |
| estimated_delivery_time | string |  |  |  |
| sequence_number | number |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "delivery_id": "uuid",
  "driver_id": "uuid",
  "vehicle_id": "uuid",
  "estimated_pickup_time": "2023-08-01T13:00:00Z",
  "estimated_delivery_time": "2023-08-01T14:00:00Z",
  "sequence_number": 1,
  "notes": "Please deliver with care"
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignment created successfully",
  "id": "child-assignment-uuid",
  "resolved_delivery_id": "active-child-delivery-uuid"
}
```

### Response 409 Error - Warehouse Item Exception Open

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "warehouse_item_exception_open",
  "message": "Resolve the open warehouse item exception before continuing"
}
```

### Response 409 Error - Warehouse Preparation Inconsistent

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "warehouse_preparation_inconsistent",
  "message": "The parcel's warehouse preparation records are inconsistent"
}
```

### Response 400 Error - Delivery Already Assigned

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery is already assigned"
}
```

### Response 409 Error - No Active Child Leg

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "no_active_child_leg",
  "message": "This order cannot move yet because its next delivery step is not ready"
}
```

### Response 409 Error - Return To Sender In Progress

Conflict

```json
{
  "success": false,
  "code": "return_to_sender_already_in_progress",
  "message": "A return to sender is already in progress"
}
```

### Response 404 Error - Driver Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not found"
}
```

### Response 400 Error - Driver Not Allowed in Pickup Zone

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not allowed in pickup zone"
}
```

### Response 400 Error - Driver Not Allowed in Delivery Zone

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not allowed in delivery zone"
}
```

## POST /delivery/assignments/bulk — Create Bulk Assignments

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#post-create-bulk-assignments)

Bulk assignment follows the same ownership rule as single assignment. Assignment rows, delivery activation, driver capacity, and all eligible P&D pending handoffs commit in one database transaction. Every P&D/operational-return row must already have one completed pick, full reservation, positive stock, and no open delivery or warehouse-item exception; any failure rolls back the complete bulk request. Concurrent state conflicts return a localized response with a stable code such as `delivery_status_changed`, `delivery_already_assigned`, `driver_not_active`, `insufficient_vehicle_capacity`, `parent_exception_open`, `delivery_has_open_exception`, `warehouse_item_exception_open`, or `warehouse_preparation_inconsistent`. Terminal assignment rows, including `returned`, are historical and do not block a new assignment for a Pending delivery. A failed operational return received back at its pickup warehouse can therefore be assigned without rewriting its prior returned assignment. Realtime mobile contract: the single existing bulk `delivery_realtime_assignments` row includes `assignment_status` and `requires_driver_response`. The boolean is true only when the tenant setting produces `pending`; `assigned`, missing, and unknown values are informational. Mobile shows Accept/Reject only when `requires_driver_response === true`.

Create multiple delivery assignments in a single request for a single driver. Only users with 'manage:operations:tenant' permission can create bulk assignments.

### Request Format
- `driver_id` (required): Single driver ID for all deliveries
- `delivery_ids` (required): Array of delivery IDs to assign

### Parent/Leg Assignment Contract
- A staged or hybrid parent delivery ID is accepted as dispatcher input.
- The backend resolves each parent to its first active non-terminal child leg by `sequence_plan` order and creates the assignment on that leg, never on the parent.
- A child leg in `draft` status is not available for assignment and returns `no_active_child_leg` (400). Warehouse receiving/execution must activate that leg first.
- If a parent and its active leg are both submitted, the resolved duplicate is rejected.
- Successful responses include `resolved_deliveries` when parent IDs were redirected. Each item contains `requested_delivery_id` and `assigned_delivery_id`. Frontends should navigate to or refresh the assigned child leg.

### Validation
- All resolved deliveries must be in 'pending' status
- All resolved deliveries must be internal carrier type
- Driver must be active and have a vehicle assigned
- Total unit size of all resolved deliveries must fit in driver's vehicle capacity
- Every resolved delivery must appear in `delivery_list_view` for the tenant; otherwise `deliveries_zone_context_incomplete` (400) with `delivery_ids` missing from the view
- Driver must have access to all required zones (`pickup_zone_id` / `dropoff_zone_id` on the resolved delivery when present) unless driver has all_zones enabled; otherwise `driver_not_allowed_in_zones` and optional `missing_zones`
- No duplicate submitted or resolved delivery IDs allowed

### Response
- Returns assignment IDs, count, total units, driver capacity used, and optional parent-to-leg resolutions
- Creates a single bulk realtime notification record in `delivery_realtime_assignments` table with:
  - `is_bulk: true`
  - `bulk_assignment_id`: Unique ID for this bulk operation
  - `assignment_count`: Number of deliveries assigned
  - `total_units`: Total units consumed
- Sends single bulk notification to driver (not individual notifications)
- Individual assignments are created in `delivery_assignments` table for querying

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/assignments/bulk?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | string |  |  |  |
| delivery_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "driver_id": "uuid",
  "delivery_ids": [
    "uuid1",
    "uuid2"
  ]
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk assignments created successfully",
  "assignment_ids": [
    "uuid1",
    "uuid2"
  ],
  "count": 2,
  "total_units": 15,
  "driver_capacity_used": 30,
  "resolved_deliveries": [
    {
      "requested_delivery_id": "parent-uuid",
      "assigned_delivery_id": "active-leg-uuid"
    }
  ]
}
```

### Response 400 Error - Some Deliveries Already Assigned

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Some deliveries are already assigned",
  "delivery_ids": [
    "uuid1"
  ]
}
```

### Response 409 Error - Return To Sender In Progress

Conflict

```json
{
  "success": false,
  "code": "return_to_sender_already_in_progress",
  "message": "A return to sender is already in progress"
}
```

### Response 400 Error - Some Drivers Not Active

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Some drivers are not active",
  "driver_ids": [
    "uuid"
  ]
}
```

## PUT /delivery/assignments/bulk/:bulkAssignmentId — Bulk Accept/Reject Assignments

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#put-bulk-accept-reject-assignments)

Acceptance/rejection updates assignments, delivery status, capacity, and eligible P&D handoffs atomically. Accept keeps the same pending handoff actionable for the accepted driver. Reject keeps the completed pick and warehouse reservation but clears the same pending handoff's assignment and driver context so it cannot be executed until reassigned.

Accept or reject all assignments in a bulk operation. Only the assigned driver can accept/reject their own bulk assignments.

### Request Format
- `action` (required): "accept" or "reject"

### Validation
- Driver must be active
- All assignments must belong to the specified bulk_assignment_id
- All assignments must belong to the authenticated driver
- All assignments must be in 'pending' status

### Accept Action
- Updates all assignments to 'accepted' status
- Updates all delivery statuses to 'assigned'
- Sends notification to dispatchers

### Reject Action
- Updates all assignments to 'rejected' status
- Frees up driver capacity (total units released)
- Sends notification to dispatchers
- Deliveries remain in 'pending' status (can be reassigned)
- Any rejected delivery that belongs to a trip has `trip_id` and `trip_sequence` cleared

### Response
- Returns assignment IDs, count, and total units freed (if rejected)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/assignments/bulk/:bulkAssignmentId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| bulkAssignmentId | path string | Yes | Bulk assignment ID from bulk assignment creation | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| action | string |  |  |  |



### Request body (raw)

```json
{
  "action": "accept"
}
```

### Response 200 Success - Bulk Accept

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk assignments accepted successfully",
  "assignment_ids": [
    "uuid1",
    "uuid2"
  ],
  "count": 2,
  "total_units_freed": 0
}
```

### Response 200 Success - Bulk Reject

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk assignments rejected successfully",
  "assignment_ids": [
    "uuid1",
    "uuid2"
  ],
  "count": 2,
  "total_units_freed": 15
}
```

### Response 400 Error - Not All Assignments Pending

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "All assignments must be in pending status",
  "invalid_assignment_ids": [
    "uuid1"
  ]
}
```

### Response 404 Error - Bulk Assignments Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Bulk assignments not found"
}
```

## PUT /delivery/assignments/:assignmentId — Update Assignment

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#put-update-assignment)

For a prepared P&D outbound or operational return, a pre-pickup driver change updates the same pending package handoff to the current assignment and driver. It never creates another pick or handoff and never moves warehouse stock. Rejection or unassignment preserves the completed pick and reservation while clearing the handoff's driver context.

Update an existing delivery assignment. Admin users can update any assignment. Drivers can only update their own assignments and are restricted to updating status, actual_pickup_time, actual_delivery_time, notes, and `reason` (supported as an alias for `notes` when self-unassigning). When assignment status becomes `accepted` or `assigned`, the related delivery row is synced to `status=assigned`; when assignment status becomes `en_route_to_pickup`, the related delivery row is synced to `status=en_route_to_pickup`.

### P&D pickup boundary

When `status=picked_up` targets a lifecycle-managed P&D or operational-return assignment, the backend requires the existing completed picking order and current handoff, rechecks both exception systems and warehouse stock, then atomically completes the handoff, marks the pick `handed_off`, records the `pick` movement, clears warehouse stock/reservation, moves parcel custody to `in_transit`, and updates the delivery and assignment to `picked_up`. It does not create missing warehouse preparation during pickup. When a rejected or unassigned assignment record is reused after warehouse receipt and Retry, pickup validates only the newly assigned handoff and picking-order custody cycle; earlier completed cycles remain historical and do not block pickup or replay. Waybill and ordinary assignment updates keep their existing flow. Important conflicts are returned as localized stable codes such as `pickup_picking_order_required`, `pickup_handoff_required`, `warehouse_item_exception_open`, and `pickup_stock_unavailable`.

### Unassign rule

`status=unassigned` is accepted before pickup when the operational delivery status is `pending`, `assigned`, or `en_route_to_pickup` and the resolved active assignment still has a driver. This includes withdrawing a pending driver offer before acceptance. The backend atomically changes `delivery_assignments.status` to `unassigned`, clears the driver/vehicle and route/trip state, releases driver capacity, and leaves or returns `deliveries.status` to `pending`. A plain Pending delivery without an active driver assignment returns HTTP 409 with code `assignment_cannot_be_unassigned`. `picked_up`, `in_transit`, and all later or terminal states return HTTP 409 with code `delivery_status_does_not_allow_unassign`.

### Parent/leg contract

If the target assignment belongs to a staged/P&D parent, unassign or an admin `driver_id` change resolves to the first active non-terminal child leg in `sequence_plan`. Driver ownership changes only on that child assignment. When unassign returns the active inbound child delivery to `pending`, the staged parent delivery also returns to `pending`; other child legs remain unchanged. Assigning a driver to that unassigned active inbound leg is recorded as `driver_assigned` and synchronizes the parent to the leg's resulting `pending` or `assigned` lifecycle status. A genuine driver-to-driver change is recorded as `driver_switched` and remains child-scoped. Outbound-leg preparation also remains child-scoped. Successful responses include `resolved_from_parent`.

### Driver change restriction

Changing `driver_id` on a routed delivery remains rejected with `cannot_switch_driver_for_routed_delivery`.

### Error contract

HTTP 404 with code `assignment_not_found` is returned only when the requested assignment ID does not exist for the tenant. Assignment lookup failures return HTTP 500 with code `internal_server_error`; they are not reported as a missing assignment.

### Zone validation (admin changing `driver_id`)

Same rules as create assignment; failures return `delivery_zone_validation_unavailable` or driver zone messages (`driver_not_allowed_in_*`). View load errors return `internal_server_error` (500).

### Frontend integration contract

Keep the existing request shape. For `status=picked_up`, display the returned localized pickup conflict and keep the current assignment/delivery state; no separate handoff or picking-order completion call is required.

### Event actor contract

when the assigned driver performs a status action, new assignment and delivery status events use `actor_role=driver` and `actor_id=drivers.id`. Dispatcher-performed actions use the dispatcher user ID. The backend derives the actor from the authenticated request and assignment; clients do not send `actor_id`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/assignments/:assignmentId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| assignmentId | path string | Yes | Assignment ID, parent assignment ID, or P&D parent delivery/order ID; parent inputs resolve to the active child assignment | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| actual_pickup_time | string |  |  |  |
| actual_delivery_time | null |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "status": "in_progress",
  "actual_pickup_time": "2023-08-01T13:05:00Z",
  "actual_delivery_time": null,
  "notes": "Customer called, will be home in 10 minutes"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignment updated successfully",
  "assignment": {
    "id": "child-assignment-uuid",
    "tenant_id": "uuid",
    "delivery_id": "active-child-delivery-uuid",
    "driver_id": "uuid",
    "status": "in_progress",
    "actual_pickup_time": "2023-08-01T13:05:00Z",
    "notes": "Customer called, will be home in 10 minutes",
    "updated_at": "2023-08-01T13:05:00Z"
  },
  "resolved_from_parent": {
    "parent_delivery_id": "parent-delivery-uuid",
    "delivery_id": "active-child-delivery-uuid",
    "assignment_id": "child-assignment-uuid"
  }
}
```

### Response 409 Error - Return To Sender In Progress

Conflict

```json
{
  "success": false,
  "code": "return_to_sender_already_in_progress",
  "message": "A return to sender is already in progress"
}
```

### Response 400 Error - Invalid Status Transition

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid status transition"
}
```

### Response 409 Error - Delivery Already In Route

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot switch driver for a delivery that is already assigned to a route"
}
```

### Response 409 Error - Delivery Status Does Not Allow Unassign

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_status_does_not_allow_unassign",
  "message": "A driver can be unassigned only before the parcel is picked up"
}
```

### Response 404 Error - Assignment Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "assignment_not_found",
  "message": "Assignment Not Found"
}
```

### Response 500 Error - Assignment Lookup Failed

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "internal_server_error",
  "message": "Internal Server Error"
}
```

## POST /delivery/assignments/:assignmentId/unassign — Unassign Assignment (Dispatcher)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#post-unassign-assignment-dispatcher)

This action applies only to a current active assignment. For prepared P&D work, unassignment atomically clears the current driver/assignment from the same pending package handoff but preserves its completed picking order and warehouse stock reservation. The handoff becomes non-actionable until another driver is assigned. After a failed operational return is received at the warehouse, the old returned assignment is historical; do not send its ID to this endpoint. Show Assign for the Pending delivery and create a fresh assignment first. Change Driver or Unassign becomes available again only for that new active assignment.

Dispatcher-only. Unassign the current driver before pickup when the operational delivery status is `pending`, `assigned`, or `en_route_to_pickup` and the resolved active assignment still has a driver. This includes withdrawing a pending driver offer before acceptance. The path target may be an assignment ID or a staged/P&D parent delivery ID/parent assignment ID. Parent targets resolve to the first active non-terminal child leg in `sequence_plan`; driver ownership changes only on that child assignment. The operation is atomic: `delivery_assignments.status` becomes `unassigned`, driver/vehicle and route/trip state are cleared, driver capacity is released, and the active delivery remains or becomes `pending`. When that active child is the inbound P&D leg, its staged parent delivery also remains or becomes `pending`; other child legs remain unchanged. An outbound child unassign does not reset a parent already at warehouse staging. A plain Pending delivery without an active driver assignment returns HTTP 409 with code `assignment_cannot_be_unassigned`. `picked_up`, `in_transit`, and all later or terminal states return HTTP 409 with code `delivery_status_does_not_allow_unassign`. The tenant assignment-change feature must be enabled.

The previous driver receives one `assignment_unassigned` notification with `priority=critical`, `display_mode=modal`, `requires_acknowledgement=true`, `portal_context=driver_app`, the reason, and the existing delivery navigation action. Notification creation and push delivery are background work and do not extend this API response path.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/assignments/:assignmentId/unassign?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| assignmentId | path string | Yes | Assignment ID, parent assignment ID, or P&D parent delivery/order ID; parent inputs resolve to the active child assignment | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "reason": "Operational change"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignment unassigned successfully",
  "assignment": {
    "id": "uuid",
    "status": "unassigned",
    "delivery_id": "uuid",
    "delivery_status": "pending",
    "previous_driver_id": "uuid",
    "resolved_from_parent": true
  },
  "resolved_from_parent": true
}
```

### Response 409 Error - Pending Delivery Has No Active Driver Assignment

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "assignment_cannot_be_unassigned",
  "message": "This assignment cannot be unassigned in its current state"
}
```

### Response 409 Error - Parent Has No Active Child Leg

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "no_active_child_leg",
  "message": "No active child delivery leg is available for this parent order"
}
```

## POST /delivery/assignments/:assignmentId/switch-driver — Switch Assignment Driver (Dispatcher)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#post-switch-assignment-driver-dispatcher)

Before pickup, a prepared P&D driver switch updates the same pending package handoff with the current assignment and replacement driver. The completed picking order and reservation are preserved; no new handoff is created and physical custody stays with the warehouse. Existing post-pickup driver-to-driver behavior is unchanged.

Dispatcher-only. Switch the driver on a standalone delivery or operational child leg using the same assignment record. The path target may also be a staged/P&D parent delivery ID or parent assignment ID; the backend resolves it to the first active non-terminal child leg and changes only that child. A pre-pickup child leg may return to `pending` while the replacement driver awaits acceptance, but its staged parent keeps its existing lifecycle status. Respects `assignment_change_settings` and validates driver zones, capacity, and primary vehicle. If the active delivery already has `route_id` or `route_session_id`, the switch is rejected with `cannot_switch_driver_for_routed_delivery`.

One `assignment_driver_switched` event keeps the existing driver-specific notifications: the previous driver receives the acknowledgement modal and the replacement driver receives the existing toast or banner. At every delivery stage, operational custody transfers to the replacement driver during the switch itself. The switch leaves `handoff_status` null and does not block the replacement driver's next status action. The previous driver's “I understand” action acknowledges the notification only. Parent inputs still change only the resolved active child leg; the P&D parent lifecycle row is not directly reassigned. A successful switch also appends one `driver_switched` activity to the canonical root delivery's `order_events` in the same transaction. For a P&D child switch, the event includes `delivery_id`, `delivery_code`, `direction`, `leg_sequence`, and `source=active_leg`; past missing child driver events are backfilled without duplicates. This activity write does not change the parent lifecycle status or customer/driver `status_updates`. Snapshots include driver ID, user ID, full name, nullable avatar URL, and nullable phone. Copy/call secondary actions are omitted when phone is null. Push text never includes phone data. Notification enrichment, rendering, insertion, and push delivery run in the EdgeRuntime background task.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/assignments/:assignmentId/switch-driver?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| assignmentId | path string | Yes | Assignment ID, parent assignment ID, or P&D parent delivery/order ID; parent inputs resolve to the active child assignment | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | string |  |  |  |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "driver_id": "uuid",
  "reason": "Driver changed"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Driver switched successfully",
  "assignment": {
    "id": "uuid",
    "driver_id": "uuid",
    "status": "pending",
    "delivery_id": "uuid"
  },
  "resolved_from_parent": true
}
```

### Response 409 Error - Terminal Delivery

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_terminal_no_actions",
  "message": "This delivery is complete and cannot be changed"
}
```

### Response 409 Error - No Active Child Leg

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "no_active_child_leg",
  "message": "This order cannot move yet because its next delivery step is not ready"
}
```

## POST /delivery/assignments/:assignmentId/route — Update Assignment Route

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#post-update-assignment-route)

Update the route details for an existing assignment. Admin users can update any assignment's route. Drivers can only update routes for their own assignments. HTTP 404 with code `assignment_not_found` is returned only when the assignment does not exist for the tenant. Assignment lookup failures return HTTP 500 with code `internal_server_error`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/assignments/:assignmentId/route?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| assignmentId | path string | Yes | ID of the assignment to update route for | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| route_details | object |  |  |  |
| route_details.distance | number |  |  |  |
| route_details.duration | number |  |  |  |
| route_details.polyline | string |  |  |  |
| route_details.stops | array<object> |  |  |  |
| route_details.stops.location | object |  |  |  |
| route_details.stops.location.latitude | number |  |  |  |
| route_details.stops.location.longitude | number |  |  |  |
| route_details.stops.sequence | number |  |  |  |
| route_details.stops.estimated_arrival | string |  |  |  |



### Request body (raw)

```json
{
  "route_details": {
    "distance": 18.5,
    "duration": 55,
    "polyline": "updated_encoded_polyline_string",
    "stops": [
      {
        "location": {
          "latitude": 37.7749,
          "longitude": -122.4194
        },
        "sequence": 1,
        "estimated_arrival": "2023-08-01T13:00:00Z"
      },
      {
        "location": {
          "latitude": 37.7694,
          "longitude": -122.4862
        },
        "sequence": 2,
        "estimated_arrival": "2023-08-01T13:30:00Z"
      }
    ]
  }
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignment route updated successfully",
  "assignment": {
    "id": "uuid",
    "tenant_id": "uuid",
    "delivery_id": "uuid",
    "driver_id": "uuid",
    "route_details": {
      "distance": 18.5,
      "duration": 55,
      "polyline": "updated_encoded_polyline_string",
      "stops": [
        {
          "location": {
            "latitude": 37.7749,
            "longitude": -122.4194
          },
          "sequence": 1,
          "estimated_arrival": "2023-08-01T13:00:00Z"
        },
        {
          "location": {
            "latitude": 37.7694,
            "longitude": -122.4862
          },
          "sequence": 2,
          "estimated_arrival": "2023-08-01T13:30:00Z"
        }
      ]
    },
    "updated_at": "2023-08-01T12:30:00Z"
  }
}
```

### Response 400 Error - Route Details Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Route details are required"
}
```

### Response 404 Error - Assignment Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "assignment_not_found",
  "message": "Assignment Not Found"
}
```

### Response 500 Error - Assignment Lookup Failed

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "internal_server_error",
  "message": "Internal Server Error"
}
```

## GET /delivery/assignments/:assignmentId — Get Assignment

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/assignments#get-get-assignment)

Get details for a specific delivery assignment. Admin users can view any assignment, while drivers can only view their own assignments. HTTP 404 with code `assignment_not_found` is returned only when the assignment is not visible for the tenant and caller. Assignment lookup failures return HTTP 500 with code `internal_server_error`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/assignments/:assignmentId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| assignmentId | path string | Yes | ID of the assignment to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignment retrieved successfully",
  "assignment": {
    "id": "uuid",
    "tenant_id": "uuid",
    "delivery_id": "uuid",
    "driver_id": "uuid",
    "vehicle_id": "uuid",
    "assignment_time": "2023-08-01T12:00:00Z",
    "assigned_by": "uuid",
    "sequence_number": 1,
    "status": "assigned",
    "estimated_pickup_time": "2023-08-01T13:00:00Z",
    "estimated_delivery_time": "2023-08-01T14:00:00Z",
    "actual_pickup_time": null,
    "actual_delivery_time": null,
    "route_details": {
      "distance": 15.5,
      "duration": 45,
      "polyline": "encoded_polyline_string",
      "stops": [
        {
          "location": {
            "latitude": 37.7749,
            "longitude": -122.4194
          },
          "sequence": 1,
          "estimated_arrival": "2023-08-01T13:00:00Z"
        }
      ]
    },
    "notes": "Sample delivery notes",
    "created_at": "2023-08-01T12:00:00Z",
    "updated_at": "2023-08-01T12:00:00Z",
    "deliveries": {
      "id": "uuid",
      "external_reference_id": "REF123",
      "status": "assigned",
      "customer_id": "uuid"
    },
    "drivers": {
      "id": "uuid",
      "user": "John Smith"
    },
    "vehicles": {
      "id": "uuid",
      "name": "Vehicle 1"
    }
  }
}
```

### Response 404 Error - Assignment Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "assignment_not_found",
  "message": "Assignment Not Found"
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 500 Error - Assignment Lookup Failed

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "internal_server_error",
  "message": "Internal Server Error"
}
```

## GET /delivery/services — List Delivery Services

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/delivery-services#get-list-delivery-services)

List delivery services with direct filters, AdvancedDataGrid column_filters, server-side sorting, pagination, customer-access scoping, and tenant-scoped KPI stats. Each service includes `allow_returns` and `allowed_delivery_attempts`.

### Frontend integration contract

every operational G2G service dropdown calls this endpoint with `context=g2g&active=true&include_stats=false`. The Transfer Manifest Settings multi-select calls it without `context=g2g` so administrators can choose from all active tenant services.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/services?page=1&limit=10&service_level=&active=true&allow_returns=&delivery_type=&delivery_type_code=&search=&price_order=&column_filters=%7B%22service_level%22%3A%22express%22%2C%22active%22%3A%22true%22%2C%22default_price%22%3A%7B%22min%22%3A100%2C%22max%22%3A5000%7D%2C%22signature_required%22%3A%22true%22%7D&sort_by=created_at&sort_order=desc&tenant_id=&customer_id=&context=&include_stats=true

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (default: 10, max: 100) | 10 |
| service_level | query string | No | Filter by service level (economy, standard, express, same_day, next_day, scheduled) economy\|standard\|express\|same_day\|next_day\|scheduled |  |
| active | query string | No | Filter by active status (true/false) | true |
| allow_returns | query string | No | Filter by return availability (true/false) |  |
| delivery_type | query string | No | Filter by delivery_type_id (UUID) |  |
| delivery_type_code | query string | No | Filter by delivery_type.code |  |
| search | query string | No | Search by name or code (ilike) ilike on name/code |  |
| price_order | query string | No | Order by default_price (asc/desc) legacy asc/desc sort on default_price |  |
| column_filters | query string | No | JSON object for grid column filters. Supports text, exact, boolean, numeric range, and created_at date-range filters. JSON string with keys `name`, `code`, `description`, `service_level`, `delivery_type_code`, `active`, `allow_returns`, `default_price`, `unit_min`, `unit_max`, `estimated_delivery_time_min`, `estimated_delivery_time_max`, `signature_required`, `insurance_available`, `created_at` | {"service_level":"express","active":"true","default_price":{"min":100,"max":5000},"signature_required":"true"} |
| sort_by | query string | No | Sort field. Allowed: name, code, default_price, unit_min, unit_max, service_level, estimated_delivery_time_min, estimated_delivery_time_max, priority, created_at, updated_at | created_at |
| sort_order | query string | No | Sort direction (asc\|desc). Defaults to desc. `asc` or `desc` (defaults to `desc` when `sort_by` is provided) | desc |
| tenant_id | query string | No | Tenant ID (defaults to authenticated user's tenant) |  |
| customer_id | query string | No | Filter by customer (user) ID to see available services for that customer. If the authenticated user is a customer/no-role business user, this is applied automatically. user_id of customer; auto-applied when caller is a business customer |  |
| context | query string | No | Use g2g in every G2G service picker. A non-empty transfer_manifest.available_delivery_service_ids setting is applied before pagination; an empty list adds no limitation. only `g2g` is supported. It intersects the configured G2G allow-list with existing access scopes before pagination. Empty/missing `available_delivery_service_ids` means unrestricted. |  |
| include_stats | query string | No | Set false for combobox requests to skip KPI queries. defaults to true; use false for repeated picker requests. | true |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery services retrieved successfully",
  "deliveryServices": [
    {
      "id": "8999fbc8-594e-4636-a88c-e8cc93609d9e",
      "name": "DCT",
      "code": "HJ888",
      "description": "test",
      "name_translations": {
        "en": "DCT",
        "ar": "...",
        "ku": "..."
      },
      "description_translations": {
        "en": "test",
        "ar": "...",
        "ku": "..."
      },
      "image_url": "https://cdn.example.com/service-icon.png",
      "priority": 10,
      "service_level": "economy",
      "default_price": 10,
      "estimated_delivery_time_min": 0,
      "estimated_delivery_time_max": 0,
      "allowed_delivery_attempts": 2,
      "service_options": {
        "signature_required": false,
        "insurance_available": false
      },
      "unit_min": 1,
      "unit_max": 5000,
      "delivery_type": {
        "id": "6e4b331f-0cff-44f4-a3c4-c4e8b3bd78fb",
        "code": "pickup_and_delivery",
        "name": "Pickup and Delivery"
      },
      "active": true,
      "allow_returns": true
    }
  ],
  "stats": {
    "totalServices": 42,
    "activeServices": 30,
    "deliveryTypesCount": 4,
    "recentlyAdded": []
  },
  "page": 1,
  "limit": 10,
  "total": 42
}
```

## POST /delivery/services — Create Delivery Service

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/delivery-services#post-create-delivery-service)

Create a new delivery service. Requires manage:operations:tenant permission.

- Requires valid authentication token with manage:operations:tenant permission

  - When `zone_selector_type=zone`: `zone_ids` required if zone_access_mode is select_zones or exclude_zones
  - When `zone_selector_type=tag`: `zone_tag_ids` required if zone_access_mode is select_zones or exclude_zones

### Errors
- 404 delivery_type_not_found
- 409 service_code_already_exists
- 400 customer_ids_required_for_selected_access_mode
- 400 zone_ids_required_for_selected_zone_mode
- 400 invalid_zone_ids
- 400 zone_tag_ids_required_for_tag_selector
- 400 invalid_zone_tag_ids
- 400 allowed_delivery_attempts_must_be_positive_integer
- 500 internal_server_error

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/services?tenant_id=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object | Yes | (string OR localized object `{en, ar, ku}`) |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| code | string | Yes |  |  |
| description | object | No | (string OR localized object) |  |
| description.en | string |  |  |  |
| description.ar | string |  |  |  |
| description.ku | string |  |  |  |
| image_url | string | No |  |  |
| priority | integer | No |  |  |
| service_level | string | Yes |  |  |
| default_price | number | No |  |  |
| estimated_delivery_time_min | number | No |  |  |
| estimated_delivery_time_max | number | No |  |  |
| allowed_delivery_attempts | number | No | (positive integer, default `1`): customer-delivery attempts allowed before retry requires a dispatcher override reason; return-to-sender deliveries do not count |  |
| unit_min | number | Yes |  |  |
| unit_max | number | Yes |  |  |
| service_options | object | No |  |  |
| service_options.tracking | boolean |  |  |  |
| service_options.signature_required | boolean |  |  |  |
| service_options.insurance_available | boolean |  |  |  |
| delivery_type_id | string | Yes |  |  |
| active | boolean | No |  |  |
| allow_returns | boolean |  |  |  |
| access_mode | string | No | (all_users\|select_users\|exclude_users) |  |
| user_ids | array<string> |  | (required if access_mode is select_users or exclude_users) |  |
| zone_access_mode | string | No | (all_zones\|select_zones\|exclude_zones) |  |
| zone_selector_type | string | No | (zone\|tag, default: zone) |  |
| zone_tag_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Express 24-Hour Delivery",
    "ar": "خدمة التوصيل السريع خلال 24 ساعة",
    "ku": "گەیاندنی خێرا 24 کاتژمێر"
  },
  "code": "EXP24",
  "description": {
    "en": "Premium express delivery service",
    "ar": "خدمة توصيل سريع مميزة",
    "ku": "خزمەتگوزاری گەیاندنی خێرای پریمیوم"
  },
  "image_url": "https://cdn.example.com/tenants/tenant_123/delivery-services/service-icon.png",
  "priority": 10,
  "service_level": "express",
  "default_price": 25.99,
  "estimated_delivery_time_min": 12,
  "estimated_delivery_time_max": 24,
  "allowed_delivery_attempts": 2,
  "unit_min": 1,
  "unit_max": 10,
  "service_options": {
    "tracking": true,
    "signature_required": true,
    "insurance_available": true
  },
  "delivery_type_id": "uuid",
  "active": true,
  "allow_returns": true,
  "access_mode": "select_users",
  "user_ids": [
    "user_uuid_1",
    "user_uuid_2"
  ],
  "zone_access_mode": "select_zones",
  "zone_selector_type": "tag",
  "zone_tag_ids": [
    "tag_uuid_1",
    "tag_uuid_2"
  ]
}
```

### Response 201 201 Created



```json
{
  "success": true,
  "message": "service_created_successfully",
  "id": "uuid"
}
```

## PUT /delivery/services/:serviceId — Update Delivery Service

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/delivery-services#put-update-delivery-service)

Update an existing delivery service. Requires manage:operations:tenant permission.

- Requires valid authentication token with manage:operations:tenant permission

- `code`, `service_level`, `delivery_type_id`, `default_price`, `estimated_delivery_time_min/max`, `allowed_delivery_attempts`, `unit_min/unit_max`, `service_options`, `active`
- `allowed_delivery_attempts` must be a positive integer. It controls customer-delivery retry warnings and excludes operational return attempts.

### Errors
- 404 service_not_found
- 404 delivery_type_not_found
- 409 service_code_already_exists
- 400 invalid_customer_ids
- 400 customer_ids_required_for_selected_access_mode
- 400 invalid_zone_ids
- 400 invalid_zone_tag_ids
- 400 allowed_delivery_attempts_must_be_positive_integer
- 500 internal_server_error

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/services/:serviceId?tenant_id=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| serviceId | path string | Yes | ID of the delivery service to update | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) Tenant ID (defaults to authenticated user's tenant) |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object | No | (string OR localized object) |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| description | object | No | (string OR localized object) |  |
| description.en | string |  |  |  |
| description.ar | string |  |  |  |
| description.ku | string |  |  |  |
| image_url | string | No |  |  |
| priority | integer | No |  |  |
| default_price | number |  |  |  |
| estimated_delivery_time_min | number |  |  |  |
| estimated_delivery_time_max | number |  |  |  |
| allowed_delivery_attempts | number |  |  |  |
| unit_min | number |  |  |  |
| unit_max | number |  |  |  |
| service_options | object |  |  |  |
| service_options.tracking | boolean |  |  |  |
| service_options.signature_required | boolean |  |  |  |
| service_options.insurance_available | boolean |  |  |  |
| active | boolean |  |  |  |
| allow_returns | boolean |  |  |  |
| access_mode | string | No | (all_users\|select_users\|exclude_users) |  |
| user_ids | array<string> | No |  |  |
| zone_access_mode | string | No | (all_zones\|select_zones\|exclude_zones) |  |
| zone_selector_type | string |  | (zone\|tag): `zone_ids` required when zone, `zone_tag_ids` required when tag |  |
| zone_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Premium Express 24-Hour Delivery",
    "ar": "...",
    "ku": "..."
  },
  "description": {
    "en": "Updated premium express delivery",
    "ar": "...",
    "ku": "..."
  },
  "image_url": "https://cdn.example.com/tenants/tenant_123/delivery-services/new-icon.png",
  "priority": 5,
  "default_price": 29.99,
  "estimated_delivery_time_min": 10,
  "estimated_delivery_time_max": 24,
  "allowed_delivery_attempts": 2,
  "unit_min": 1,
  "unit_max": 10,
  "service_options": {
    "tracking": true,
    "signature_required": true,
    "insurance_available": true
  },
  "active": true,
  "allow_returns": true,
  "access_mode": "exclude_users",
  "user_ids": [
    "user_uuid_3"
  ],
  "zone_access_mode": "select_zones",
  "zone_selector_type": "zone",
  "zone_ids": [
    "zone_uuid_1"
  ]
}
```

### Response 409 Conflict - Service type used by settings

Conflict

```json
{
  "success": false,
  "code": "delivery_service_type_change_blocked_by_settings",
  "message": "The service type cannot be changed because this delivery service is used in system settings",
  "setting_references": [
    {
      "source": "tenant_settings",
      "setting_key": "return_requests"
    }
  ]
}
```

### Response 200 200 OK



```json
{
  "success": true,
  "message": "service_updated_successfully",
  "deliveryService": {
    "id": "uuid"
  }
}
```

## GET /delivery/services/:serviceId — Get Delivery Service

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/delivery-services#get-get-delivery-service)

Get a delivery service by ID.

token required.

### Response (full) includes
- `name` / `description` (strings, localized to Accept-Language if translations available)
- `name_translations` / `description_translations` (full JSONB objects for admin edit forms)
- `image_url` (string, nullable)
- `priority` (integer, nullable)
- `allowed_delivery_attempts` (positive integer used for customer-delivery retry limits)
- `zone_access_mode` (all_zones|select_zones|exclude_zones)
- `zone_selector_type` (zone|tag)
- `zone_ids` (array, populated when zone_selector_type=zone)
- `zones` (array of {id, name}, populated when zone_selector_type=zone)
- `zone_tag_ids` (array, populated when zone_selector_type=tag)
- `zone_tags` (array of {id, name}, populated when zone_selector_type=tag)
- `teams`, `all_teams` (team assignment info)
- `effective_policy` (resolved fulfillment policy)
- `vehicle_ids`, `vehicles` (vehicle restrictions)

Response (basic) includes only the core service record with `delivery_type`.

### Responses
- 200 service_retrieved_successfully
- 404 service_not_found
- 500 internal_server_error

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/services/:serviceId?tenant_id=&response_type=full

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| serviceId | path string | Yes | ID of the delivery service | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| response_type | query string | No | Response detail level: 'basic' (core fields + delivery_type only) or 'full' (includes users, zones, teams, policy, vehicles). Defaults to 'full'. `basic` or `full` (default: `full`). Use `basic` for fast lookups that only need core service fields. | full |



## DELETE /delivery/services/:serviceId — Delete Delivery Service

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/delivery-services#delete-delete-delivery-service)

Delete (permanently) a delivery service. Requires manage:operations:tenant permission.

### Responses
- 200 service_deactivated_successfully
- 404 service_not_found
- 403 insufficient_permissions
- 500 internal_server_error

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/services/:serviceId?tenant_id=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| serviceId | path string | Yes | ID of the delivery service | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |



## GET /delivery/types — List Delivery Types

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/delivery-types#get-list-delivery-types)

List all delivery types. This is a public endpoint that does not require authentication.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/types

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery types retrieved successfully",
  "deliveryTypes": [
    {
      "id": "uuid",
      "name": "Express"
    },
    {
      "id": "uuid",
      "name": "Standard"
    },
    {
      "id": "uuid",
      "name": "Economy"
    }
  ]
}
```

### Response 500 Error Response

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error",
  "error": {}
}
```

## GET /delivery/types/:typeId — Get Delivery Type by ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/delivery-types#get-get-delivery-type-by-id)

Get a specific delivery type by its ID. This is a public endpoint but benefits from language support if a token is provided.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/types/:typeId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| typeId | path string | Yes | ID of the delivery type to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery type retrieved successfully",
  "deliveryType": {
    "id": "uuid",
    "name": "Express"
  }
}
```

### Response 404 Error - Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery type not found"
}
```

### Response 400 Error - Invalid ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request, missing ID"
}
```

## GET /delivery/price — Get Delivery Price (Legacy Units)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/price#get-get-delivery-price-legacy-units)

Backward-compatible delivery price calculation. The caller supplies a positive `units` query value. The response now also returns `unit_size` and `unit_size_source=unit_size`. New create forms should use POST /delivery/price so the backend can choose between package measurements, submitted unit size, and the configured missing-unit fallback.

### Errors

HTTP 400 validation failures return `{success:false,error}` for missing required query parameters or tenant context, non-positive/non-numeric `units`, or invalid coordinates. Pricing-domain failures return HTTP 400 with `{success:false,code,message,...details}`; `message` is localized and additional detail fields depend on `code`. Unexpected failures return HTTP 500 with `{success:false,message}`. Display the localized `error` or `message` and branch on HTTP status and `code` when present.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/price?delivery_service_id=uuid&pickup_lat=23.780&pickup_lng=90.410&dropoff_lat=23.810&dropoff_lng=90.430&units=1&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_service_id | query string |  |  | uuid |
| pickup_lat | query string |  |  | 23.780 |
| pickup_lng | query string |  |  | 90.410 |
| dropoff_lat | query string |  |  | 23.810 |
| dropoff_lng | query string |  |  | 90.430 |
| units | query string |  |  | 1 |
| language | query string |  |  | en |



### Response 200 Success

OK

```json
{
  "success": true,
  "price": 5000,
  "unit_size": 1,
  "unit_size_source": "unit_size",
  "message": "Price calculated successfully"
}
```

## POST /delivery/price — Preview Delivery Price From Order Data

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/price#post-preview-delivery-price-from-order-data)

Returns the exact backend-selected unit and delivery price before order creation. Authentication and tenant context are required.

- Send pickup and delivery coordinates in `pickup_location` and `delivery_location`; aliases `pickup_address`/`delivery_address` and flat coordinate fields are also accepted. Location objects accept `latitude`/`longitude` or `lat`/`lng`/`long`.
- `package` is the customer-compatible package shape and may contain direct single-package measurements or `items[]`.
- `package_details` remains accepted for backward compatibility. When both package fields are sent, `package_details` takes precedence.

### Unit selection priority
1. If every item provides positive `weight`, `length`, `width`, and `height`, the backend calculates unit size from total weight and largest dimensions using tenant unit settings. This calculated unit overrides submitted `unit_size`; `quantity` defaults to 1 and must be a positive integer.
2. If any item has a zero, missing, or empty measurement, the package is treated as unmeasurable and the submitted `unit_size` is used.
3. If neither usable package measurements nor a submitted unit is available, apply `general.unit_size.settings.missing_unit_size_fallback`: `service_min`, `service_half`, `service_max`, or HTTP 400 `unit_size_or_dimensions_required`.

Success returns `price`, `unit_size`, and `unit_size_source` (`package_details`, `unit_size`, `service_min`, `service_half`, or `service_max`). Negative or non-numeric package measurements return HTTP 400 with `field=package` for the customer-compatible shape or `field=package_details` for the legacy shape. HTTP 400 with `code=can_not_use_this_delivery_for_this_unit_size` returns a localized cause plus `calculated_unit_size`, `allowed_unit_min`, `allowed_unit_max`, and `violation` when the selected service cannot carry the selected units.

### Frontend integration contract

use the same `package` shape as `/customer/delivery/price`, add the current `unit_size`, and send the current locations and service whenever pricing-relevant form data changes. Zero, missing, or empty measurements may remain in package items; the response will use `unit_size_source=unit_size`. Display the returned `unit_size` and `price`; submit the same package data to POST /delivery/deliveries using its existing `package_details` field. On the unit-range code, display `message` and use the numeric fields to highlight the service/package mismatch.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/price?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_service_id | string | Yes | is required. |  |
| pickup_location | object |  |  |  |
| pickup_location.latitude | number |  |  |  |
| pickup_location.longitude | number |  |  |  |
| delivery_location | object |  |  |  |
| delivery_location.latitude | number |  |  |  |
| delivery_location.longitude | number |  |  |  |
| unit_size | number | No | is optional. `units` is accepted as a legacy alias. |  |
| package | object |  |  |  |
| package.items | array<object> |  |  |  |
| package.items.name | string |  |  |  |
| package.items.quantity | number |  |  |  |
| package.items.weight | number |  |  |  |
| package.items.length | number |  |  |  |
| package.items.width | number |  |  |  |
| package.items.height | number |  |  |  |



### Request body (raw)

```json
{
  "delivery_service_id": "uuid",
  "pickup_location": {
    "latitude": 36.1911,
    "longitude": 44.0092
  },
  "delivery_location": {
    "latitude": 36.205,
    "longitude": 44.03
  },
  "unit_size": 1,
  "package": {
    "items": [
      {
        "name": "Large box",
        "quantity": 1,
        "weight": 1,
        "length": 100,
        "width": 100,
        "height": 100
      }
    ]
  }
}
```

### Response 200 Success - Package Measurements Override Unit

OK

```json
{
  "success": true,
  "price": 20000,
  "unit_size": 4,
  "unit_size_source": "package_details",
  "message": "Price calculated successfully"
}
```

### Response 400 Error - Invalid Package Measurements

Bad Request

```json
{
  "success": false,
  "message": "Invalid package details",
  "field": "package"
}
```

### Response 400 Error - Calculated Units Outside Service Range

Bad Request

```json
{
  "success": false,
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum"
}
```

### Response 400 Error - Delivery Service Not Found

Bad Request

```json
{
  "success": false,
  "message": "Delivery service not found"
}
```

## POST /delivery/returns — Create Return Request (Customer)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/return-requests#post-create-return-request-customer)

Create a return request for a delivered delivery.

### Flow
- Creates a `return_requests` record with `status = pending`
- Updates the original delivery `return_status = return_requested`
- If `system_settings(scope=tenant).return_requests.auto_approval = true`, the system auto-approves and creates a return delivery using `default_delivery_service_id` and `destination_mode`. The configured service must be active and have `allow_returns=true`.

### Return Delivery Payment Settings
- Return deliveries are ALWAYS created with `payment_method = wallet` and `payer_type = sender` (customer pays via wallet), regardless of the original delivery's payment settings.

### Endpoint alias
- Same payload is also accepted at `POST /delivery/returns/request`.

### Business Rules
- Delivery must have `status = delivered`
- Only one non-cancelled return request per delivery
- An open delivery exception or Waybill `missing_on_receive` discrepancy returns HTTP 409 `delivery_has_open_exception`; a missing receipt must be resolved as `found` or `lost` before a return request can be created

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/returns?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string | No | Preferred language (optional) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | string |  |  |  |
| return_reason | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "delivery_id": "delivery-uuid",
  "return_reason": "Damaged product",
  "notes": "Package arrived with visible damage"
}
```

### Response 201 Success - Pending (auto_approval=false)



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "return_request_created",
  "return_request_id": "return-request-uuid",
  "auto_approved": false
}
```

### Response 201 Success - Auto Approved (auto_approval=true)



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "return_request_created",
  "return_request_id": "return-request-uuid",
  "auto_approved": true,
  "return_delivery_id": "return-delivery-uuid"
}
```

### Response 400 Error - Delivery Not Eligible



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "delivery_not_eligible_for_return"
}
```

### Response 400 Error - Return Already Exists



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "return_already_exists_for_delivery"
}
```

## POST /delivery/returns/request — Create Return Request (Customer) - Alias

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/return-requests#post-create-return-request-customer-alias)

Alias of `POST /delivery/returns` (kept for compatibility). Same behavior and payload.

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/returns/request?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string | No | Preferred language (optional) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | string |  |  |  |
| return_reason | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "delivery_id": "delivery-uuid",
  "return_reason": "Damaged product",
  "notes": "Package arrived with visible damage"
}
```

## GET /delivery/returns/requests — List Return Requests (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/return-requests#get-list-return-requests-admin)

List return requests for a tenant with pagination, sorting, column filters and stats.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/returns/requests?page=1&limit=10&status=&search=&sort_by=&sort_order=desc&column_filters=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 10) | 10 |
| status | query string |  | Filter by return request status: pending, approved, cancelled |  |
| search | query string |  | Search across original_delivery_code, return_delivery_code, customer_name (ILIKE) |  |
| sort_by | query string |  | Sort column: delivery_code \| return_delivery_code \| customer_name \| requested_at (default: requested_at) |  |
| sort_order | query string |  | asc or desc (default: desc) | desc |
| column_filters | query string |  | JSON object. Keys: delivery_code (string ILIKE), return_delivery_code (string ILIKE), customer_name (string ILIKE), return_reason (string ILIKE), status (string exact), requested_at ({from?, to?} date range). Example: {"status":"pending","requested_at":{"from":"2025-01-01","to":"2025-01-31"}} |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "return_requests_retrieved",
  "data": {
    "returns": [
      {
        "id": "uuid",
        "delivery_id": "uuid",
        "return_delivery_id": null,
        "return_reason": "Damaged product",
        "status": "pending",
        "requested_at": "2025-01-15T10:00:00Z",
        "notes": null,
        "original_delivery_code": 1001,
        "return_delivery_code": null,
        "original_delivery_status": "delivered",
        "return_delivery_status": null,
        "customer_name": "John Doe"
      }
    ],
    "total": 42,
    "stats": {
      "totalReturns": 100,
      "pendingReturns": 15,
      "approvedReturns": 75,
      "recentlyAddedCount": 8
    }
  }
}
```

### Response 400 Error - Invalid column_filters JSON

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "column_filters",
  "message": "invalid_json"
}
```

## GET /delivery/returns/requests/:id — Get Return Request (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/return-requests#get-get-return-request-admin)

Get return request details including original delivery + (optional) warehouse + (optional) created return delivery.

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/returns/requests/:id?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | return-request-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string | No | Preferred language (optional) | en |



## DELETE /delivery/returns/:id — Delete Return Request (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/return-requests#delete-delete-return-request-admin)

Delete a return request by ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/returns/:id?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | return-request-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  |  | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "return-request-uuid"
  }
}
```

### Response 404 Error - Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "return_not_found"
}
```

## POST /delivery/returns/:id/approve — Approve Return Request (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/return-requests#post-approve-return-request-admin)

Approve a pending return request and create the return delivery using tenant setting `return_requests` (default service + destination mode). The configured default service must be active and have `allow_returns=true`.

### Payment Settings
- Return delivery is created with `payment_method = wallet` and `payer_type = sender` (customer pays via wallet).

### Pickup Address Resolution
- Uses `original.delivery_location_id` if present, otherwise falls back to `original.delivery_location` JSON (the original dropoff address).
- Returns `original_delivery_dropoff_address_missing` only when both are absent.

### Note

this endpoint has no request body; it is fully driven by settings.

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/returns/:id/approve?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | return-request-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string | No | Preferred language (optional) | en |



### Response 200 Success



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "return_request_approved",
  "return_delivery_id": "return-delivery-uuid"
}
```

### Response 400 Dropoff Address Missing



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "original_delivery_dropoff_address_missing"
}
```

## GET /delivery/exceptions — List Exceptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/exceptions#get-list-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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

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

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 10) | 10 |
| status | query string |  | Filter by exception status: open \| resolved |  |
| delivery_id | query string |  | Filter by delivery UUID (standalone param) |  |
| driver_id | query string |  | Filter by driver UUID (standalone param) |  |
| exception_type | query string |  | Filter by exception type (standalone param) |  |
| sort_by | query string |  | Sort column: exception_type \| status \| created_at (default) \| resolved_at |  |
| sort_order | query string |  | asc or desc (default: desc) | desc |
| column_filters | query string |  | 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_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "exceptions_retrieved_successfully",
  "exceptions": [
    {
      "id": "uuid",
      "exception_type": "address_issue",
      "status": "open",
      "created_at": "2025-01-15T08:00:00Z",
      "resolved_at": null,
      "resolution_type": null,
      "actions": [
        {
          "key": "retry",
          "label": "Retry delivery",
          "description": "Try to deliver the parcel again"
        },
        {
          "key": "return",
          "label": "Return to sender",
          "description": "Send the parcel back to its original sender"
        }
      ],
      "exception_details": {
        "trigger_point": "delivery",
        "reason": "Address not found"
      },
      "delivery": {
        "id": "uuid",
        "status": "exception",
        "return_status": null,
        "external_reference_id": "EXT-001",
        "delivery_type": "OnDemand",
        "delivery_type_id": "452ffddf-a8d9-4d16-a5ea-46d16491979c",
        "delivery_fee": 10
      },
      "driver": {
        "id": "uuid",
        "full_name": "John Driver"
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 42,
  "stats": {
    "totalExceptions": 200,
    "openCount": 70,
    "resolvedCount": 130
  }
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /delivery/exceptions/available — List Available Exception Types

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/exceptions#get-list-available-exception-types)

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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/exceptions/available?delivery_id={{delivery_id}}&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | query string |  | Delivery/order UUID whose current status is used to filter exception types | {{delivery_id}} |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Available exception types retrieved successfully",
  "delivery_id": "uuid-child-outbound",
  "delivery_status": "en_route_to_pickup",
  "requested_delivery_id": "uuid-parent",
  "resolved_from_parent_delivery_id": "uuid-parent",
  "order_statuses": [
    "draft",
    "pending",
    "assigned",
    "en_route_to_pickup",
    "picked_up",
    "in_transit",
    "arrived_at_warehouse",
    "staged_at_warehouse"
  ],
  "exception_types": [
    {
      "code": "vehicle_issue",
      "label": "Vehicle Issue",
      "allowed_statuses": [
        "en_route_to_pickup",
        "picked_up",
        "in_transit"
      ]
    }
  ]
}
```

### Response 400 Error - Missing Delivery ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "delivery_id",
  "message": "Delivery ID is required"
}
```

### Response 404 Error - Delivery Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery not found"
}
```

## POST /delivery/exceptions — Create Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/exceptions#post-create-exception)

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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/exceptions?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| 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 |  |  |  |
| 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 |  |  |  |



### Request body (raw)

```json
{
  "delivery_id": "delivery-uuid",
  "driver_id": null,
  "exception_type": "address_issue",
  "exception_details": {
    "reason": "Address not found",
    "severity": "high",
    "location": {
      "latitude": 37.7749,
      "longitude": -122.4194,
      "address": "123 Invalid Street"
    },
    "photos": [
      {
        "url": "https://example.com/photo1.jpg",
        "timestamp": "2023-08-01T14:28:00Z",
        "caption": "Location where address should be"
      }
    ],
    "customer_contacted": true,
    "customer_contact_method": "phone",
    "customer_contact_timestamp": "2023-08-01T14:35:00Z",
    "customer_response": "Will provide updated address",
    "eta_impact": 45
  },
  "notes": "Customer provided incorrect address"
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Exception created successfully",
  "exception": {
    "id": "uuid-exception",
    "tenant_id": "uuid-tenant",
    "delivery_id": "uuid-child-outbound",
    "driver_id": "uuid-driver",
    "exception_type": "not_home",
    "status": "open",
    "reported_at": "2026-08-19T10:30:00Z",
    "resolved_at": null,
    "resolution_type": null,
    "reported_by": "uuid-user",
    "resolved_by": null,
    "exception_details": {
      "reason": "Customer was not available",
      "delivery_status_at_exception": "in_transit",
      "parent_delivery_id_at_exception": "uuid-parent",
      "parent_delivery_status_at_exception": "in_transit",
      "attempt": {
        "number": 2,
        "allowed": 2,
        "limit_reached": true,
        "driver_id": "uuid-driver",
        "failed_at": "2026-08-19T10:30:00Z",
        "reason": "not_home"
      },
      "custody": {
        "state": "awaiting_warehouse_receipt",
        "warehouse_id": "uuid-warehouse",
        "receiving_order_id": "uuid-receiving-order"
      }
    },
    "resolution_details": null,
    "notes": "Customer was not available",
    "created_at": "2026-08-19T10:30:00Z",
    "updated_at": "2026-08-19T10:30:00Z"
  },
  "requested_delivery_id": "uuid-child-outbound",
  "resolved_delivery_id": "uuid-child-outbound",
  "resolved_from_parent_delivery_id": null
}
```

### Response 400 Error - Missing Required Field

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required delivery_id"
}
```

### Response 404 Error - Delivery Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery not found"
}
```

### Response 400 Error - Invalid Driver ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "driver_id",
  "message": "invalid_uuid"
}
```

### Response 404 Error - Driver Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "driver_id",
  "message": "driver_not_found"
}
```

### Response 400 Error - Exception Type Not Available For Status

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "exception_type",
  "message": "This exception type is not available for the current delivery status",
  "delivery_status": "pending",
  "allowed": [
    "delivery_delay",
    "other"
  ]
}
```

### Response 409 Error - Delivery Already Has Open Exception

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "delivery_has_open_exception",
  "error": "delivery_has_open_exception",
  "exception": {
    "id": "exception-uuid",
    "exception_type": "address_issue"
  }
}
```

### Response 409 Error - Terminal Delivery

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "exception_not_allowed_for_terminal_delivery",
  "message": "An exception cannot be added after the delivery is completed, cancelled, or returned"
}
```

### Response 409 Error - Parent Exception Open

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "parent_exception_open",
  "message": "Resolve the parent order exception before starting this delivery"
}
```

## PUT /delivery/exceptions/:exceptionId — Update Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/exceptions#put-update-exception)

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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/exceptions/:exceptionId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| exceptionId | path string | Yes | ID of the exception to update | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| exception_type | string |  |  |  |
| exception_details | object |  |  |  |
| exception_details.reason | string |  |  |  |
| exception_details.severity | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "exception_type": "address_issue",
  "exception_details": {
    "reason": "Address not found",
    "severity": "medium"
  },
  "notes": "Customer provided more address information"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Exception updated successfully",
  "exception": {
    "id": "uuid",
    "tenant_id": "uuid",
    "delivery_id": "uuid",
    "driver_id": "uuid",
    "exception_type": "address_issue",
    "status": "open",
    "reported_at": "2023-08-01T14:30:00Z",
    "resolved_at": null,
    "resolution_type": null,
    "reported_by": "uuid",
    "resolved_by": null,
    "exception_details": {
      "reason": "Address not found",
      "severity": "medium"
    },
    "resolution_details": null,
    "notes": "Customer provided more address information",
    "created_at": "2023-08-01T14:30:00Z",
    "updated_at": "2023-08-01T15:45:00Z"
  }
}
```

### Response 400 Error - Explicit Action Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "exception_action_required",
  "message": "Select an action to resolve this exception"
}
```

### Response 400 Error - Cannot Reopen Resolved Exception

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot reopen resolved exception"
}
```

### Response 400 Error - Invalid Exception Status

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "status",
  "message": "invalid_exception_status",
  "allowed": [
    "open",
    "resolved"
  ]
}
```

## POST /delivery/exceptions/:exceptionId/resolve — Resolve Exception Action

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/exceptions#post-resolve-exception-action)

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`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/exceptions/:exceptionId/resolve?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| exceptionId | path string | Yes | Open exception ID | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| action_key | string |  |  |  |
| notes | string |  |  |  |
| override_reason | string |  |  |  |
| resolution_details | object |  |  |  |
| resolution_details.decision_note | string |  |  |  |



### Request body (raw)

```json
{
  "action_key": "retry",
  "notes": "Parcel is ready for another attempt",
  "override_reason": "Customer confirmed the corrected address",
  "resolution_details": {
    "decision_note": "Retry approved"
  }
}
```

### Response 200 Success - Retry

OK

```json
{
  "success": true,
  "message": "Exception action completed successfully",
  "action_key": "retry",
  "action": {
    "key": "retry",
    "label": "Retry delivery",
    "description": "Try to deliver the parcel again"
  },
  "data": {
    "success": true,
    "action_key": "retry",
    "exception_id": "exception-uuid",
    "delivery_id": "delivery-uuid",
    "delivery_status": "in_transit",
    "root_delivery_id": "delivery-uuid",
    "root_delivery_status": "in_transit",
    "driver_id": "driver-uuid",
    "assignment_id": "assignment-uuid",
    "retry_mode": "resolve_only",
    "pre_exception_status": "in_transit",
    "notify_driver": true,
    "attempt_number": 1,
    "allowed_attempts": 2,
    "idempotent_replay": false
  }
}
```

### Response 200 Success - Pre-Pickup Retry

OK

```json
{
  "success": true,
  "message": "Exception action completed successfully",
  "action_key": "retry",
  "action": {
    "key": "retry",
    "label": "Retry delivery",
    "description": "Try to deliver the parcel again"
  },
  "data": {
    "success": true,
    "action_key": "retry",
    "exception_id": "exception-uuid",
    "delivery_id": "operational-return-uuid",
    "delivery_code": "3481",
    "delivery_status": "en_route_to_pickup",
    "root_delivery_id": "operational-return-uuid",
    "root_delivery_status": "en_route_to_pickup",
    "driver_id": "driver-uuid",
    "assignment_id": "assignment-uuid",
    "driver_notification": "exception_resolved",
    "pre_pickup_status": "en_route_to_pickup",
    "notify_driver": true,
    "idempotent_replay": false
  }
}
```

### Response 200 Success - Staged P&D Outbound Retry

OK

```json
{
  "success": true,
  "message": "Exception action completed successfully",
  "action_key": "retry",
  "action": {
    "key": "retry",
    "label": "Retry delivery",
    "description": "Try to deliver the parcel again"
  },
  "data": {
    "success": true,
    "action_key": "retry",
    "exception_id": "exception-uuid",
    "delivery_id": "outbound-child-uuid",
    "delivery_status": "pending",
    "root_delivery_id": "pnd-parent-uuid",
    "root_delivery_status": "staged_at_warehouse",
    "retry_mode": "warehouse_redispatch",
    "attempt_number": 2,
    "allowed_attempts": 2,
    "picking_order_id": "picking-order-uuid",
    "handoff_id": null,
    "idempotent_replay": false
  }
}
```

### Response 200 Success - Draft/Pending Origin Retry

OK

```json
{
  "success": true,
  "message": "Exception action completed successfully",
  "action_key": "retry",
  "action": {
    "key": "retry",
    "label": "Retry delivery",
    "description": "Try to deliver the parcel again"
  },
  "data": {
    "success": true,
    "action_key": "retry",
    "exception_id": "exception-uuid",
    "delivery_id": "inbound-child-uuid",
    "delivery_status": "pending",
    "root_delivery_id": "pnd-parent-uuid",
    "root_delivery_status": "pending",
    "retry_mode": "resolve_only",
    "retry_scope": "origin",
    "pre_exception_status": "pending",
    "notify_driver": false,
    "idempotent_replay": false
  }
}
```

### Response 200 Success - Cancel Non-terminal Order

OK

```json
{
  "success": true,
  "message": "Exception action completed successfully",
  "action_key": "cancel",
  "action": {
    "key": "cancel",
    "label": "Cancel order",
    "description": "Cancel this delivery order",
    "reason_required": true
  },
  "data": {
    "success": true,
    "action_key": "cancel",
    "exception_id": "exception-uuid",
    "delivery_id": "delivery-uuid",
    "delivery_code": "1865",
    "delivery_status": "cancelled",
    "root_delivery_id": "logical-order-root-uuid",
    "reversal": {},
    "driver_notification_assignments": [],
    "notify_driver": false,
    "idempotent_replay": false,
    "resolution_details": {
      "action_key": "cancel",
      "decision": "cancel",
      "reason": "Sender requested cancellation",
      "action_mode": "logical_order_cancellation"
    }
  }
}
```

### Response 200 Success - Return To Sender

OK

```json
{
  "success": true,
  "message": "Exception action completed successfully",
  "action_key": "return",
  "action": {
    "key": "return",
    "label": "Return to sender",
    "description": "Send the parcel back to its original sender"
  },
  "data": {
    "success": true,
    "action_key": "return",
    "exception_id": "exception-uuid",
    "delivery_id": "outbound-child-uuid",
    "delivery_status": "in_transit",
    "return_status": "return_approved",
    "driver_id": "driver-uuid",
    "assignment_id": "assignment-uuid",
    "notify_driver": true,
    "return_handoff_mode": "warehouse",
    "idempotent_replay": false
  }
}
```

### Response 200 Success - Pre-Pickup P&D Outbound Return

OK

```json
{
  "success": true,
  "message": "Exception action completed successfully",
  "action_key": "return",
  "action": {
    "key": "return",
    "label": "Return to sender",
    "description": "Send the parcel back to its original sender"
  },
  "data": {
    "success": true,
    "action_key": "return",
    "exception_id": "exception-uuid",
    "delivery_id": "outbound-child-uuid",
    "delivery_status": "failed",
    "root_delivery_id": "pnd-parent-uuid",
    "root_delivery_status": "staged_at_warehouse",
    "driver_id": "driver-uuid",
    "assignment_id": "assignment-uuid",
    "return_request_id": "return-request-uuid",
    "return_delivery_id": "return-delivery-uuid",
    "return_delivery_status": "pending",
    "picking_order_id": "picking-order-uuid",
    "handoff_id": null,
    "pre_pickup_status": "en_route_to_pickup",
    "notify_driver": true,
    "driver_notification": "pickup_cancelled",
    "idempotent_replay": false
  }
}
```

### Response 200 Success - Warehouse P&D Parent Return

OK

```json
{
  "success": true,
  "message": "Exception action completed successfully",
  "action_key": "return",
  "action": {
    "key": "return",
    "label": "Return to sender",
    "description": "Send the parcel back to its original sender"
  },
  "data": {
    "success": true,
    "action_key": "return",
    "exception_id": "exception-uuid",
    "delivery_id": "pnd-parent-uuid",
    "delivery_status": "staged_at_warehouse",
    "outbound_delivery_id": "outbound-child-uuid",
    "outbound_delivery_status": "failed",
    "return_request_id": "return-request-uuid",
    "return_delivery_id": "return-delivery-uuid",
    "return_delivery_code": "3252",
    "return_delivery_status": "pending",
    "picking_order_id": "picking-order-uuid",
    "handoff_id": null,
    "notify_driver": false,
    "idempotent_replay": false
  }
}
```

### Response 200 Success - Re-staged P&D Outbound Return

OK

```json
{
  "success": true,
  "message": "Exception action completed successfully",
  "action_key": "return",
  "action": {
    "key": "return",
    "label": "Return to sender",
    "description": "Send the parcel back to its original sender"
  },
  "data": {
    "success": true,
    "action_key": "return",
    "exception_id": "exception-uuid",
    "delivery_id": "outbound-child-uuid",
    "delivery_status": "failed",
    "root_delivery_id": "pnd-parent-uuid",
    "root_delivery_status": "staged_at_warehouse",
    "return_request_id": "return-request-uuid",
    "return_delivery_id": "return-delivery-uuid",
    "return_delivery_status": "pending",
    "picking_order_id": "picking-order-uuid",
    "handoff_id": null,
    "idempotent_replay": false
  }
}
```

### Response 409 Error - Parcel Not Picked Up For Return

Conflict

```json
{
  "success": false,
  "code": "delivery_not_picked_up_for_return",
  "field": "action_key",
  "message": "This order has not been picked up yet. Cancel it instead.",
  "allowed": [
    "retry"
  ]
}
```

### Response 409 Error - Action Not Configured

Conflict

```json
{
  "success": false,
  "code": "exception_action_not_available",
  "field": "action_key",
  "message": "This action is not available for this exception",
  "allowed": [
    "retry"
  ]
}
```

### Response 409 Error - Warehouse Receipt Required

Conflict

```json
{
  "success": false,
  "code": "exception_action_not_available",
  "field": "action_key",
  "message": "Receive this parcel through its normal receiving order",
  "allowed": []
}
```

### Response 400 Error - Cancellation Reason Required

Bad Request

```json
{
  "success": false,
  "code": "reason_required",
  "field": "reason",
  "message": "Reason is required"
}
```

### Response 409 Error - Invalid Status Transition

Conflict

```json
{
  "success": false,
  "code": "invalid_status_transition",
  "message": "Invalid Status Transition"
}
```

### Response 400 Error - Return Service Missing

Bad Request

```json
{
  "success": false,
  "code": "return_delivery_service_required",
  "message": "Configure a default return delivery service first"
}
```

## GET /delivery/exceptions/:exceptionId — Get Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/exceptions#get-get-exception)

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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/exceptions/:exceptionId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| exceptionId | path string | Yes | ID of the exception to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Exception retrieved successfully",
  "exception": {
    "id": "uuid",
    "tenant_id": "uuid",
    "delivery_id": "uuid",
    "driver_id": "uuid",
    "exception_type": "address_issue",
    "status": "open",
    "reported_at": "2023-08-01T14:30:00Z",
    "resolved_at": null,
    "resolution_type": null,
    "reported_by": "uuid",
    "resolved_by": null,
    "actions": [
      {
        "key": "retry",
        "label": "Retry delivery",
        "description": "Try to deliver the parcel again"
      },
      {
        "key": "return",
        "label": "Return to sender",
        "description": "Send the parcel back to its original sender"
      }
    ],
    "delivery": {
      "id": "uuid",
      "status": "exception",
      "return_status": null
    },
    "exception_details": {
      "reason": "Address not found",
      "severity": "high"
    },
    "resolution_details": null,
    "notes": "Customer provided incorrect address",
    "created_at": "2023-08-01T14:30:00Z",
    "updated_at": "2023-08-01T14:30:00Z"
  }
}
```

### Response 404 Error - Exception Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Exception not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

## GET /delivery/driver-performance — Get Driver Performance

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/driver-performance#get-get-driver-performance)

Returns one row per driver from the maintained monthly Driver Performance projection. With month/year omitted, monthly rows are aggregated into one all-time row per driver. A delivery is counted once and attributed to its final relevant driver assignment. Successful deliveries use delivery status delivered; failed deliveries use failed, cancelled, or returned. Period selection follows delivery created_at, matching the Deliveries list date filter. The projection refreshes every 15 minutes and during report rebuilds. on_time_rate and avg_delivery_time_minutes are null when the source records do not contain a real promised-delivery or pickup timestamp. Requires the existing Delivery Management permission enforced by the delivery router.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver-performance?month=9&year=2026&page=1&page_size=20&search=&sort_by=total_deliveries&sort_order=desc&column_filters={}&driver_id=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| month | query string | No | Optional month (1-12). Must be sent together with year. Omit both for All months. | 9 |
| year | query string | No | Optional year (2020-2100). Must be sent together with month. | 2026 |
| page | query string |  | One-based details-table page. | 1 |
| page_size | query string |  | Rows per page (1-100). | 20 |
| search | query string | No | Optional case-insensitive driver-name search. |  |
| sort_by | query string | No | Optional sort field: driver_name, delivery counts, rates, time, distance, value, period_month, or period_year. | total_deliveries |
| sort_order | query string |  | asc or desc. | desc |
| column_filters | query string | No | Optional JSON object. driver_name supports text; numeric fields support an exact number or {"min":n,"max":n}. | {} |
| driver_id | query string | No | Optional driver UUID. |  |
| tenant_id | query string | No | Optional when tenant is present in the authenticated context. |  |
| language | query string |  | Preferred response-message language. | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Driver performance retrieved successfully",
  "month": 9,
  "year": 2026,
  "pagination": {
    "page": 1,
    "page_size": 20,
    "total_records": 2,
    "total_pages": 1
  },
  "available_months": [
    {
      "year": 2026,
      "month": 9
    }
  ],
  "summary": {
    "total_deliveries": 408,
    "successful_deliveries": 408,
    "failed_deliveries": 0,
    "success_rate": 100,
    "on_time_rate": null,
    "avg_delivery_time_minutes": 52.13,
    "total_delivery_value": 0,
    "active_drivers": 2,
    "top_drivers": [
      {
        "driver_id": "uuid",
        "driver_name": "Driver One",
        "total_deliveries": 407,
        "success_rate": 100
      }
    ]
  },
  "data": [
    {
      "driver_id": "uuid",
      "driver_name": "Driver One",
      "period_month": 9,
      "period_year": 2026,
      "total_deliveries": 407,
      "successful_deliveries": 407,
      "failed_deliveries": 0,
      "on_time_deliveries": 0,
      "late_deliveries": 0,
      "success_rate": 100,
      "on_time_rate": null,
      "average_delivery_time_minutes": null,
      "total_distance_km": 0,
      "km_per_delivery": 0,
      "total_delivery_value": 0
    }
  ]
}
```

### Response 200 Success - No records

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Driver performance retrieved successfully",
  "month": 1,
  "year": 2020,
  "pagination": {
    "page": 1,
    "page_size": 20,
    "total_records": 0,
    "total_pages": 0
  },
  "available_months": [],
  "summary": {
    "total_deliveries": 0,
    "successful_deliveries": 0,
    "failed_deliveries": 0,
    "success_rate": 0,
    "on_time_rate": null,
    "avg_delivery_time_minutes": null,
    "total_delivery_value": 0,
    "active_drivers": 0,
    "top_drivers": []
  },
  "data": []
}
```

### Response 400 Error - Invalid period or filter

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Bad request"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /delivery/driver-performance/detail — Get Driver Performance Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/driver-performance#get-get-driver-performance-detail)

Get detailed performance for a single driver including current month, previous month comparison, and 6-month history for charts.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver-performance/detail?driver_id=uuid&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | query string | Yes | Required. Driver ID to get detail for. | uuid |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Response language | en |



### Response 200 Success - Driver detail with history

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "driver": {
    "driver_id": "abc-123",
    "driver_name": "Ahmed Ali",
    "avatar_url": ""
  },
  "current_month": {
    "year": 2025,
    "month": 7,
    "total_deliveries": 45,
    "successful_deliveries": 42,
    "failed_deliveries": 3,
    "on_time_deliveries": 38,
    "late_deliveries": 4,
    "success_rate": 93.33,
    "on_time_rate": 90.48,
    "average_delivery_time_minutes": 28,
    "total_distance_traveled": 320.5,
    "kilometers_per_delivery": 7.63,
    "total_delivery_value": 4500
  },
  "previous_month": {
    "year": 2025,
    "month": 6,
    "total_deliveries": 38,
    "successful_deliveries": 35,
    "failed_deliveries": 3,
    "on_time_deliveries": 30,
    "late_deliveries": 5,
    "success_rate": 92.11,
    "on_time_rate": 85.71,
    "average_delivery_time_minutes": 31,
    "total_distance_traveled": 280,
    "kilometers_per_delivery": 8,
    "total_delivery_value": 3800
  },
  "changes": {
    "total_deliveries": 18.42,
    "success_rate": 1.22,
    "on_time_rate": 4.77,
    "avg_delivery_time": -9.68,
    "total_delivery_value": 18.42
  },
  "history": [
    {
      "year": 2025,
      "month": 2,
      "total_deliveries": 30,
      "successful_deliveries": 28,
      "failed_deliveries": 2,
      "success_rate": 93.33,
      "on_time_rate": 88,
      "average_delivery_time_minutes": 30,
      "total_delivery_value": 3000
    }
  ]
}
```

### Response 400 Error - driver_id required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "driver_id_required"
}
```

## GET /delivery/customer-communications — List Customer Communications

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-communications#get-list-customer-communications)

Get a list of customer communications with filtering and pagination. Customers can only view their own communications, while admin users can view all communications.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/customer-communications?page=1&limit=10&customer_id=&delivery_id=&type=&status=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of items per page | 10 |
| customer_id | query string | No | Filter by customer ID (optional - if not provided and user is a customer, returns their own communications) |  |
| delivery_id | query string |  | Filter by delivery ID |  |
| type | query string |  | Filter by communication type (sms, email, push_notification, call) |  |
| status | query string |  | Filter by status (sent, delivered, failed, read) |  |
| tenant_id | query string | No | Filter by tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Communications retrieved successfully",
  "communications": [
    {
      "id": "uuid",
      "tenant_id": "uuid",
      "customer_id": "uuid",
      "delivery_id": "uuid",
      "type": "sms",
      "direction": "outbound",
      "status": "delivered",
      "content": "Your delivery #1234 is on its way and will arrive between 2-4pm today.",
      "communication_details": {
        "sender": {
          "system_component": "delivery_notifications",
          "phone": "+15551234567"
        },
        "recipient": {
          "phone": "+19876543210",
          "name": "John Doe"
        },
        "delivery_status": [
          {
            "status": "sent",
            "timestamp": "2023-08-01T14:30:00Z",
            "gateway_reference": "sms-ref-123456"
          },
          {
            "status": "delivered",
            "timestamp": "2023-08-01T14:31:00Z",
            "gateway_reference": "sms-ref-123456"
          }
        ],
        "template_id": "delivery-on-way",
        "template_variables": {
          "delivery_id": "1234",
          "delivery_window": "2-4pm"
        }
      },
      "sent_at": "2023-08-01T14:30:00Z",
      "created_at": "2023-08-01T14:30:00Z",
      "updated_at": "2023-08-01T14:31:00Z",
      "tenant_users": {
        "id": "uuid",
        "user_id": "uuid",
        "tenant_id": "uuid",
        "status": "active"
      },
      "deliveries": {
        "id": "uuid",
        "status": "in_transit",
        "scheduled_delivery_time": "2023-08-01T16:00:00Z"
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 25
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## POST /delivery/customer-communications — Create Customer Communication

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-communications#post-create-customer-communication)

Create a new customer communication. This requires admin permissions with manage:operations:tenant. Automatically records outbound communications and can be used to log inbound communications.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/customer-communications?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| customer_id | string |  |  |  |
| delivery_id | string |  |  |  |
| type | string |  |  |  |
| direction | string |  |  |  |
| status | string |  |  |  |
| content | string |  |  |  |
| communication_details | object |  |  |  |
| communication_details.sender | object |  |  |  |
| communication_details.sender.user_id | string |  |  |  |
| communication_details.sender.system_component | string |  |  |  |
| communication_details.sender.phone | string |  |  |  |
| communication_details.recipient | object |  |  |  |
| communication_details.recipient.phone | string |  |  |  |
| communication_details.recipient.name | string |  |  |  |
| communication_details.template_id | string |  |  |  |
| communication_details.template_variables | object |  |  |  |
| communication_details.template_variables.delivery_id | string |  |  |  |
| communication_details.template_variables.delivery_window | string |  |  |  |
| communication_details.attachments | array<object> |  |  |  |
| communication_details.attachments.name | string |  |  |  |
| communication_details.attachments.type | string |  |  |  |
| communication_details.attachments.url | string |  |  |  |
| communication_details.attachments.size | number |  |  |  |



### Request body (raw)

```json
{
  "customer_id": "uuid",
  "delivery_id": "uuid",
  "type": "sms",
  "direction": "outbound",
  "status": "sent",
  "content": "Your delivery #1234 is on its way and will arrive between 2-4pm today.",
  "communication_details": {
    "sender": {
      "user_id": "uuid",
      "system_component": "delivery_notifications",
      "phone": "+15551234567"
    },
    "recipient": {
      "phone": "+19876543210",
      "name": "John Doe"
    },
    "template_id": "delivery-on-way",
    "template_variables": {
      "delivery_id": "1234",
      "delivery_window": "2-4pm"
    },
    "attachments": [
      {
        "name": "delivery_details.pdf",
        "type": "application/pdf",
        "url": "https://example.com/attachments/delivery_details.pdf",
        "size": 245678
      }
    ]
  }
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Communication created successfully",
  "id": "uuid"
}
```

### Response 400 Error - Invalid Communication Type

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid communication type"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /delivery/customer-preferences — List Customer Preferences

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-preferences#get-list-customer-preferences)

List customer delivery preferences with pagination, sorting, column filters and stats.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/customer-preferences?page=1&limit=10&customer_id=&location_id=&search=&sort_by=&sort_order=desc&column_filters=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 10) | 10 |
| customer_id | query string |  | Filter by customer UUID (standalone param) |  |
| location_id | query string |  | Filter by address UUID (standalone param) |  |
| search | query string |  | Search across customer full_name and address full_address (ILIKE) |  |
| sort_by | query string |  | Sort column: customer_id (sorts by full_name) \| preferred_day_of_week \| preferred_time_start \| preferred_time_end \| created_at (default) |  |
| sort_order | query string |  | asc or desc (default: desc) | desc |
| column_filters | query string |  | JSON object. Keys: customer_id (uuid string), location_id (uuid string), preferred_day_of_week ("0"–"6"), delivery_instructions (ILIKE), delivery_location ("true"/"false" → leave_at_door), signature_required ("true"/"false"). Example: {"preferred_day_of_week":"1","signature_required":"true"} |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Customer preferences retrieved successfully",
  "preferences": [
    {
      "id": "uuid",
      "preferred_day_of_week": "1",
      "preferred_time_start": "09:00",
      "preferred_time_end": "17:00",
      "delivery_instructions": "Leave at door",
      "delivery_options": {
        "signature_required": false,
        "leave_at_door": true
      },
      "created_at": "2025-01-15T08:00:00Z",
      "customer": {
        "id": "uuid",
        "full_name": "Jane Doe"
      },
      "location": {
        "id": "uuid",
        "full_address": "123 Main St"
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 25,
  "stats": {
    "totalPreferences": 100,
    "signatureRequiredCount": 30,
    "leaveAtDoorCount": 55,
    "recentlyAddedCount": 7
  }
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## POST /delivery/customer-preferences — Create Customer Preference

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-preferences#post-create-customer-preference)

Create a new customer delivery preference. Customers can only create preferences for themselves, while admin users can create preferences for any customer.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/customer-preferences?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| customer_id | string |  |  |  |
| location_id | string |  |  |  |
| preferred_day_of_week | number |  |  |  |
| preferred_time_start | string |  |  |  |
| preferred_time_end | string |  |  |  |
| delivery_instructions | string |  |  |  |
| delivery_options | object |  |  |  |
| delivery_options.signature_required | boolean |  |  |  |
| delivery_options.leave_with_neighbor | boolean |  |  |  |
| delivery_options.leave_at_door | boolean |  |  |  |
| delivery_options.requires_appointment | boolean |  |  |  |
| delivery_options.package_handling | object |  |  |  |
| delivery_options.package_handling.fragile | boolean |  |  |  |
| delivery_options.package_handling.temperature_sensitive | boolean |  |  |  |
| delivery_options.authorized_receivers | array<object> |  |  |  |
| delivery_options.authorized_receivers.name | string |  |  |  |
| delivery_options.authorized_receivers.relationship | string |  |  |  |
| delivery_options.authorized_receivers.contact | string |  |  |  |
| contact_preferences | object |  |  |  |
| contact_preferences.notification_methods | array<object> |  |  |  |
| contact_preferences.notification_methods.type | string |  |  |  |
| contact_preferences.notification_methods.enabled | boolean |  |  |  |
| contact_preferences.notification_methods.contact | string |  |  |  |
| contact_preferences.notification_events | object |  |  |  |
| contact_preferences.notification_events.on_dispatch | boolean |  |  |  |
| contact_preferences.notification_events.day_before | boolean |  |  |  |
| contact_preferences.notification_events.day_of | boolean |  |  |  |
| contact_preferences.notification_events.hour_before | boolean |  |  |  |
| contact_preferences.notification_events.on_arrival | boolean |  |  |  |
| contact_preferences.notification_events.on_completion | boolean |  |  |  |
| contact_preferences.language | string |  |  |  |



### Request body (raw)

```json
{
  "customer_id": "uuid",
  "location_id": "uuid",
  "preferred_day_of_week": 1,
  "preferred_time_start": "09:00",
  "preferred_time_end": "17:00",
  "delivery_instructions": "Leave packages at the front door",
  "delivery_options": {
    "signature_required": false,
    "leave_with_neighbor": true,
    "leave_at_door": true,
    "requires_appointment": false,
    "package_handling": {
      "fragile": true,
      "temperature_sensitive": false
    },
    "authorized_receivers": [
      {
        "name": "John Smith",
        "relationship": "Neighbor",
        "contact": "+1987654321"
      }
    ]
  },
  "contact_preferences": {
    "notification_methods": [
      {
        "type": "sms",
        "enabled": true,
        "contact": "+1234567890"
      },
      {
        "type": "email",
        "enabled": true,
        "contact": "customer@example.com"
      }
    ],
    "notification_events": {
      "on_dispatch": true,
      "day_before": false,
      "day_of": true,
      "hour_before": true,
      "on_arrival": true,
      "on_completion": true
    },
    "language": "en"
  }
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Preference created successfully",
  "id": "uuid"
}
```

### Response 409 Error - Preference Already Exists

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Preference already exists",
  "preference_id": "uuid"
}
```

### Response 400 Error - Invalid Time Format

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid time format"
}
```

## PUT /delivery/customer-preferences/:preferenceId — Update Customer Preference

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-preferences#put-update-customer-preference)

Update an existing customer delivery preference. Customers can only update their own preferences, while admin users can update any customer's preferences. The customer_id cannot be changed.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/customer-preferences/:preferenceId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| preferenceId | path string | Yes | ID of the preference to update |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | string |  |  |  |
| preferred_day_of_week | number |  |  |  |
| preferred_time_start | string |  |  |  |
| preferred_time_end | string |  |  |  |
| delivery_instructions | string |  |  |  |
| delivery_options | object |  |  |  |
| delivery_options.signature_required | boolean |  |  |  |
| delivery_options.leave_with_neighbor | boolean |  |  |  |
| delivery_options.leave_at_door | boolean |  |  |  |
| contact_preferences | object |  |  |  |
| contact_preferences.notification_methods | array<object> |  |  |  |
| contact_preferences.notification_methods.type | string |  |  |  |
| contact_preferences.notification_methods.enabled | boolean |  |  |  |
| contact_preferences.notification_methods.contact | string |  |  |  |
| contact_preferences.notification_events | object |  |  |  |
| contact_preferences.notification_events.on_dispatch | boolean |  |  |  |
| contact_preferences.notification_events.day_before | boolean |  |  |  |
| contact_preferences.notification_events.day_of | boolean |  |  |  |
| contact_preferences.notification_events.hour_before | boolean |  |  |  |
| contact_preferences.notification_events.on_arrival | boolean |  |  |  |
| contact_preferences.notification_events.on_completion | boolean |  |  |  |



### Request body (raw)

```json
{
  "location_id": "uuid",
  "preferred_day_of_week": 2,
  "preferred_time_start": "10:00",
  "preferred_time_end": "18:00",
  "delivery_instructions": "Updated delivery instructions",
  "delivery_options": {
    "signature_required": true,
    "leave_with_neighbor": false,
    "leave_at_door": false
  },
  "contact_preferences": {
    "notification_methods": [
      {
        "type": "sms",
        "enabled": true,
        "contact": "+1234567890"
      },
      {
        "type": "email",
        "enabled": false,
        "contact": "customer@example.com"
      }
    ],
    "notification_events": {
      "on_dispatch": true,
      "day_before": true,
      "day_of": true,
      "hour_before": true,
      "on_arrival": true,
      "on_completion": true
    }
  }
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Preference updated successfully",
  "preference": {
    "id": "uuid",
    "tenant_id": "uuid",
    "customer_id": "uuid",
    "location_id": "uuid",
    "preferred_day_of_week": 2,
    "preferred_time_start": "10:00",
    "preferred_time_end": "18:00",
    "delivery_instructions": "Updated delivery instructions",
    "delivery_options": {
      "signature_required": true,
      "leave_with_neighbor": false,
      "leave_at_door": false
    },
    "contact_preferences": {
      "notification_methods": [
        {
          "type": "sms",
          "enabled": true,
          "contact": "+1234567890"
        },
        {
          "type": "email",
          "enabled": false,
          "contact": "customer@example.com"
        }
      ],
      "notification_events": {
        "on_dispatch": true,
        "day_before": true,
        "day_of": true,
        "hour_before": true,
        "on_arrival": true,
        "on_completion": true
      }
    },
    "created_at": "2023-08-01T12:00:00Z",
    "updated_at": "2023-08-02T14:30:00Z"
  }
}
```

### Response 404 Error - Preference Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Preference not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /delivery/customer-preferences/:preferenceId — Get Customer Preference

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-preferences#get-get-customer-preference)

Get a single customer delivery preference by ID. Customers can only view their own preferences, while admin users can view any preference.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/customer-preferences/:preferenceId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| preferenceId | path string | Yes | ID of the preference to retrieve |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Preference retrieved successfully",
  "preference": {
    "id": "uuid",
    "preferred_day_of_week": 1,
    "preferred_time_start": "09:00",
    "preferred_time_end": "17:00",
    "delivery_instructions": "Leave packages at the front door",
    "delivery_options": {
      "signature_required": false,
      "leave_with_neighbor": true,
      "leave_at_door": true
    },
    "contact_preferences": {
      "notification_methods": [
        {
          "type": "sms",
          "enabled": true,
          "contact": "+1234567890"
        }
      ]
    },
    "created_at": "2023-08-01T12:00:00Z",
    "updated_at": "2023-08-01T12:00:00Z",
    "customer": {
      "id": "uuid",
      "full_name": "John Doe"
    },
    "location": {
      "id": "uuid",
      "full_address": "123 Main St, City, State 12345"
    }
  }
}
```

### Response 404 Error - Preference Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Preference not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /delivery/zone-pricing — List Zone Pricing

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zone-pricing#get-list-zone-pricing)

List pricing rules with SQL-side filtering, sorting, exact pagination, and optional tenant KPI stats. Multi-select semantics are (origin zones OR origin tags) AND (destination zones OR destination tags) AND (services). Each row returns origin.preview, destination.preview, and services.preview with at most three active entities plus exact counts. Filter matches are prioritized in each preview and marked matched=true. services is the only service response field.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/zone-pricing?page=1&limit=10&active=true&include_stats=true&column_filters=%7B%22origin_zone_ids%22%3A%5B%22uuid-origin-1%22%2C%22uuid-origin-2%22%5D%2C%22service_ids%22%3A%5B%22uuid-service-1%22%5D%2C%22status%22%3A%22true%22%7D&sort_by=created_at&sort_order=desc&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of items per page | 10 |
| origin_zone_id | query string |  | Legacy single origin-zone filter. Prefer origin_zone_ids. |  |
| origin_zone_ids | query string |  | Comma-separated or repeated origin zone IDs. OR within origin filters. |  |
| destination_zone_id | query string |  | Legacy single destination-zone filter. Prefer destination_zone_ids. |  |
| destination_zone_ids | query string |  | Comma-separated or repeated destination zone IDs. OR within destination filters. |  |
| origin_tag_id | query string |  | Legacy single origin-tag filter. Prefer origin_tag_ids. |  |
| origin_tag_ids | query string |  | Comma-separated or repeated origin tag IDs. Combined with origin_zone_ids using OR. |  |
| destination_tag_id | query string |  | Legacy single destination-tag filter. Prefer destination_tag_ids. |  |
| destination_tag_ids | query string |  | Comma-separated or repeated destination tag IDs. Combined with destination_zone_ids using OR. |  |
| origin_selector_type | query string |  | Filter by origin selector type (zone\|tag) |  |
| destination_selector_type | query string |  | Filter by destination selector type (zone\|tag) |  |
| service_id | query string |  | Legacy single service filter. Prefer service_ids. |  |
| service_ids | query string |  | Comma-separated or repeated service IDs. A rule matches any selected service. |  |
| active | query string |  | Filter by active status (true/false) | true |
| include_stats | query string |  | Set false when KPI stats are not rendered. Rows and stats otherwise come from one database call. | true |
| column_filters | query string |  | JSON object for grid column filters. Multi-select arrays: origin_zone_ids, origin_tag_ids, destination_zone_ids, destination_tag_ids, service_ids. Also supports status, selector types, service_code, numeric ranges, and effective date ranges. Multi-select values are merged with equivalent top-level query parameters. | {"origin_zone_ids":["uuid-origin-1","uuid-origin-2"],"service_ids":["uuid-service-1"],"status":"true"} |
| sort_by | query string |  | Sort field. Allowed: base_price, per_unit_price, carrier_price, min_transit_time, max_transit_time, effective_from, effective_to, service.name, created_at | created_at |
| sort_order | query string |  | Sort direction (asc\|desc). Defaults to desc. | desc |
| tenant_id | query string |  | Tenant ID (defaults to authenticated user's tenant) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Zone pricing retrieved successfully",
  "zonePricing": [
    {
      "id": "pricing-rule-uuid",
      "active": true,
      "base_price": 1000,
      "per_unit_price": 90,
      "origin_selector_type": "zone",
      "destination_selector_type": "tag",
      "origin": {
        "type": "zone",
        "count": 7,
        "preview": [
          {
            "id": "origin-filtered",
            "name": "West Zone",
            "code": "WEST",
            "matched": true
          },
          {
            "id": "origin-2",
            "name": "Central",
            "code": "CTR",
            "matched": false
          },
          {
            "id": "origin-3",
            "name": "North",
            "code": "NTH",
            "matched": false
          }
        ]
      },
      "destination": {
        "type": "tag",
        "count": 4,
        "preview": [
          {
            "id": "tag-priority",
            "name": "priority",
            "code": null,
            "matched": true
          }
        ]
      },
      "services": {
        "count": 5,
        "preview": [
          {
            "id": "service-express",
            "code": "EXP",
            "name": "Express",
            "matched": true
          },
          {
            "id": "service-parcel",
            "code": "PAR",
            "name": "Parcel",
            "matched": false
          },
          {
            "id": "service-same-day",
            "code": "SD",
            "name": "Same Day",
            "matched": false
          }
        ]
      }
    }
  ],
  "stats": {
    "totalRules": 42,
    "activeRules": 30,
    "inactiveRules": 12,
    "recentlyAdded": []
  },
  "page": 1,
  "limit": 10,
  "total": 42
}
```

## GET /delivery/zone-pricing/:pricingId — Get Zone Pricing

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zone-pricing#get-get-zone-pricing)

Get details for a pricing rule. Response includes services[{id,name,code,unit_min,unit_max}] for edit-form initialization plus full selector arrays. services is the only service response field.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/zone-pricing/:pricingId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pricingId | path string | Yes | ID of the pricing rule to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Zone pricing retrieved successfully",
  "pricing": {
    "id": "uuid",
    "active": true,
    "base_price": 10.5,
    "per_unit_price": 0.75,
    "min_transit_time": 30,
    "max_transit_time": 60,
    "pricing_rules": {},
    "effective_from": "2023-01-01",
    "effective_to": null,
    "origin_selector_type": "zone",
    "destination_selector_type": "tag",
    "services": [
      {
        "id": "uuid-standard",
        "name": "Standard Delivery",
        "code": "STD",
        "unit_min": 1,
        "unit_max": 10
      },
      {
        "id": "uuid-express",
        "name": "Express Delivery",
        "code": "EXP",
        "unit_min": 1,
        "unit_max": 5
      }
    ],
    "origins": [
      {
        "id": "uuid-origin-1",
        "name": "Downtown",
        "code": "DT-01"
      }
    ],
    "destinations": [],
    "origin_tags": [],
    "destination_tags": [
      {
        "id": "uuid-tag-1",
        "name": "erbil"
      }
    ]
  }
}
```

### Response 404 Error - Pricing Rule Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Pricing rule not found"
}
```

## POST /delivery/zone-pricing — Create Zone Pricing

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zone-pricing#post-create-zone-pricing)

Create one pricing rule for one or more delivery services. service_ids is required and must contain at least one tenant service. A legacy service_id string is accepted as a one-item array. All services share the same selectors, prices, effective window, carrier price, transit times, and pricing_rules. The write is atomic; an overlap for any selected service rejects the entire request with HTTP 409. Unit tiers must cover every selected service's unit range. Frontend: submit all chips as service_ids and display the localized message for 400/404/409 responses.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/zone-pricing?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID (defaults to authenticated user's tenant) |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| origin_selector_type | string |  |  |  |
| destination_selector_type | string |  |  |  |
| origin_zone_ids | array<string> |  |  |  |
| destination_tag_ids | array<string> |  |  |  |
| service_ids | array<string> |  |  |  |
| base_price | number |  |  |  |
| per_unit_price | number |  |  |  |
| min_transit_time | number |  |  |  |
| max_transit_time | number |  |  |  |
| pricing_rules | object |  |  |  |
| effective_from | string |  |  |  |
| effective_to | null |  |  |  |



### Request body (raw)

```json
{
  "origin_selector_type": "zone",
  "destination_selector_type": "tag",
  "origin_zone_ids": [
    "uuid-origin-1"
  ],
  "destination_tag_ids": [
    "uuid-tag-erbil"
  ],
  "service_ids": [
    "uuid-service-standard",
    "uuid-service-express"
  ],
  "base_price": 10.5,
  "per_unit_price": 0.75,
  "min_transit_time": 30,
  "max_transit_time": 60,
  "pricing_rules": {},
  "effective_from": "2025-01-01",
  "effective_to": null
}
```

### Response 201 Success Response

Created

```json
{\n  "success": true,\n  "message": "Pricing rule created successfully",\n  "pricing_id": "pricing-rule-uuid"\n}
```

### Response 400 Error - Services Required

Bad Request

```json
{\n  "success": false,\n  "code": "service_ids_required",\n  "message": "Select at least one service"\n}
```

### Response 400 Error - Unit Tier Coverage

Bad Request

```json
{\n  "success": false,\n  "message": "Unit tiers do not completely cover the service range",\n  "service_id": "service_uuid_1",\n  "details": {\n    "type": "incomplete_coverage",\n    "tier_range": { "min_unit": 1, "max_unit": 5 },\n    "service_range": { "min_unit": 1, "max_unit": 10 }\n  }\n}
```

### Response 409 Error - Overlapping Rule

Conflict

```json
{\n  "success": false,\n  "code": "overlapping_pricing_rule_exists",\n  "message": "Overlapping pricing rule exists",\n  "existing_rule_ids": ["existing_rule_uuid_1"],\n  "conflicting_service_ids": ["service_uuid_1"]\n}
```

## PUT /delivery/zone-pricing/:pricingId — Update Zone Pricing

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zone-pricing#put-update-zone-pricing)

Update a pricing rule and its covered services atomically. When service_ids is provided, it becomes the complete active service set: added IDs are activated and omitted IDs are soft-deactivated. Omit both service_ids and service_id to preserve the current set. An explicit empty service_ids array is rejected. The legacy service_id field is accepted as a one-service replacement. Exact zone rules remain prioritized during runtime resolution. Frontend: initialize chips from pricing.services, submit the complete service_ids array, and use code plus localized message for errors.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/zone-pricing/:pricingId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pricingId | path string | Yes | ID of the pricing rule to update | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| origin_selector_type | string |  |  |  |
| destination_selector_type | string |  |  |  |
| origin_tag_ids | array<string> |  |  |  |
| destination_zone_ids | array<string> |  |  |  |
| service_ids | array<string> |  |  |  |
| base_price | number |  |  |  |
| per_unit_price | number |  |  |  |
| min_transit_time | number |  |  |  |
| max_transit_time | number |  |  |  |
| pricing_rules | object |  |  |  |
| effective_from | string |  |  |  |
| effective_to | null |  |  |  |



### Request body (raw)

```json
{
  "origin_selector_type": "tag",
  "destination_selector_type": "zone",
  "origin_tag_ids": [
    "uuid-tag-erbil"
  ],
  "destination_zone_ids": [
    "uuid-dest-1",
    "uuid-dest-2"
  ],
  "service_ids": [
    "uuid-service-standard",
    "uuid-service-express"
  ],
  "base_price": 12.5,
  "per_unit_price": 0.85,
  "min_transit_time": 24,
  "max_transit_time": 72,
  "pricing_rules": {},
  "effective_from": "2025-01-01",
  "effective_to": null
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": true,\n  "message": "Pricing rule updated successfully"\n}
```

### Response 404 Error - Pricing Rule Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "message": "Pricing rule not found"\n}
```

### Response 400 Error - Validation Failed (effective_dates)

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "message": "Validation failed",\n  "errors": [\n    {\n      "field": "effective_to",\n      "message": "effective_to_must_be_after_effective_from"\n    }\n  ]\n}
```

### Response 400 Error - Validation Failed (transit_times)

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "message": "Validation failed",\n  "errors": [\n    {\n      "field": "min_transit_time",\n      "message": "min_transit_time_must_be_less_than_max_transit_time"\n    }\n  ]\n}
```

### Response 404 Error - Origin Zone Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "message": "Origin zone not found"\n}
```

### Response 404 Error - Destination Zone Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "message": "Destination zone not found"\n}
```

### Response 404 Error - Service Type Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "message": "Service type not found"\n}
```

### Response 409 Error - Overlapping Rule

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "code": "overlapping_pricing_rule_exists",\n  "message": "Overlapping pricing rule exists",\n  "existing_rule_ids": ["existing_rule_uuid_1"],\n  "conflicting_service_ids": ["service_uuid_1"]\n}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "message": "Insufficient permissions"\n}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "message": "Tenant not found"\n}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{\n  "success": false,\n  "message": "Internal server error"\n}
```

## DELETE /delivery/zone-pricing/:pricingId — Delete Zone Pricing

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zone-pricing#delete-delete-zone-pricing)

Soft-delete a pricing rule by setting active=false. Pricing and relationship history are preserved, and the rule no longer participates in order pricing.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/zone-pricing/:pricingId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pricingId | path string | Yes | ID of the pricing rule to delete | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



## GET /delivery/drivers — List Drivers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#get-list-drivers)

Returns paginated drivers with a live `workload` object for assignment pickers. `workload.capacity` contains `total` (primary vehicle `capacity_unit`, or `null`), `filled` (authoritative `drivers.used_capacity_unit`), and `available` (`max(total - filled, 0)`, or `null` when total is unknown). `workload.orders` contains `assigned` (`pending`, `assigned`, or `accepted` assignments), `in_progress` (`en_route_to_pickup`, `picked_up`, or `en_route_to_delivery` assignments), and `active` (all distinct non-terminal assigned deliveries). Order counts exclude deleted, delivered, failed, cancelled, and returned deliveries even when a custody assignment remains for return handoff. Workload is informational only and does not change driver eligibility, capacity enforcement, zone rules, or assignment behavior. Frontend integration: fetch this endpoint fresh whenever an assignment driver picker opens, render the workload summary from the returned values, and continue using `drivers[].id` as the assignment `driver_id`.

Get a paginated list of drivers with optional filtering. Requires `view:tenant` permission or system admin privileges.

- Type: JWT Bearer Token
- Permissions: Requires `view:tenant` or user must be a system admin.

### Responses

Returns a list of drivers.

### Response Fields
- `success` (boolean): Indicates if the request was successful.
- `message` (string): Localized success message.
- `drivers` (array): List of driver objects.
  - `id` (string, UUID): Driver's unique ID.
  - `tenant_id` (string, UUID): Tenant ID.
  - `status` (string): Current status of the driver.
  - `created_at` (string, ISO8601 DateTime): Timestamp of creation.
  - `license_number` (string): Driver's license number.
  - `vehicle` (object): Assigned vehicle details.
    - `id` (string, UUID): Vehicle ID.
    - `name` (string): Vehicle name.
  - `zones` (array of objects): Assigned delivery zones.
    - `id` (string, UUID): Zone ID.
    - `name` (string): Zone name.
  - `user` (object): Associated user details.
    - `id` (string, UUID): User ID.
    - `full_name` (string): User's full name.
- `page` (integer): Current page number.
- `limit` (integer): Number of items per page.
- `total` (integer): Total number of drivers found.

If `tenant_id` is missing and not available in context.

Standard unauthorized response if token is missing or invalid.

User does not have the required permissions.

If there's an issue with the database query or an unexpected server error.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/drivers?page=1&limit=10&search=&status=&vehicle_id=&zone_id=&column_filters=%7B%22status%22%3A%22active%22%2C%22is_online%22%3Atrue%2C%22created_at%22%3A%7B%22from%22%3A%222025-01-01%22%2C%22to%22%3A%222025-12-31%22%7D%2C%22vehicle_id%22%3A%22uuid1%2Cuuid2%22%2C%22zone_id%22%3A%22uuid3%2Cuuid4%22%2C%22license_number%22%3A%22DL123%22%7D&sort_by=created_at&sort_order=desc&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{access_token}} | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | (integer, optional, default: 1): Page number for pagination. Example: `1` | 1 |
| limit | query string | No | (integer, optional, default: 10): Number of items per page. Example: `10` | 10 |
| search | query string | No | Search by driver full name, phone, email, or license number (string, optional): Search term to filter by driver's full name or license number. Example: `John` |  |
| status | query string | No | Filter by driver status (active, inactive, on_break, etc.) (string, optional): Filter by driver status (e.g., 'active', 'inactive', 'on_break'). Example: `active` |  |
| vehicle_id | query string | No | (string, UUID, optional): Filter by vehicle ID. Example: `a1b2c3d4-e5f6-7890-1234-567890abcdef` |  |
| zone_id | query string | No | (string, UUID, optional): Filter by delivery zone ID. Example: `b2c3d4e5-f6a7-8901-2345-67890abcdef0` |  |
| column_filters | query string |  | JSON string for column filters. Supported keys: status, is_online, created_at, vehicle_id, zone_id, license_number | {"status":"active","is_online":true,"created_at":{"from":"2025-01-01","to":"2025-12-31"},"vehicle_id":"uuid1,uuid2","zone_id":"uuid3,uuid4","license_number":"DL123"} |
| sort_by | query string |  | Sort column. Supported values: created_at, license_number | created_at |
| sort_order | query string |  | Sort order. Supported values: asc, desc | desc |
| tenant_id | query string | No | Tenant ID (optional if in auth token) (string, UUID, optional): Tenant ID. If not provided, it's taken from the authenticated user's context. Example: `c3d4e5f6-a7b8-9012-3456-7890abcdef01` |  |
| language | query string | No | (string, optional, default: 'en'): Preferred language for response messages. Example: `en` | en |



### Response 200 Live Driver Workload

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Drivers retrieved successfully",
  "drivers": [
    {
      "id": "driver-uuid-123",
      "users": {
        "full_name": "Alan Test Driver",
        "phone": "+9647500000000"
      },
      "operation_status": "available",
      "vehicle": {
        "id": "vehicle-uuid-789",
        "capacity_unit": 100
      },
      "workload": {
        "capacity": {
          "total": 100,
          "filled": 76,
          "available": 24
        },
        "orders": {
          "assigned": 3,
          "in_progress": 1,
          "active": 4
        }
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Drivers retrieved successfully",
  "drivers": [
    {
      "id": "driver-uuid-123",
      "tenant_id": "tenant-uuid-456",
      "user_id": "user-uuid-def",
      "status": "active",
      "created_at": "2025-10-01T00:00:00Z",
      "updated_at": "2025-10-02T00:00:00Z",
      "license_number": "DL12345678",
      "is_online": true,
      "all_zones": false,
      "preferred_zone_ids": [
        "zone-uuid-abc"
      ],
      "users": {
        "id": "user-uuid-def",
        "full_name": "John Driver",
        "first_name": "John",
        "last_name": "Driver",
        "avatar_url": null,
        "phone": "+1234567890",
        "email": "john.driver@example.com"
      },
      "vehicle": {
        "id": "vehicle-uuid-789",
        "name": "Van 101",
        "code": "VAN001",
        "image_url": null,
        "capacity_unit": 50,
        "description": null,
        "active": true,
        "vehicle_requirements": null,
        "is_primary": true,
        "sort_order": 0
      },
      "vehicles": [
        {
          "id": "vehicle-uuid-789",
          "name": "Van 101",
          "code": "VAN001",
          "image_url": null,
          "capacity_unit": 50,
          "description": null,
          "active": true,
          "vehicle_requirements": null,
          "is_primary": true,
          "sort_order": 0
        }
      ],
      "vehicle_count": 1,
      "primary_vehicle_id": "vehicle-uuid-789",
      "zones": [
        {
          "id": "zone-uuid-abc",
          "name": "Downtown"
        }
      ]
    }
  ],
  "stats": {
    "totalDrivers": 120,
    "activeDrivers": 95,
    "onlineDrivers": 30,
    "recentlyAdded": [
      {
        "id": "driver-uuid-999",
        "full_name": "John Doe",
        "avatar_url": null,
        "created_at": "2025-10-01T00:00:00Z"
      }
    ]
  },
  "page": 1,
  "limit": 10,
  "total": 25
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Drivers retrieved successfully",
  "drivers": [
    {
      "id": "driver-uuid-123",
      "tenant_id": "tenant-uuid-456",
      "status": "active",
      "created_at": "2023-10-26T10:00:00Z",
      "license_number": "XYZ12345",
      "vehicle": {
        "id": "vehicle-uuid-789",
        "name": "Van 101"
      },
      "zones": [
        {
          "id": "zone-uuid-abc",
          "name": "Downtown"
        }
      ],
      "user": {
        "id": "user-uuid-def",
        "full_name": "John Driver"
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 400 Error (400 Bad Request - Invalid Request)



```json
{
  "success": false,
  "message": "Invalid request"
}
```

### Response 401 Standard unauthorized response if token is missing or invalid.



```json
{
  "error": "Unauthorized",
  "message": "Authentication token is missing or invalid",
  "status": 401
}
```

### Response 403 Error (403 Forbidden - Insufficient Permissions)



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Internal server error",
  "error": {
    "message": "database error details"
  }
}
```

## GET /delivery/drivers/:driverId — Get Driver

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#get-get-driver)

Get details for a specific driver. Requires `view:tenant` or `manage:operations:tenant` permission or system admin privileges.

- Type: JWT Bearer Token
- Permissions: Requires `view:tenant`, `manage:operations:tenant`, or user must be a system admin.

### Responses

Returns details of the specified driver.

### Response Fields
- `success` (boolean): Indicates if the request was successful.
- `message` (string): Localized success message.
- `driver` (object): Driver details.
  - `id` (string, UUID): Driver's unique ID.
  - `user` (object): Associated user details.
    - `id` (string, UUID): User ID.
    - `full_name` (string): User's full name.
  - `vehicle` (object): Assigned vehicle details.
    - `id` (string, UUID): Vehicle ID.
    - `name` (string): Vehicle name.
  - `license_number` (string): Driver's license number.
  - `zones` (array of objects): Assigned delivery zones.
    - `id` (string, UUID): Zone ID.
    - `name` (string): Zone name.
  - `preferred_zones` (array of objects): Preferred delivery zones for auto-assignment priority. Contains zone details with id, name, and code. Empty array if no preferred zones are set.
    - `id` (string, UUID): Zone ID.
    - `name` (string): Zone name.
    - `code` (string): Zone code.
  - `driver_details` (object): Additional JSON details about the driver.
  - `status` (string): Current status of the driver.

If `tenant_id` is missing and not available in context.

Standard unauthorized response.

User does not have the required permissions.

If the driver with the specified ID does not exist or does not belong to the tenant.

If there's an issue with the database query or an unexpected server error.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/drivers/:driverId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{access_token}} | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes | ID of the driver to retrieve (string, UUID, required): The unique identifier of the driver to retrieve. Example: `driver-uuid-123` | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) (string, UUID, optional): Tenant ID. If not provided, it's taken from the authenticated user's context. Example: `c3d4e5f6-a7b8-9012-3456-7890abcdef01` |  |
| language | query string | No | (string, optional, default: 'en'): Preferred language for response messages. Example: `en` | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Driver retrieved successfully",
  "driver": {
    "driver_id": "driver-uuid-123",
    "license_number": "DL12345678",
    "status": "active",
    "driver_details": {},
    "documents": {
      "avatar": [
        "https://example.com/avatar.jpg"
      ],
      "identity_card_photos": [
        "https://example.com/id-front.jpg",
        "https://example.com/id-back.jpg"
      ],
      "driving_licence_photos": [
        "https://example.com/licence-front.jpg"
      ],
      "vehicle_photos": [
        "https://example.com/car-front.jpg",
        "https://example.com/car-side.jpg"
      ]
    },
    "is_online": true,
    "all_zones": false,
    "used_capacity_unit": 25,
    "current_location": {
      "latitude": 37.7749,
      "longitude": -122.4194,
      "last_update": "2023-08-01T15:30:00Z"
    },
    "user_id": "user-uuid-def",
    "email": "john.driver@example.com",
    "first_name": "John",
    "last_name": "Driver",
    "full_name": "John Driver",
    "phone": "+1234567890",
    "avatar_url": "https://example.com/avatar.jpg",
    "profile_data": {},
    "user_status": "active",
    "vehicle": {
      "id": "vehicle-uuid-789",
      "name": "Van 101",
      "code": "VAN001",
      "image_url": "https://example.com/van.jpg",
      "capacity_unit": 50
    },
    "delivery_zones": [
      {
        "id": "zone-uuid-abc",
        "name": "Downtown",
        "code": "DT"
      }
    ],
    "preferred_zones": [],
    "created_at": "2023-07-01T10:00:00Z",
    "updated_at": "2023-08-01T15:30:00Z"
  }
}
```

### Response 404 Error - Driver Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not found"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Driver retrieved successfully",
  "driver": {
    "id": "driver-uuid-123",
    "user": {
      "id": "user-uuid-def",
      "full_name": "John Driver"
    },
    "vehicle": {
      "id": "vehicle-uuid-789",
      "name": "Van 101"
    },
    "license_number": "XYZ12345",
    "zones": [
      {
        "id": "zone-uuid-abc",
        "name": "Downtown"
      }
    ],
    "preferred_zones": [
      {
        "id": "zone-uuid-abc",
        "name": "Downtown",
        "code": "DT"
      }
    ],
    "driver_details": {
      "rating": 4.5,
      "experience_years": 3
    },
    "status": "active"
  }
}
```

### Response 400 Error (400 Bad Request - Invalid Request)



```json
{
  "success": false,
  "message": "Invalid request"
}
```

### Response 401 Standard unauthorized response.



```json
{
  "error": "Unauthorized",
  "message": "Authentication token is missing or invalid",
  "status": 401
}
```

### Response 403 Error (403 Forbidden - Insufficient Permissions)



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /delivery/drivers — Create Driver

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#post-create-driver)

Create a new driver. Requires manage:users:tenant permission.

### Validation Rules
- If `all_zones` is `true`, `zone_ids` must not be provided
- If `all_zones` is `false`, `zone_ids` must be provided with at least one zone
- `preferred_zone_ids` must be a subset of `zone_ids` when `all_zones` is false
- `preferred_zone_ids` zones must exist in the delivery_zones table

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/drivers?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_id | string | Yes | (string, UUID, required): User ID to assign as driver |  |
| vehicle_ids | array<string> |  |  |  |
| primary_vehicle_id | string |  |  |  |
| license_number | string | No | (string, optional): Driver's license number |  |
| all_zones | boolean | No | (boolean, optional, default: false): If true, driver can operate in all zones. If false, driver can only operate in zones specified in `zone_ids` |  |
| zone_ids | array<string> |  | (array of UUIDs, required if `all_zones` is false): Array of delivery zone IDs the driver can operate in. Cannot be provided if `all_zones` is true |  |
| preferred_zone_ids | array<string> | No | (array of UUIDs, optional): Array of preferred delivery zone IDs for auto-assignment priority. Must be subset of `zone_ids` if `all_zones` is false. Empty array or omitted means no zone preference |  |
| status | string | No | (string, optional, default: "active"): Driver status |  |
| identity_card_photos | array<string> |  |  |  |
| driving_licence_photos | array<string> |  |  |  |
| vehicle_photos | array<string> |  |  |  |
| national_id | string | No | (string, optional): National ID (persisted in `drivers.national_id`) |  |
| bank_info | object | No | (object, optional): Bank fields — `bank_name`, `account_name`, `account_number`, `iban`, `swift_code`, `branch_name` (persisted in `drivers.bank_info` JSONB) |  |
| bank_info.bank_name | string |  |  |  |
| bank_info.account_name | string |  |  |  |
| bank_info.account_number | string |  |  |  |
| bank_info.iban | string |  |  |  |
| bank_info.swift_code | string |  |  |  |
| bank_info.branch_name | string |  |  |  |
| address | object | No | (object or string, optional): Structured address JSON (persisted in `drivers.address`); string values are normalized to `{ line1: "..." }` |  |
| address.line1 | string |  |  |  |
| address.city | string |  |  |  |
| address.country | string |  |  |  |
| driver_details | object | No | (object, optional): Additional driver-specific information (legacy bag; `bank_info`, `national_id`, `address` are stripped here and stored in DB columns when provided) |  |
| driver_details.rating | null |  |  |  |
| vehicle_id |  | Yes | (string, UUID, required): Vehicle ID to assign to driver |  |



### Request body (raw)

```json
{
  "user_id": "uuid",
  "vehicle_ids": [
    "vehicle-uuid-1",
    "vehicle-uuid-2"
  ],
  "primary_vehicle_id": "vehicle-uuid-1",
  "license_number": "DL12345678",
  "all_zones": false,
  "zone_ids": [
    "uuid1",
    "uuid2"
  ],
  "preferred_zone_ids": [
    "uuid1"
  ],
  "status": "active",
  "identity_card_photos": [
    "https://example.com/id-front.jpg",
    "https://example.com/id-back.jpg"
  ],
  "driving_licence_photos": [
    "https://example.com/licence-front.jpg"
  ],
  "vehicle_photos": [
    "https://example.com/car-front.jpg",
    "https://example.com/car-side.jpg"
  ],
  "national_id": "1234567890",
  "bank_info": {
    "bank_name": "Example Bank",
    "account_name": "John Driver",
    "account_number": "000111",
    "iban": "IQ00XXXX",
    "swift_code": "EXAMIQB0",
    "branch_name": "Main"
  },
  "address": {
    "line1": "Street 1",
    "city": "Baghdad",
    "country": "IQ"
  },
  "driver_details": {
    "rating": null
  }
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Driver created successfully",
  "id": "uuid"
}
```

## POST /delivery/drivers/wizard — Create Driver (Wizard)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#post-create-driver-wizard)

Create driver wizard. Always creates user from user object. Use `vehicle_ids` + `primary_vehicle_id` for existing vehicles, or use inline `vehicle` to create a new primary vehicle.

Optional profile fields (stored on `drivers`): `bank_info` (object), `national_id` (string), `address` (object or string). Same semantics as create driver.

### Frontend integration

on HTTP 409, display the response `message`. Phone conflicts return `This phone number is already registered to another account`; email conflicts return `This email is already registered to another account`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/drivers/wizard?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user | object |  |  |  |
| user.email | string |  |  |  |
| user.phone | string |  |  |  |
| user.password | string |  |  |  |
| user.first_name | string |  |  |  |
| user.last_name | string |  |  |  |
| vehicle_ids | array<string> |  |  |  |
| primary_vehicle_id | string |  |  |  |
| license_number | string |  |  |  |
| all_zones | boolean |  |  |  |
| zone_ids | array<string> |  |  |  |
| preferred_zone_ids | array<string> |  |  |  |
| status | string |  |  |  |
| national_id | string |  |  |  |
| bank_info | object |  |  |  |
| bank_info.bank_name | string |  |  |  |
| bank_info.iban | string |  |  |  |
| address | object |  |  |  |
| address.line1 | string |  |  |  |
| address.city | string |  |  |  |
| identity_card_photos | array<string> |  |  |  |
| driving_licence_photos | array<string> |  |  |  |
| vehicle_photos | array<string> |  |  |  |



### Request body (raw)

```json
{
  "user": {
    "email": "driver@example.com",
    "phone": "+1234567890",
    "password": "SecurePass123!",
    "first_name": "John",
    "last_name": "Driver"
  },
  "vehicle_ids": [
    "vehicle-uuid-1",
    "vehicle-uuid-2"
  ],
  "primary_vehicle_id": "vehicle-uuid-1",
  "license_number": "DL12345678",
  "all_zones": false,
  "zone_ids": [
    "uuid1",
    "uuid2"
  ],
  "preferred_zone_ids": [
    "uuid1"
  ],
  "status": "active",
  "national_id": "1234567890",
  "bank_info": {
    "bank_name": "Example Bank",
    "iban": "IQ00XXXX"
  },
  "address": {
    "line1": "Street 1",
    "city": "Baghdad"
  },
  "identity_card_photos": [
    "https://example.com/id-front.jpg",
    "https://example.com/id-back.jpg"
  ],
  "driving_licence_photos": [
    "https://example.com/licence-front.jpg"
  ],
  "vehicle_photos": [
    "https://example.com/car-front.jpg",
    "https://example.com/car-side.jpg"
  ]
}
```

### Response 201 Success

Created

```json
{
  "success": true,
  "message": "Driver created successfully",
  "driver": {
    "id": "uuid"
  }
}
```

### Response 409 Phone Already Registered

Conflict

```json
{
  "success": false,
  "message": "This phone number is already registered to another account"
}
```

### Response 409 Email Already Registered

Conflict

```json
{
  "success": false,
  "message": "This email is already registered to another account"
}
```

## PUT /delivery/drivers/:driverId — Update Driver

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#put-update-driver)

Update an existing driver with both user and driver information in a single API call. Optimized for driver mobile applications. Requires manage:operations:tenant permission.

- Type: JWT Bearer Token
- Permissions: Requires `manage:operations:tenant` or user must be a system admin.

### User Information (optional)
- `first_name` (string): Driver's first name
- `last_name` (string): Driver's last name
- `phone` (string): Driver's phone number
- `avatar_url` (string): Driver's profile picture URL
- `profile_data` (object): Additional user profile information

### Driver Information (optional)
- `vehicle_id` (string, UUID): Assign different vehicle to driver
- `vehicle_ids` (array of UUIDs): Full vehicle assignment list; omit all of `vehicle_id`, `vehicle_ids`, and `primary_vehicle_id` to update profile only without touching vehicle assignments
- `primary_vehicle_id` (string, UUID): Primary vehicle; must be included in `vehicle_ids` when both are sent
- `license_number` (string): Driver's license number
- `all_zones` (boolean): If true, driver can operate in all zones. If false, driver can only operate in zones specified in `zone_ids`. Cannot be set to true if `zone_ids` is provided
- `zone_ids` (array of UUIDs): Delivery zones for the driver. Required if `all_zones` is false. Cannot be provided if `all_zones` is true
- `preferred_zone_ids` (array of UUIDs): Preferred zones for auto-assignment. Omitted = unchanged; `[]` = clear. When the driver does **not** have all-zones access (see below), every id must belong to the driver’s **selected** zones: same-request `zone_ids` if provided, otherwise current `delivery_zone_drivers` assignments. When `all_zones` is true (from body or already on the driver if body omits it), subset check is skipped.
- `status` (string): Driver status (active, inactive, etc.)
- `driver_details` (object): Additional driver-specific information (nested `bank_info`, `national_id`, `address` are written to columns and stripped from stored JSON)
- `bank_info` (object): Bank fields → `drivers.bank_info`
- `national_id` (string): → `drivers.national_id`
- `address` (object or string): → `drivers.address` JSONB
- `current_latitude` (number): Current latitude position
- `current_longitude` (number): Current longitude position
- `is_online` (boolean): Driver's online status

### Validation Rules
- `preferred_zone_ids` must be tenant `delivery_zones` ids
- Subset rule: required only when effective `all_zones` is false (request `all_zones: true` overrides; if `all_zones` omitted, uses stored `drivers.all_zones`)
- Assigned set for subset: request `zone_ids` when present, else existing zone assignments
- When updating `zone_ids` while `all_zones` is false: **409** if the driver has an assigned delivery that is not terminal (`delivered` / `failed` / `cancelled` / `returned`) and has `pickup_zone_id` or `dropoff_zone_id` set to a zone **not** in the new `zone_ids` (legs with null zone id are ignored)

### Note

`used_capacity_unit` is read-only and managed automatically by the backend.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/drivers/:driverId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{access_token}} | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes | ID of the driver to update (string, UUID, required): The unique identifier of the driver to update. | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| first_name | string |  |  |  |
| last_name | string |  |  |  |
| phone | string |  |  |  |
| avatar_url | string |  |  |  |
| profile_data | object |  |  |  |
| profile_data.bio | string |  |  |  |
| vehicle_ids | array<string> |  |  |  |
| primary_vehicle_id | string |  |  |  |
| license_number | string |  |  |  |
| all_zones | boolean |  |  |  |
| zone_ids | array<string> |  |  |  |
| preferred_zone_ids | array<string> |  |  |  |
| status | string |  |  |  |
| national_id | string |  |  |  |
| bank_info | object |  |  |  |
| bank_info.bank_name | string |  |  |  |
| bank_info.account_number | string |  |  |  |
| address | object |  |  |  |
| address.line1 | string |  |  |  |
| address.city | string |  |  |  |
| identity_card_photos | array<string> |  |  |  |
| driving_licence_photos | array<string> |  |  |  |
| vehicle_photos | array<string> |  |  |  |
| driver_details | object |  |  |  |
| driver_details.rating | number |  |  |  |
| current_latitude | number |  |  |  |
| current_longitude | number |  |  |  |
| is_online | boolean |  |  |  |



### Request body (raw)

```json
{
  "first_name": "John",
  "last_name": "Smith",
  "phone": "+1234567890",
  "avatar_url": "https://example.com/new-avatar.jpg",
  "profile_data": {
    "bio": "Experienced driver with 5 years experience"
  },
  "vehicle_ids": [
    "new-vehicle-uuid",
    "backup-vehicle-uuid"
  ],
  "primary_vehicle_id": "new-vehicle-uuid",
  "license_number": "DL87654321",
  "all_zones": false,
  "zone_ids": [
    "zone-uuid-1",
    "zone-uuid-2"
  ],
  "preferred_zone_ids": [
    "zone-uuid-1"
  ],
  "status": "active",
  "national_id": "1234567890",
  "bank_info": {
    "bank_name": "Example Bank",
    "account_number": "000111"
  },
  "address": {
    "line1": "Street 1",
    "city": "Baghdad"
  },
  "identity_card_photos": [
    "https://example.com/id-front.jpg",
    "https://example.com/id-back.jpg"
  ],
  "driving_licence_photos": [
    "https://example.com/licence-front.jpg"
  ],
  "vehicle_photos": [
    "https://example.com/car-front.jpg"
  ],
  "driver_details": {
    "rating": 4.9
  },
  "current_latitude": 37.7849,
  "current_longitude": -122.4094,
  "is_online": true
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Driver updated successfully",
  "driver": {
    "driver_id": "driver-uuid-123",
    "license_number": "DL87654321",
    "status": "active",
    "driver_details": {},
    "documents": {
      "avatar": [
        "https://example.com/new-avatar.jpg"
      ],
      "identity_card_photos": [
        "https://example.com/id-front.jpg",
        "https://example.com/id-back.jpg"
      ],
      "driving_licence_photos": [
        "https://example.com/licence-front.jpg"
      ],
      "vehicle_photos": [
        "https://example.com/car-front.jpg"
      ]
    },
    "is_online": true,
    "all_zones": false,
    "preferred_zone_ids": [
      "zone-uuid-1"
    ],
    "used_capacity_unit": 30,
    "current_location": {
      "latitude": 37.7849,
      "longitude": -122.4094,
      "last_update": "2023-08-01T16:00:00Z"
    },
    "user_id": "user-uuid-def",
    "first_name": "John",
    "last_name": "Smith",
    "phone": "+1234567890",
    "avatar_url": "https://example.com/new-avatar.jpg",
    "profile_data": {},
    "vehicle": {
      "id": "new-vehicle-uuid",
      "name": "Truck 202",
      "code": "TRK002",
      "image_url": "https://example.com/truck.jpg",
      "capacity_unit": 100
    },
    "updated_at": "2023-08-01T16:00:00Z"
  }
}
```

### Response 409 Conflict - zone shrink blocked by active delivery

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot remove zone access while the driver has active deliveries in zones that would no longer be allowed"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## DELETE /delivery/drivers/:driverId — Delete Driver

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#delete-delete-driver)

Soft delete a driver (changes status to inactive). The driver can delete themselves, or this can be done by a user with manage:users:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/drivers/:driverId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes | ID of the driver to delete | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Driver deleted successfully"
}
```

## POST /delivery/drivers/:driverId/location — Update Driver Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#post-update-driver-location)

Update a driver's current location. The driver can update their own location, or this can be done by a user with manage:operations:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/drivers/:driverId/location?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes | ID of the driver whose location to update | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| latitude | number |  |  |  |
| longitude | number |  |  |  |



### Request body (raw)

```json
{
  "latitude": 37.7749,
  "longitude": -122.4194
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Driver location updated successfully",
  "timestamp": "2023-08-01T15:30:00Z",
  "driver": {
    "id": "uuid",
    "current_latitude": 37.7749,
    "current_longitude": -122.4194,
    "last_location_update": "2023-08-01T15:30:00Z"
  }
}
```

### Response 400 Error - Missing Location Data

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Both latitude and longitude are required"
}
```

## POST /delivery/driver/primary-vehicle — Switch Primary Vehicle (Driver)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#post-switch-primary-vehicle-driver)

Switch the logged-in driver's primary vehicle. The driver can only switch among vehicles already assigned to them. The switch will be rejected if the driver's current used capacity exceeds the target vehicle's capacity.

- Type: JWT Bearer Token
- The driver is resolved from the authenticated user; no `driverId` path parameter is required.

### Validation Rules
- `vehicle_id` is required and must be a string.
- The vehicle must be assigned to the current driver.
- The vehicle must be active and not soft-deleted.
- The driver's `used_capacity_unit` must be less than or equal to the vehicle's `capacity_unit`. If `used_capacity_unit` is greater, the request fails with a capacity error.

### Responses

Returns the updated driver profile with new primary vehicle information.

When the driver's used capacity is greater than the target vehicle's capacity.

If the vehicle is not assigned to the driver or does not exist.

If the authenticated user is not a driver in the tenant.

For unexpected server or database issues.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/driver/primary-vehicle?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{access_token}} | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| vehicle_id | string | Yes | (string, UUID, required): The ID of the vehicle to make primary. Must be already assigned to the driver. |  |



### Request body (raw)

```json
{
  "vehicle_id": "vehicle-uuid-1"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Driver updated successfully",
  "driver": {
    "driver_id": "driver-uuid-123",
    "primary_vehicle_id": "vehicle-uuid-1",
    "vehicle": {
      "id": "vehicle-uuid-1",
      "name": "Van 101",
      "capacity_unit": 50,
      "is_primary": true
    },
    "vehicles": [
      {
        "id": "vehicle-uuid-1",
        "name": "Van 101",
        "capacity_unit": 50,
        "is_primary": true
      },
      {
        "id": "vehicle-uuid-2",
        "name": "Small Car",
        "capacity_unit": 10,
        "is_primary": false
      }
    ],
    "used_capacity_unit": 20
  }
}
```

### Response 400 Error (400 Bad Request - Capacity Exceeded)



```json
{
  "success": false,
  "message": "Driver used capacity exceeds selected vehicle capacity"
}
```

### Response 404 Error (404 Not Found - Vehicle Not Assigned)



```json
{
  "success": false,
  "message": "Vehicle not found"
}
```

### Response 404 Error (404 Not Found - Driver Not Found)



```json
{
  "success": false,
  "message": "Driver not found"
}
```

### Response 500 Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /delivery/drivers/:driverId/wallet-transactions — Get Driver Wallet Transactions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#get-get-driver-wallet-transactions)

Get a driver's wallet transaction list with filters for search, date range, type, source, recorded actor, and payment method.

### Contract
- Returns only rows from `wallet_transactions` for the driver's wallet.
- Does not include `cod_collections` rows. Use `/delivery/cod-collections` or `/delivery/cod-summary` for COD cash collection/liability records.
- `id` is always the `wallet_transactions.id` value.
- Existing `source` remains the compatibility category: `Support` when the description contains support, otherwise `Order` for a linked delivery or `Support` for other rows.
- `transaction_source` is the literal stored metadata source, or `null` when unavailable.
- `recorded_by` is `{id, name}` when actor metadata exists, otherwise `null`. A saved actor-name snapshot is preferred; older rows without one use the current profile name of the saved actor ID. Missing names remain `null`; no actor is inferred from the delivery or wallet owner.
- `data.source_options` is the sorted distinct non-empty raw transaction sources from the full displayed wallet history (earning, transfer, payment, or support-description rows). Options are independent of pagination, date range, and other filters, and remain available when the filtered result is empty.
- `data.summary.current_balance` is the wallet's usable balance.
- `data.summary.reserved` is the total currently held for draft delivery fees. Reserved funds are not ledger transactions until captured.
- Currency is not returned; the frontend uses tenant configuration.
- `pagination.total`, `total_credited`, and `total_debited` use the same full filtered result before pagination. Filtering, totals, and pagination run in the database, so older matching rows are not limited to the first 1,000 wallet entries.

- `source` (optional category: `cod`, `order`, `support`; case-insensitive). `cod` is accepted for the existing Source dropdown and returns no matching rows while this wallet-only ledger contains only Order/Support categories; it never adds COD cash collection records.

### Errors

HTTP 400 for invalid parameters or missing tenant context, 404 for a driver outside the selected tenant or unavailable driver, and 500 for a failed data read. Response messages are localized.

### Frontend integration contract

Keep one existing Source column. When `transaction_source` is exactly `manual_credit` or `manual_debit`, display a single localized Manual label; otherwise preserve the existing category display from `source`. Keep the dropdown's COD/Order/Support options; add Manual credit and Manual debit only when their exact values are present in `data.source_options`. Existing category choices submit top-level `source`; manual choices submit top-level `transaction_source`. Clear the other source parameter when changing between category and manual choices. Do not add a separate Transaction Source column. Render Recorded By from `recorded_by.name` and send its text filter as `actor_name`. A legacy actor name may reflect the actor's current profile name rather than the name at recording time. Treat every item in `data.transactions` as a wallet transaction row. Display usable funds from `data.summary.current_balance`, reserved funds from `data.summary.reserved`, and currency from tenant configuration. Do not use this endpoint for COD collection selection, remittance, or COD cash liability display.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/drivers/:driverId/wallet-transactions?tenant_id=&page=1&limit=15&search=&from_date=&to_date=&type=&source=&transaction_source=&actor_name=&payment_method=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes |  | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | (required when tenant is not resolved from auth context) |  |
| page | query string | No | (optional, default 1) | 1 |
| limit | query string | No | (optional, default 15, max 100) | 15 |
| search | query string | No | (optional, matches description, driver/customer phone/name, reference, or delivery code) |  |
| from_date | query string | No | / `to_date` (optional, filters `wallet_transactions.created_at`) |  |
| to_date | query string |  |  |  |
| type | query string | No | (optional: `debtor`, `creditor`) |  |
| source | query string |  |  |  |
| transaction_source | query string | No | Exact saved value from data.source_options (optional: exact, case-sensitive saved source selected from `data.source_options`) |  |
| actor_name | query string | No | Literal case-insensitive substring of recorded_by.name (optional: case-insensitive literal substring of the returned `recorded_by.name`; wildcard and regex characters are ordinary text) are trimmed; empty values are ignored. |  |
| payment_method | query string | No | (optional: `cash`, `wallet`, `card`) |  |
| language | query string |  |  | en |



### Response 200 Success

OK

```json
{
  "success": true,
  "message": "Driver wallet transactions retrieved successfully",
  "data": {
    "transactions": [
      {
        "id": "wallet-transaction-uuid",
        "amount": 10000,
        "type": "debtor",
        "source": "Support",
        "payment_method": "Wallet",
        "short_description": "COD settlement by wallet",
        "driver_name": "Driver Name",
        "driver_phone": "+9647700000000",
        "commission_amount": null,
        "user": null,
        "created_at": "2026-06-25T10:00:00.000Z",
        "delivery_id": null,
        "delivery_code": null,
        "external_reference_id": null,
        "status": null,
        "transaction_source": "cod_remittance",
        "recorded_by": null
      },
      {
        "id": "manual-wallet-transaction-uuid",
        "amount": 500,
        "type": "creditor",
        "source": "Support",
        "payment_method": "Wallet",
        "short_description": "Manual credit: Performance bonus",
        "driver_name": "Driver Name",
        "driver_phone": "+9647700000000",
        "commission_amount": null,
        "user": null,
        "created_at": "2026-06-25T10:00:00.000Z",
        "delivery_id": null,
        "delivery_code": null,
        "external_reference_id": null,
        "status": null,
        "transaction_source": "manual_credit",
        "recorded_by": {
          "id": "actor-user-uuid",
          "name": "Finance User"
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 15,
      "total": 2
    },
    "summary": {
      "current_balance": 0,
      "reserved": 2500,
      "total_credited": 500,
      "total_debited": 10000
    },
    "source_options": [
      "cod_remittance",
      "manual_credit"
    ]
  }
}
```

## GET /delivery/drivers/:driverId/accounting — Get Driver Accounting

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#get-get-driver-accounting)

Get a driver's accounting summary for the selected period.

### Cash allocation rules
- `cash_collected.total` is the total physical cash collected by the driver in the selected period.
- `cash_collected.breakdown.cod` and `cash_collected.allocations.cod` are only the goods/COD portion from `cod_collections.allocations.cod`.
- `cash_collected.breakdown.order_fee` and `cash_collected.allocations.delivery_fee` are the delivery-fee cash portion from `cod_collections.allocations.delivery_fee`.
- `order_costs.breakdown.cash` and `order_costs.breakdown.wallet` remain total settled/remitted cash amounts; use `cash_cod`, `cash_delivery_fee`, `wallet_cod`, and `wallet_delivery_fee` when the UI needs the split.

### Frontend integration contract

Show `cash_collected.total` as total cash held/collected. Show COD from `cash_collected.allocations.cod` and delivery fee from `cash_collected.allocations.delivery_fee`; do not treat `cash_collected.total` as COD.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/drivers/:driverId/accounting?tenant_id=&from_date=&to_date=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes |  | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  |  |
| from_date | query string |  |  |  |
| to_date | query string |  |  |  |
| language | query string |  |  | en |



### Response 200 Success

OK

```json
{
  "success": true,
  "message": "Driver accounting retrieved successfully",
  "data": {
    "period": {
      "from_date": null,
      "to_date": null
    },
    "currency": "IQD",
    "balance_summary": {
      "amount": 0,
      "type": "creditor",
      "description": "Driver wallet is settled."
    },
    "cash_collected": {
      "total": 60,
      "breakdown": {
        "cod": 50,
        "order_fee": 10
      },
      "allocations": {
        "cod": 50,
        "delivery_fee": 10
      }
    },
    "earnings": {
      "total": 0,
      "breakdown": {
        "cash": 0,
        "online_credit": 0
      }
    },
    "order_costs": {
      "total": 60,
      "breakdown": {
        "cash": 60,
        "cash_cod": 50,
        "cash_delivery_fee": 10,
        "paid_by_receiver": 0,
        "card": 0,
        "wallet": 0,
        "wallet_cod": 0,
        "wallet_delivery_fee": 0
      }
    },
    "company_earnings": 0,
    "paid_withdrawals": 0,
    "from_support": {
      "cod_type_creditor": 0,
      "cod_type_debtor": 0,
      "support_type_creditor": 0,
      "support_type_debtor": 0
    }
  }
}
```

## GET /delivery/drivers/:driverId/operation-status-history — Get Driver Operation Status History

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/drivers#get-get-driver-operation-status-history)

Get paginated history of a driver's operation_status changes (available / on_leave / idle). Each record includes old_status, new_status, changed_by, and changed_at.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/drivers/:driverId/operation-status-history?tenant_id=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes |  | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  |  |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |



### Response 200 Success

OK

```json
{
  "success": true,
  "history": [
    {
      "id": "uuid",
      "driver_id": "uuid",
      "tenant_id": "uuid",
      "old_status": "available",
      "new_status": "on_leave",
      "changed_by": "uuid",
      "changed_at": "2026-04-27T12:00:00Z"
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1
}
```

## GET /delivery/vehicles — List Vehicles

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/vehicles#get-list-vehicles)

Get a paginated list of vehicles with optional column_filters, sort_by, sort_order and inline stats. Requires view:tenant or manage:operations:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/vehicles?page=1&limit=10&search=&sort_by=created_at&sort_order=desc&column_filters=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination (1-based) | 1 |
| limit | query string |  | Number of items per page | 10 |
| search | query string |  | Free-text search across name and code |  |
| sort_by | query string |  | Sort column. Allowed: name, code, created_at | created_at |
| sort_order | query string |  | asc or desc (default desc) | desc |
| column_filters | query string |  | JSON object with per-column filters. Keys: name (ILIKE), code (ILIKE), active (boolean), fuel_type (exact: petrol\|diesel\|electric\|hybrid\|cng), license_requirements (exact array element: standard\|commercial\|special), created_at ({from?, to?} ISO date range). Example: {"active":true,"fuel_type":"diesel","created_at":{"from":"2025-01-01","to":"2025-12-31"}} |  |
| active | query string |  | Legacy: filter by active status (true/false) |  |
| name | query string |  | Legacy: filter by vehicle name (ILIKE) |  |
| code | query string |  | Legacy: filter by vehicle code (ILIKE) |  |
| fuel_type | query string |  | Legacy: filter by fuel type (exact) |  |
| license_requirements | query string |  | Legacy: filter by license requirement (array element match) |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Vehicles retrieved successfully",
  "vehicles": [
    {
      "id": "uuid-1",
      "name": "Delivery Van - Medium",
      "code": "DVM-001",
      "capacity_unit": 3,
      "vehicle_requirements": {
        "license_requirements": [
          "standard"
        ],
        "special_features": [
          "lift_gate"
        ],
        "fuel_type": "diesel"
      },
      "image_url": "https://example.com/images/delivery-van.jpg",
      "active": true,
      "created_at": "2025-01-15T10:00:00Z"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 98,
  "stats": {
    "totalVehicles": 124,
    "activeVehicles": 98,
    "inactiveVehicles": 26,
    "recentlyAddedCount": 5
  }
}
```

### Response 400 Bad Request - invalid column_filters

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "column_filters",
  "message": "invalid_json"
}
```

## GET /delivery/vehicles/:vehicleId — Get Vehicle

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/vehicles#get-get-vehicle)

Get details for a specific vehicle. Requires view:tenant or manage:operations:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/vehicles/:vehicleId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| vehicleId | path string | Yes | ID of the vehicle to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Vehicle retrieved successfully",
  "vehicle": {
    "id": "uuid",
    "tenant_id": "uuid",
    "name": "Delivery Van - Medium",
    "code": "DVM-001",
    "description": "Standard medium-sized delivery van for urban deliveries",
    "capacity_unit": 3,
    "capacity_volume": null,
    "capacity_weight": null,
    "capacity_metrics": null,
    "vehicle_requirements": {
      "license_requirements": [
        "standard"
      ],
      "special_features": [
        "lift_gate"
      ],
      "fuel_type": "diesel"
    },
    "image_url": "https://example.com/images/delivery-van.jpg",
    "active": true,
    "created_at": "2023-07-01T10:00:00Z",
    "updated_at": "2023-08-01T15:30:00Z",
    "deleted_at": null
  }
}
```

## POST /delivery/vehicles — Create Vehicle

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/vehicles#post-create-vehicle)

Create a new vehicle type. Requires manage:operations:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/vehicles?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| code | string |  |  |  |
| description | string |  |  |  |
| capacity_unit | number |  |  |  |
| vehicle_requirements | object |  |  |  |
| vehicle_requirements.license_requirements | array<string> |  |  |  |
| vehicle_requirements.special_features | array<string> |  |  |  |
| vehicle_requirements.fuel_type | string |  |  |  |
| image_url | string |  |  |  |
| active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": "Delivery Van - Medium",
  "code": "DVM-001",
  "description": "Standard medium-sized delivery van for urban deliveries",
  "capacity_unit": 3,
  "vehicle_requirements": {
    "license_requirements": [
      "standard"
    ],
    "special_features": [
      "lift_gate"
    ],
    "fuel_type": "diesel"
  },
  "image_url": "https://example.com/images/delivery-van.jpg",
  "active": true
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Vehicle created successfully",
  "id": "uuid"
}
```

## PUT /delivery/vehicles/:vehicleId — Update Vehicle

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/vehicles#put-update-vehicle)

Update an existing vehicle. Only the fields provided in the request will be updated. Requires manage:operations:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/vehicles/:vehicleId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| vehicleId | path string | Yes | ID of the vehicle to update | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| description | string |  |  |  |
| capacity_unit | number |  |  |  |
| vehicle_requirements | object |  |  |  |
| vehicle_requirements.special_features | array<string> |  |  |  |
| vehicle_requirements.fuel_type | string |  |  |  |
| image_url | string |  |  |  |
| active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": "Delivery Van - Medium Plus",
  "description": "Updated medium-sized delivery van for urban deliveries",
  "capacity_unit": 5,
  "vehicle_requirements": {
    "special_features": [
      "lift_gate",
      "refrigeration"
    ],
    "fuel_type": "hybrid"
  },
  "image_url": "https://example.com/images/delivery-van-plus.jpg",
  "active": true
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Vehicle updated successfully"
}
```

## DELETE /delivery/vehicles/:vehicleId — Delete Vehicle

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/vehicles#delete-delete-vehicle)

Soft delete a vehicle (sets active = false). Requires manage:operations:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/vehicles/:vehicleId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| vehicleId | path string | Yes | ID of the vehicle to delete | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Vehicle deactivated successfully"
}
```

## GET /delivery/zones — List Zones

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zones#get-list-zones)

List delivery zones with pagination, advanced column filters, sorting, and tenant-wide stats. Requires valid authentication.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/zones?page=1&limit=10&is_active=true&inbound_enabled=&outbound_enabled=&external_integration=&name=&code=&search=&carrier_code=&sort_by=&sort_order=&column_filters=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (default: 10, max: 100) | 10 |
| is_active | query string | No | Filter by active status (true/false) | true |
| inbound_enabled | query string | No | Filter zones by inbound external integration status (true/false) Filter by inbound external integration status (true/false) |  |
| outbound_enabled | query string | No | Filter zones by outbound external integration status (true/false) Filter by outbound external integration status (true/false) |  |
| external_integration | query string | No | Filter zones by external integration enabled/disabled status (true/false) Filter by external integration enabled/disabled status (true/false) |  |
| name | query string | No | Filter zones by name (partial match) |  |
| code | query string | No | Filter zones by exact code match |  |
| search | query string | No | Search by zone name or code |  |
| carrier_code | query string | No | Filter zones by enabled external carrier code Filter by enabled external carrier code |  |
| sort_by | query string |  | Sort by one of: name, code, created_at, is_active, carrier_code One of `name`, `code`, `created_at`, `is_active`, `carrier_code` |  |
| sort_order | query string | No | Sort order: asc or desc. Defaults to desc when sort_by is provided `asc` or `desc` (defaults to `desc` when `sort_by` is provided) |  |
| column_filters | query string | No | JSON string with column filters. Supported keys: name, code, is_active, carrier_code, internal_enabled, inbound_enabled, outbound_enabled, external_integration, created_at JSON string with keys `name`, `code`, `is_active`, `carrier_code`, `internal_enabled`, `inbound_enabled`, `outbound_enabled`, `external_integration`, `created_at` |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) Tenant ID (defaults to authenticated user's tenant) |  |
| language | query string | No | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Zones retrieved successfully",
  "zones": [
    {
      "id": "uuid",
      "name": "Downtown",
      "code": "DT-01",
      "internal_enabled": true,
      "is_active": true,
      "created_at": "2023-01-01T00:00:00Z",
      "updated_at": "2023-01-02T00:00:00Z",
      "boundaries": {
        "type": "polygon",
        "coordinates": []
      },
      "zone_geom": null,
      "zone_metadata": {
        "population_density": "high"
      },
      "tags": [
        "erbil",
        "airport"
      ],
      "external_integration": {
        "carrier_code": "PRIME",
        "is_enabled": true,
        "inbound_enabled": true,
        "outbound_enabled": true,
        "warehouse_id": "uuid"
      }
    }
  ],
  "stats": {
    "totalZones": 5,
    "activeZones": 4,
    "externalZones": 2,
    "recentlyAdded": [
      {
        "name": "Airport Zone",
        "created_at": "2025-10-01T00:00:00Z"
      }
    ]
  },
  "page": 1,
  "limit": 10,
  "total": 5
}
```

### Response 500 Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /delivery/zones — Create Zone

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zones#post-create-zone)

Create a new delivery zone. Requires manage:operations:tenant permission.

- Requires valid authentication token with manage:operations:tenant permission

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/zones?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) Tenant ID (defaults to authenticated user's tenant) |  |
| language | query string | No | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| code | string |  |  |  |
| boundaries | object |  |  |  |
| boundaries.type | string |  |  |  |
| boundaries.coordinates | array |  |  |  |
| zone_metadata | object |  |  |  |
| zone_metadata.population_density | string |  |  |  |
| tags | array<string> |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": "Downtown",
  "code": "DT-01",
  "boundaries": {
    "type": "polygon",
    "coordinates": []
  },
  "zone_metadata": {
    "population_density": "high"
  },
  "tags": [
    "Erbil",
    "Airport"
  ],
  "is_active": true
}
```

### Request example 1

```json
{
  "name": "Downtown",
  "code": "DT-01",
  "boundaries": {
    "type": "polygon",
    "coordinates": []
  },
  "zone_metadata": {
    "population_density": "high"
  },
  "is_active": true
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Zone created successfully"
}
```

### Response 400 Error (400 Bad Request)



```json
{
  "success": false,
  "message": "Zone code already exists"
}
```

## GET /delivery/zones/:zoneId — Get Zone

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zones#get-get-zone)

Get details of a specific delivery zone. Requires view:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/zones/:zoneId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| zoneId | path string | Yes | ID of the zone to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) Tenant ID (defaults to authenticated user's tenant) |  |
| language | query string | No | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Zone retrieved successfully",
  "zone": {
    "id": "uuid",
    "tenant_id": "uuid",
    "name": "Downtown",
    "code": "DT-01",
    "boundaries": {
      "type": "polygon",
      "coordinates": []
    },
    "zone_metadata": {
      "population_density": "high"
    },
    "tags": [
      "erbil",
      "airport"
    ],
    "is_active": true,
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-01-02T00:00:00Z"
  }
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Zone retrieved successfully",
  "zone": {
    "id": "uuid",
    "tenant_id": "uuid",
    "name": "Downtown",
    "code": "DT-01",
    "boundaries": {
      "type": "polygon",
      "coordinates": []
    },
    "zone_metadata": {
      "population_density": "high"
    },
    "is_active": true,
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-01-02T00:00:00Z"
  }
}
```

### Response 404 Error (404 Not Found)



```json
{
  "success": false,
  "message": "Zone not found"
}
```

## PUT /delivery/zones/:zoneId — Update Zone

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zones#put-update-zone)

Update an existing delivery zone. Requires manage:operations:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/zones/:zoneId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| zoneId | path string | Yes | ID of the zone to update | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| code | string |  |  |  |
| boundaries | object |  |  |  |
| boundaries.type | string |  |  |  |
| boundaries.coordinates | array |  |  |  |
| zone_metadata | object |  |  |  |
| zone_metadata.population_density | string |  |  |  |
| tags | array<string> |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": "Downtown Updated",
  "code": "DT-01-UPD",
  "boundaries": {
    "type": "polygon",
    "coordinates": []
  },
  "zone_metadata": {
    "population_density": "medium"
  },
  "tags": [
    "Erbil",
    "Out Of City"
  ],
  "is_active": true
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Zone updated successfully",
  "zone": {
    "id": "uuid",
    "name": "Downtown Updated",
    "tags": [
      "erbil",
      "out of city"
    ],
    "updated_at": "2023-01-02T00:00:00Z"
  }
}
```

## DELETE /delivery/zones/:zoneId — Delete Zone

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zones#delete-delete-zone)

### Hard-delete a delivery zone after checks

blocks if any non-terminal delivery (status not delivered/failed/cancelled/returned) references the zone as pickup or dropoff zone, or if the zone appears in zone pricing origin/destination rules. Clears the zone id from all drivers' preferred_zone_ids for the tenant. Requires manage:operations:tenant permission. Apply migration `20260331130000_zone_delete_driver_preferences_and_delivery_zone_fk` so `remove_zone_from_tenant_driver_preferences` exists and historical deliveries can release FKs (SET NULL).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/zones/:zoneId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| zoneId | path string | Yes | ID of the zone to delete | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Zone deleted successfully",
  "data": null
}
```

### Response 409 Conflict — active deliveries

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This zone cannot be deleted because it is linked to deliveries that are still in progress",
  "data": null
}
```

### Response 409 Conflict — pricing rules

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This zone cannot be deleted because it is used in zone pricing rules. Remove or update those rules first",
  "data": null
}
```

## GET /delivery/zone-tags — List Zone Tags

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zones#get-list-zone-tags)

List tenant zone tags for tag autocomplete.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/zone-tags?page=1&limit=20&search=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| search | query string |  |  |  |
| tenant_id | query string |  |  |  |
| language | query string |  |  | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Zone tags retrieved successfully",
  "tags": [
    {
      "id": "uuid-tag-1",
      "name": "erbil"
    },
    {
      "id": "uuid-tag-2",
      "name": "airport"
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 2
}
```

## GET /delivery/zones/:zoneId/pricing — Show Zone Pricing (Perspective)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/zones#get-show-zone-pricing-perspective)

Show all pricing rules related to this zone. Each row is projected from the zone perspective: the matched side contains only the main zone, the counterparty side contains all zones/tags, and rule.services contains every active service covered by the rule. service_id filters by membership in that array. rule.services is the only service response field.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/zones/:zoneId/pricing?page=1&limit=50&service_id=&active=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| zoneId | path string | Yes | ID of the zone to show pricing for | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 50 |
| service_id | query string |  |  |  |
| active | query string |  |  |  |
| tenant_id | query string |  |  |  |
| language | query string |  |  | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Zone Pricing Retrieved Successfully",
  "zone": {
    "id": "uuid",
    "name": "Downtown",
    "code": "DT-01"
  },
  "pricing": [
    {
      "rule": {
        "id": "uuid",
        "active": true,
        "base_price": 10.5,
        "per_unit_price": 0.75,
        "effective_from": "2025-01-01",
        "effective_to": null,
        "origin_selector_type": "zone",
        "destination_selector_type": "tag",
        "origin_zones_count": 2,
        "destination_tags_count": 1,
        "services": [
          {
            "id": "uuid-standard",
            "name": "Standard Delivery",
            "code": "STD"
          },
          {
            "id": "uuid-express",
            "name": "Express Delivery",
            "code": "EXP"
          }
        ]
      },
      "match": {
        "matched_side": "origin",
        "matched_by": "zone",
        "matched_tags": []
      },
      "origin": {
        "selector_type": "zone",
        "zones": [
          {
            "id": "uuid",
            "name": "Downtown",
            "code": "DT-01"
          }
        ],
        "tags": []
      },
      "destination": {
        "selector_type": "tag",
        "zones": [],
        "tags": [
          {
            "id": "uuid",
            "name": "erbil"
          }
        ]
      }
    }
  ],
  "page": 1,
  "limit": 50,
  "total": 1
}
```

## GET /delivery/cod-collections — List COD Collections

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/cod-collections#get-list-cod-collections)

List physical cash collections with pagination, search, sorting, and column filters.

Drivers always see only their own collections; any supplied `driver_id` is ignored for driver callers. Dispatcher and tenant_admin roles see all tenant collections and may filter by `driver_id`. Authorization for this scope is role-only and does not use permissions. Other roles receive HTTP 403.

`amount` is the total cash held by the driver. `allocations.cod` / `cod_amount` is the goods/COD portion and `allocations.delivery_fee` / `delivery_fee_amount` is the delivery-fee portion. `collection_stage` is `pickup` or `dropoff` for new records.

### Cash record rules
- Sender pays delivery fee in cash: create a pickup record with `{cod: 0, delivery_fee}`.
- Receiver pays COD while sender paid the fee: create a separate dropoff record with `{cod, delivery_fee: 0}`.
- Receiver pays both COD and delivery fee in cash: create one dropoff record with `{cod, delivery_fee}`.

### Frontend integration contract

Display `amount` as the amount held/remitted. Display `cod_amount` and `delivery_fee_amount` as first-class numeric columns and keep `allocations` for the original breakdown object. Use `collection_stage` and `allocations` for the collection point and breakdown. Status values remain `collected`, `remitted`, and `settled_by_wallet`.

### Search example

`?search=chirag` matches rows where driver name, customer name, email, or address contains 'chirag'

### Sort example

`?sort_by=amount&sort_order=desc`

### Column Filters (`column_filters` as JSON)

Per-column filter object. Takes priority over direct query params for the same field when direct params are absent.

| Key | Type | Behaviour |
|-----|------|----------|
| `customer_name` | string | ILIKE match |
| `driver_name` | string | ILIKE match |
| `delivery_address` | string | ILIKE match |
| `external_reference_id` | string | ILIKE match |
| `status` | string | Exact match (`collected` \| `remitted` \| `settled_by_wallet`) |
| `delivery_code` | number or `{min,max}` | Exact or range |
| `amount` | number or `{min,max}` | Exact cash amount or range |
| `cod_amount` | number or `{min,max}` | Exact COD allocation or range |
| `delivery_fee_amount` | number or `{min,max}` | Exact delivery-fee allocation or range |
| `collected_at` | `{from,to}` | Date range (day boundaries) |
| `remitted_at` | `{from,to}` | Date range (day boundaries) |
| `settled_at` | `{from,to}` | Date range (day boundaries) |

### Example

`?column_filters={"customer_name":"john","cod_amount":{"min":50},"delivery_fee_amount":{"max":20},"collected_at":{"from":"2024-01-01","to":"2024-01-31"}}`

### Summary fields
- `total_collected` — sum of `collected` status amounts
- `total_remitted` — sum of `remitted` status amounts
- `total_settled_by_wallet` — sum of `settled_by_wallet` status amounts
- `total_pending` — same as `total_collected` (unremitted)
- `total_cod_collections` — total count (ignores status filter)

Each row also returns `settlement_payment_id` (UUID or null). Rows settled after receipt support point to one saved batch; use this ID to reopen the receipt endpoint. Historical settled rows remain null until first print. Show Print Receipt for remitted/settled_by_wallet rows; POST /delivery/cod-collections/:id/receipt creates a receipt for that collection on demand. Do not infer historical batches from dates.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/cod-collections?page=1&limit=20&search=&sort_by=collected_at&sort_order=desc&status=&driver_id=&delivery_id=&delivery_code=&amount_min=&amount_max=&cod_amount_min=&cod_amount_max=&delivery_fee_amount_min=&delivery_fee_amount_max=&collected_at_from=&collected_at_to=&remitted_at_from=&remitted_at_to=&settled_at_from=&settled_at_to=&column_filters=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 20, max: 100) | 20 |
| search | query string |  | Case-insensitive partial match across delivery code, customer name, driver name, driver email, delivery address |  |
| sort_by | query string |  | Sort column: delivery_code \| amount \| status \| collected_at \| remitted_at \| settled_at \| customer_name \| driver_name (default: collected_at) | collected_at |
| sort_order | query string |  | Sort direction: asc \| desc (default: desc) | desc |
| status | query string |  | Filter by status: collected \| remitted \| settled_by_wallet |  |
| driver_id | query string |  | Filter by driver ID (Dispatcher and tenant_admin roles only; ignored for driver callers) |  |
| delivery_id | query string |  | Filter by delivery ID |  |
| delivery_code | query string |  | Filter by exact delivery code |  |
| amount_min | query string |  | Filter amount >= value |  |
| amount_max | query string |  | Filter amount <= value |  |
| cod_amount_min | query string |  | Filter allocations.cod >= value |  |
| cod_amount_max | query string |  | Filter allocations.cod <= value |  |
| delivery_fee_amount_min | query string |  | Filter allocations.delivery_fee >= value |  |
| delivery_fee_amount_max | query string |  | Filter allocations.delivery_fee <= value |  |
| collected_at_from | query string |  | Filter collected_at >= date (ISO, normalized to day start). Alias: from_date |  |
| collected_at_to | query string |  | Filter collected_at <= date (ISO, normalized to day end). Alias: to_date |  |
| remitted_at_from | query string |  | Filter remitted_at >= date (ISO) |  |
| remitted_at_to | query string |  | Filter remitted_at <= date (ISO) |  |
| settled_at_from | query string |  | Filter settled_at >= date (ISO) |  |
| settled_at_to | query string |  | Filter settled_at <= date (ISO) |  |
| column_filters | query string |  | JSON object for per-column filtering. Supported keys: customer_name (ilike), driver_name (ilike), delivery_address (ilike), external_reference_id (ilike), status (exact string), delivery_code (number or {min,max}), amount (number or {min,max}), cod_amount (number or {min,max}), delivery_fee_amount (number or {min,max}), collected_at ({from,to} date range), remitted_at ({from,to} date range), settled_at ({from,to} date range). Example: {"customer_name":"john","cod_amount":{"min":50},"delivery_fee_amount":{"max":20},"collected_at":{"from":"2024-01-01","to":"2024-01-31"}} |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Response language | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "COD collections retrieved successfully",
  "cod": {
    "collections": [
      {
        "id": "uuid",
        "delivery_id": "uuid",
        "driver_id": "uuid",
        "amount": 60,
        "allocations": {
          "cod": 50,
          "delivery_fee": 10
        },
        "cod_amount": 50,
        "delivery_fee_amount": 10,
        "status": "collected",
        "collected_at": "2023-01-01T10:00:00Z",
        "remitted_at": null,
        "settled_at": null,
        "delivery_code": 1001,
        "delivery_details": {
          "delivery_number": "DEL-001",
          "customer_name": "John Doe",
          "delivery_address": "456 Oak Ave, City"
        },
        "driver_info": {
          "full_name": "Driver Name",
          "email": "driver@email.com",
          "avatar": "https://avatar.url"
        },
        "settlement_payment_id": null
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 15
    },
    "summary": {
      "total_collected": 500,
      "total_remitted": 300,
      "total_pending": 200,
      "total_cod_collections": 25
    }
  }
}
```

## GET /delivery/cod-summary — Get COD Summary

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/cod-collections#get-get-cod-summary)

Get driver's COD summary for wallet dashboard display. Shows unremitted amounts and pending collection counts.

- Drivers always see only their own summary; any supplied `driver_id` is ignored for driver callers
- Dispatcher and tenant_admin roles can fetch a specific driver's summary only when that driver belongs to their tenant
- Other roles receive HTTP 403

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/cod-summary?driver_id=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | query string | No | Same-tenant driver ID to fetch summary for (Dispatcher and tenant_admin roles only; ignored for driver callers) |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) Tenant ID (defaults to authenticated user's tenant) |  |
| language | query string | No | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "summary": {
    "total_unremitted": 200,
    "pending_collections_count": 5,
    "oldest_unremitted_date": "2023-01-01T10:00:00Z"
  }
}
```

### Response 404 Error (404 Not Found)



```json
{
  "success": false,
  "message": "Driver not found"
}
```

## POST /delivery/cod-collections/remit — Bulk Remit COD Collections

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/cod-collections#post-bulk-remit-cod-collections)

Mark physical cash collections as remitted after a dispatcher receives cash from the driver.

### Atomic business rules
- COD deliveries credit the delivery customer/merchant wallet at delivery completion and mark the `COD_COLLECTION` payment `SUCCESS` immediately.
- Cash remittance only marks each collection `remitted` and records who received the driver cash. It does not credit the customer/merchant wallet.
- The delivery-fee allocation is not moved to a wallet in cash-remittance mode.
- If any wallet/payment/collection update fails, the whole remittance fails.

### Frontend integration contract

For selected rows, submit only `scope=selected` and `cod_collection_ids`; do not send `driver_id`. For remit-all, submit `scope=all` and `driver_id` when the actor is dispatcher/admin. Show `total_cash_remitted`, `total_cod_remitted`, and `total_delivery_fee_remitted` separately. Handle HTTP 400 when a selection is mixed-driver, invalid, no longer pending, or missing the required driver for remit-all.

### Persistent settlement receipt

The settlement and one `DELIVERY` / `COD_SETTLEMENT` payment are saved atomically. The payment snapshots the driver, actor/receiver, timestamp, reference, exact settled collections, per-order COD/delivery-fee breakdown, and totals. The PDF is generated asynchronously; PDF failure never repeats or reverses settlement. A successful response adds `settlement_payment_id`, `receipt_status` (`pending`, `in_process`, `success`, or `failure`), and `currency`. The returned date is the persisted settlement date. Cash currency uses the tenant currency setting; wallet currency is the actual driver wallet currency.

Send one stable `Idempotency-Key` (up to 200 characters) per submission and reuse it for network retries. Its receipt identity is durable. Replaying the same request returns the original batch even for `scope=all`; reusing the same key with changed input returns HTTP 409 and code `settlement_idempotency_conflict`. A new intended settlement requires a new key.

### Receipt integration

Open `GET /delivery/cod-settlements/:settlement_payment_id/receipt`. HTTP 202 means pending/in_process; poll briefly or reopen later. HTTP 200 supplies a fresh signed `url`. HTTP 409 with `settlement_receipt_failed` means settlement succeeded but PDF failed; retry only `POST /delivery/cod-settlements/:settlement_payment_id/receipt`, never resubmit settlement to print. For history rows with `settlement_payment_id=null`, first print uses POST /delivery/cod-collections/:id/receipt to save a receipt for that collection only.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/cod-collections/remit?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| scope | string |  | `selected` or `all`. |  |
| cod_collection_ids | array<string> |  | required when `scope=selected`. The backend derives the driver from these rows and rejects mixed-driver selections. |  |
| remitted_to_user_id | string | No | optional receiver/dispatcher user ID; defaults to the current user. |  |
| notes | string |  |  |  |
| reference | string |  |  |  |
| driver_id |  |  | required only for dispatcher/admin `scope=all`. Driver users settle their own rows from auth context. |  |



### Request body (raw)

```json
{
  "scope": "selected",
  "cod_collection_ids": [
    "uuid1",
    "uuid2",
    "uuid3"
  ],
  "remitted_to_user_id": "dispatcher-uuid",
  "notes": "Weekly remittance batch",
  "reference": "REM-REF-001"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "scope": "selected",
    "remitted_collections": [
      {
        "cod_collection_id": "uuid1",
        "amount": 60,
        "allocations": {
          "cod": 50,
          "delivery_fee": 10
        },
        "delivery_code": 1001,
        "delivery_number": "DEL-001"
      }
    ],
    "total_remitted": 60,
    "total_cash_remitted": 60,
    "total_cod_remitted": 50,
    "total_delivery_fee_remitted": 10,
    "remitted_count": 1,
    "failed_count": 0,
    "remitted_at": "2023-01-01T12:00:00Z",
    "settlement_payment_id": "91000000-0000-4000-8000-000000000001",
    "receipt_status": "pending",
    "currency": "IQD"
  }
}
```

## POST /delivery/cod-collections/settle-by-wallet — Settle COD by Wallet

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/cod-collections#post-settle-cod-by-wallet)

Settle cash collections using the driver's wallet.

### Atomic business rules
- Debit the driver wallet by total collection `amount`.
- COD deliveries already credit customer/merchant wallets at delivery completion. Settlement does not credit the customer/merchant wallet.
- Credit the tenant wallet with `allocations.delivery_fee`.
- Mark collections `settled_by_wallet`.

### Frontend integration contract

For selected rows, submit only `scope=selected` and `cod_collection_ids`; do not send `driver_id`. For settle-all, submit `scope=all` and `driver_id` when the actor is dispatcher/admin. Show `total_cash_settled`, `total_cod_settled`, and `total_delivery_fee_settled` separately. Handle HTTP 400 for insufficient balance, mixed-driver selections, invalid collections, missing driver for settle-all, or amount mismatch.

### Persistent settlement receipt

The settlement and one `DELIVERY` / `COD_SETTLEMENT` payment are saved atomically. The payment snapshots the driver, actor/receiver, timestamp, reference, exact settled collections, per-order COD/delivery-fee breakdown, and totals. The PDF is generated asynchronously; PDF failure never repeats or reverses settlement. A successful response adds `settlement_payment_id`, `receipt_status` (`pending`, `in_process`, `success`, or `failure`), and `currency`. The returned date is the persisted settlement date. Cash currency uses the tenant currency setting; wallet currency is the actual driver wallet currency.

Send one stable `Idempotency-Key` (up to 200 characters) per submission and reuse it for network retries. Its receipt identity is durable. Replaying the same request returns the original batch even for `scope=all`; reusing the same key with changed input returns HTTP 409 and code `settlement_idempotency_conflict`. A new intended settlement requires a new key.

### Receipt integration

Open `GET /delivery/cod-settlements/:settlement_payment_id/receipt`. HTTP 202 means pending/in_process; poll briefly or reopen later. HTTP 200 supplies a fresh signed `url`. HTTP 409 with `settlement_receipt_failed` means settlement succeeded but PDF failed; retry only `POST /delivery/cod-settlements/:settlement_payment_id/receipt`, never resubmit settlement to print. For history rows with `settlement_payment_id=null`, first print uses POST /delivery/cod-collections/:id/receipt to save a receipt for that collection only.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/cod-collections/settle-by-wallet?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| scope | string |  | `selected` or `all`. |  |
| notes | string |  |  |  |
| reference | string |  |  |  |
| driver_id | string |  | required only for dispatcher/admin `scope=all`. Driver users settle their own rows from auth context. |  |
| cod_collection_ids |  |  | required when `scope=selected`. The backend derives the driver from these rows and rejects mixed-driver selections. |  |
| settlement_amount |  | No | optional guard amount; when supplied it must match the selected/all cash total. |  |



### Request body (raw)

```json
{
  "scope": "all",
  "notes": "Settled all collected CODs using wallet balance",
  "reference": "INV-2024-001",
  "driver_id": "driver-uuid"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "COD collections settled successfully",
  "settlement": {
    "scope": "all",
    "settled_collections": [
      {
        "cod_collection_id": "uuid1",
        "amount": 60,
        "allocations": {
          "cod": 50,
          "delivery_fee": 10
        },
        "delivery_code": 1001,
        "delivery_number": "DEL-001"
      }
    ],
    "total_settled": 60,
    "total_cash_settled": 60,
    "total_cod_settled": 50,
    "total_delivery_fee_settled": 10,
    "settled_at": "2023-01-01T12:00:00Z",
    "settlement_payment_id": "91000000-0000-4000-8000-000000000001",
    "receipt_status": "pending",
    "currency": "USD"
  }
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "COD collections settled successfully",
  "settlement": {
    "scope": "selected",
    "settled_collections": [
      {
        "cod_collection_id": "uuid1",
        "amount": 60,
        "allocations": {
          "cod": 50,
          "delivery_fee": 10
        },
        "delivery_code": 1001,
        "delivery_number": "DEL-001"
      }
    ],
    "total_settled": 60,
    "total_cash_settled": 60,
    "total_cod_settled": 50,
    "total_delivery_fee_settled": 10,
    "settled_at": "2023-01-01T12:00:00Z",
    "settlement_payment_id": "91000000-0000-4000-8000-000000000001",
    "receipt_status": "pending",
    "currency": "USD"
  }
}
```

## POST /delivery/cod-collections/:codId/remit — Remit Single COD Collection

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/cod-collections#post-remit-single-cod-collection)

Remit a single COD collection. Requires admin permissions.

- Requires manage:operations:tenant permission or system admin

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/cod-collections/:codId/remit?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| codId | path string | Yes | ID of the COD collection to remit | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) Tenant ID (defaults to authenticated user's tenant) |  |
| language | query string | No | Preferred language for response messages | en |



### Request body (raw)

```json
{}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "COD collection remitted successfully",
  "data": {
    "id": "uuid",
    "delivery_id": "uuid",
    "driver_id": "uuid",
    "amount": "50.00",
    "delivery_code": 1001,
    "collected_at": "2023-01-01T10:00:00Z",
    "remitted_at": "2023-01-01T12:00:00Z",
    "status": "remitted"
  }
}
```

### Response 403 Error (403 Forbidden)



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 404 Error (404 Not Found)



```json
{
  "success": false,
  "message": "COD collection not found"
}
```

## GET /delivery/cod-settlements/:payment_id/receipt — Get COD Settlement Receipt

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/cod-collections#get-get-cod-settlement-receipt)

Access the saved settlement batch receipt. Requires authentication. Dispatcher/tenant_admin roles or manage:operations:tenant may access their tenant's settlements; drivers may access only their own. Other roles receive 403; unknown/out-of-tenant/out-of-driver scope returns 404. `payment_id` is the settlement_payment_id returned by either settlement action or a COD collection row.

GET reads status and signs an existing PDF. POST returns the saved PDF when available, reuses an active job, or retries missing/failed PDF generation; it never changes cash, wallet balances, collection status, batch identity, or immutable receipt amounts. No request body is required.

### Responses
- 200: `{success:true,data:{settlement_payment_id,receipt_status:"success",url,expires_in:3600}}`
- 202: `{success:true,data:{settlement_payment_id,receipt_status:"pending"|"in_process"}}`
- 409: `{success:false,code:"settlement_receipt_failed",message,data:{settlement_payment_id,receipt_status:"failure"}}`. Use POST to retry.
- 400: invalid payment UUID.
- 403: insufficient permissions.
- 404: payment missing or outside caller scope.
- 500: unexpected server or storage error.

Display localized `message`; use `code` for branching. Signed URLs expire, so reopen this endpoint for a new URL. Bound polling and offer reopen/retry when generation takes longer. Both endpoints send Cache-Control:no-store.

### Printed receipt contract

one database-assigned human-readable number (for example CS-1000) is stored in metadata.receipt_snapshot.receipt_number and remains unchanged on retries/reprints. The payment UUID remains the API/storage identifier. The shared cash/wallet template includes the issuer, driver and phone, UTC settlement date, cash receiver or wallet processor, currency, exact COD/fee/order totals, distinct order and collection counts, and optional entered reference, notes and external order references. Internal payment/wallet UUIDs remain in the stored payload rather than the printed layout. No wallet or settlement API response changes are required.

### Frontend Print action

call POST once on click to open a saved PDF or retry a failed receipt automatically. Use GET for subsequent status polling; never POST from a polling loop. Initial successful-settlement confirmation may use GET because the backend already queues generation. Repeated/concurrent requests reuse the same payment and active PDF job.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/cod-settlements/:payment_id/receipt?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| payment_id | path string | Yes |  | 91000000-0000-4000-8000-000000000001 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  |  |
| language | query string |  |  | en |



### Response 202 Receipt pending

Accepted

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "settlement_payment_id": "91000000-0000-4000-8000-000000000001",
    "receipt_status": "pending"
  }
}
```

## POST /delivery/cod-settlements/:payment_id/receipt — Retry COD Settlement Receipt

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/cod-collections#post-retry-cod-settlement-receipt)

Access the saved settlement batch receipt. Requires authentication. Dispatcher/tenant_admin roles or manage:operations:tenant may access their tenant's settlements; drivers may access only their own. Other roles receive 403; unknown/out-of-tenant/out-of-driver scope returns 404. `payment_id` is the settlement_payment_id returned by either settlement action or a COD collection row.

GET reads status and signs an existing PDF. POST returns the saved PDF when available, reuses an active job, or retries missing/failed PDF generation; it never changes cash, wallet balances, collection status, batch identity, or immutable receipt amounts. No request body is required.

### Responses
- 200: `{success:true,data:{settlement_payment_id,receipt_status:"success",url,expires_in:3600}}`
- 202: `{success:true,data:{settlement_payment_id,receipt_status:"pending"|"in_process"}}`
- 409: `{success:false,code:"settlement_receipt_failed",message,data:{settlement_payment_id,receipt_status:"failure"}}`. Use POST to retry.
- 400: invalid payment UUID.
- 403: insufficient permissions.
- 404: payment missing or outside caller scope.
- 500: unexpected server or storage error.

Display localized `message`; use `code` for branching. Signed URLs expire, so reopen this endpoint for a new URL. Bound polling and offer reopen/retry when generation takes longer. Both endpoints send Cache-Control:no-store.

### Frontend Print action

call POST once on click to open a saved PDF or retry a failed receipt automatically. Use GET for subsequent status polling; never POST from a polling loop. Initial successful-settlement confirmation may use GET because the backend already queues generation. Repeated/concurrent requests reuse the same payment and active PDF job.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/cod-settlements/:payment_id/receipt?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| payment_id | path string | Yes |  | 91000000-0000-4000-8000-000000000001 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  |  |
| language | query string |  |  | en |



### Response 202 Receipt pending

Accepted

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "settlement_payment_id": "91000000-0000-4000-8000-000000000001",
    "receipt_status": "pending"
  }
}
```

## POST /delivery/cod-collections/:collection_id/receipt — Print COD Collection Settlement Receipt

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/cod-collections#post-print-cod-collection-settlement-receipt)

Print a settled collection receipt on demand. POST requires authentication and no body. Dispatcher/tenant_admin or manage:operations:tenant users may access their tenant records; drivers only their own. Other roles receive 403.

If settlement_payment_id already exists, reuse that saved settlement (including its full original batch). Otherwise, lock the historical collection and create one COD_SETTLEMENT receipt payment for this collection only, link it, and use the existing receipt number, HTML template, PDF queue and storage. Concurrent/repeated clicks cannot create duplicate receipts. No bulk historical backfill occurs.

Historical receipts preserve recorded collection amount, COD/fee allocations, method and original remitted_at. The requester is recorded separately from the original receiver/processor. Current profile names are used because historical names were not captured. The original settlement batch is not reconstructed. Receipt creation never changes settlement status/date, order payments, wallet balances or wallet transactions.

### Response matches the settlement receipt endpoint

200 supplies data.settlement_payment_id, receipt_status=success, url and expires_in=3600; 202 supplies payment ID and pending/in_process; 409 supplies settlement_receipt_failed and failure status. A saved PDF is reused, an active job is reused, and missing/failed generation is queued. Cache-Control:no-store.

### Frontend contract

show the existing print icon and Print Receipt menu action for remitted or settled_by_wallet collections. When no payment ID exists, POST this endpoint once on click. Retain returned settlement_payment_id and poll GET /delivery/cod-settlements/:payment_id/receipt with bounded retries. Later prints may use the payment endpoint POST; even stale rows that still call this collection endpoint reuse the saved receipt. Never resubmit cash/wallet settlement to generate a receipt.

Currency uses the unique matching original driver wallet debit for wallet receipts, or one unambiguous currency from the original successful order payments for cash receipts. It never falls back to current tenant settings. Missing or ambiguous original currency returns 409 with settlement_receipt_history_incomplete and a localized explanation.

### Errors

400 invalid UUID or invalid_cod_collections (unsettled or incomplete historical data); 403 insufficient_permissions; 404 cod_collection_not_found for missing/out-of-tenant/out-of-driver rows; 500 unexpected server/storage error. Show localized message, branch on code.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/cod-collections/:collection_id/receipt?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| collection_id | path string | Yes |  | 018ce2e7-7165-432d-8ff3-7d7dff7b8ec7 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  |  |
| language | query string |  |  | en |



### Response 202 Receipt pending

Accepted

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "settlement_payment_id": "91000000-0000-4000-8000-000000000001",
    "receipt_status": "pending"
  }
}
```

## GET /delivery/customer-portal/settings — Get Customer Portal Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-portal-settings#get-get-customer-portal-settings)

Returns the effective Customer Portal settings for delivery creation for the authenticated tenant.

Tenant ID is derived from req.context.tenantId (JWT / auth). No tenant ID in URL.

### Access
- Tenant admin with manage:tenant, manage:operations:tenant, or manage:module:delivery.

### Response
`data` includes `{ options, values }`. `values.support_ticket_settings.support_users` includes the selected support users with their names.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/customer-portal/settings?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Response 200 Success



```json
{
  "success": true,
  "data": {
    "options": {
      "pickup_settings": {
        "modes": [
          "fixed_default",
          "select_from_list",
          "predefined_only",
          "any_address"
        ],
        "allowed_address_types": [
          "pickup",
          "warehouse",
          "branch"
        ]
      },
      "wallet_settings": {
        "allowed_payout_user_types": [
          "individual",
          "business"
        ]
      }
    },
    "values": {
      "pickup_settings": {
        "mode": "fixed_default",
        "allow_change": false,
        "default_pickup_address_id": "uuid",
        "allowed_pickup_address_ids": []
      },
      "delivery_service_settings": {
        "default_delivery_service_id": "uuid",
        "allowed_delivery_service_ids": []
      },
      "cod_settings": {
        "enabled": true,
        "required": false,
        "allow_zero": true,
        "min": 0,
        "max": null
      },
      "delivery_creation_fields": {
        "reference_id_enabled": true,
        "invoicing_enabled": false,
        "custom_shipment_label_enabled": false
      },
      "package_settings": {
        "multipart_enabled": true
      },
      "visibility_settings": {
        "driver_info": {
          "show_driver_info": true,
          "show_driver_phone": true
        },
        "tracking": {
          "realtime_tracking_enabled": true
        },
        "chat": {
          "support_chat_enabled": true,
          "driver_chat_enabled": false
        }
      },
      "support_ticket_settings": {
        "enabled": true,
        "allow_customer_close": true,
        "support_user_ids": [
          "uuid",
          "uuid"
        ],
        "support_users": [
          {
            "id": "uuid",
            "full_name": "Support User One"
          },
          {
            "id": "uuid",
            "full_name": "Support User Two"
          }
        ],
        "default_priority": "medium",
        "auto_assign_on_first_reply": false
      },
      "wallet_settings": {
        "allowed_payout_user_types": [
          "business"
        ]
      },
      "brand_settings": {
        "brand": {
          "name": {
            "en": "",
            "ar": "",
            "ku": ""
          },
          "page_title": ""
        },
        "assets": {
          "favicon_url": "",
          "logo_url": ""
        },
        "theme": {
          "colors": {
            "primary": "#2B14DA",
            "primary2": "#000000",
            "misGrey": "#F7F8FA",
            "misGrey2": "#F3F5F9",
            "blueBigOrder": "#2B7FFF",
            "orangeBgTransit": "#FF6B2B",
            "yellowBgPending": "#E6C718",
            "greenBgDelivered": "#19B53B",
            "redBgDropOff": "#F40909",
            "selectedColor": "rgba(43, 20, 218, 0.05)",
            "activeTextColor": "rgba(0, 0, 0, 1)",
            "iconRestState": "rgba(105, 98, 98, 1)",
            "textRestState": "rgba(105, 98, 98, 1)",
            "characterDisabled": "rgba(0, 0, 0, 0.2)",
            "buttonTextColor": "rgba(255, 255, 255, 1)",
            "overlaysDefault": "rgba(0, 0, 0, 0.2)",
            "neutralStroke": "rgba(217, 217, 217, 1)",
            "backgroundColorWhite": "rgba(255, 255, 255, 1)",
            "borderColor": "rgba(243, 243, 243, 1)"
          }
        }
      }
    }
  }
}
```

## PUT /delivery/customer-portal/settings — Update Customer Portal Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-portal-settings#put-update-customer-portal-settings)

Upserts tenant customer portal settings for delivery creation.

Tenant ID is derived from req.context.tenantId. No tenant ID in URL.

### Access
- Tenant admin with manage:tenant, manage:operations:tenant, or manage:module:delivery.

### Validation
- Pickup address/service IDs must belong to the tenant.
- predefined_only requires allowed_pickup_address_ids.
- fixed_default requires default_pickup_address_id.
- any_address: no restrictions; customer provides pickup (address_id or location JSON) at order creation.

**support_ticket_settings** (optional):
- enabled: enable or disable customer support tickets.
- allow_customer_close: allow customers to close their own tickets.
- support_user_ids: array of tenant support user ids for the shared queue. Each must be an active tenant user in the same tenant.
- support_users: optional frontend object list; if sent, its ids are also validated.
- users with the `driver` tenant role are not allowed as support users.
- default_priority: low | medium | high | urgent.
- auto_assign_on_first_reply: when true, the first support reply to an unassigned ticket claims it automatically.

**brand_settings.theme.colors** (partial update supported; defaults for omitted keys):
- primary: #2B14DA | primary2: #000000 | misGrey: #F7F8FA | misGrey2: #F3F5F9
- blueBigOrder: #2B7FFF | orangeBgTransit: #FF6B2B | yellowBgPending: #E6C718 | greenBgDelivered: #19B53B | redBgDropOff: #F40909
- selectedColor: rgba(43, 20, 218, 0.05) | activeTextColor: rgba(0, 0, 0, 1)
- iconRestState: rgba(105, 98, 98, 1) | textRestState: rgba(105, 98, 98, 1) | characterDisabled: rgba(0, 0, 0, 0.2)
- buttonTextColor: rgba(255, 255, 255, 1) | overlaysDefault: rgba(0, 0, 0, 0.2)
- neutralStroke: rgba(217, 217, 217, 1) | backgroundColorWhite: rgba(255, 255, 255, 1) | borderColor: rgba(243, 243, 243, 1)

**wallet_settings** (optional):
- allowed_payout_user_types: array of "individual" and/or "business" - which customer types can request payouts from customer portal. Default: ["business"]. Enterprise users are treated as business.

**track_orders** (optional):
- enable: enable or disable order tracking.
- track_url: tracking application URL.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/customer-portal/settings?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pickup_settings | object |  |  |  |
| pickup_settings.mode | string |  |  |  |
| pickup_settings.default_pickup_address_id | string |  |  |  |
| pickup_settings.allowed_pickup_address_ids | array<string> |  |  |  |
| delivery_service_settings | object |  |  |  |
| delivery_service_settings.default_delivery_service_id | string |  |  |  |
| delivery_service_settings.allowed_delivery_service_ids | array |  |  |  |
| cod_settings | object |  |  |  |
| cod_settings.enabled | boolean |  |  |  |
| cod_settings.required | boolean |  |  |  |
| cod_settings.allow_zero | boolean |  |  |  |
| cod_settings.min | number |  |  |  |
| cod_settings.max | number |  |  |  |
| delivery_creation_fields | object |  |  |  |
| delivery_creation_fields.reference_id_enabled | boolean |  |  |  |
| delivery_creation_fields.invoicing_enabled | boolean |  |  |  |
| delivery_creation_fields.custom_shipment_label_enabled | boolean |  |  |  |
| package_settings | object |  |  |  |
| package_settings.multipart_enabled | boolean |  |  |  |
| visibility_settings | object |  |  |  |
| visibility_settings.driver_info | object |  |  |  |
| visibility_settings.driver_info.show_driver_info | boolean |  |  |  |
| visibility_settings.driver_info.show_driver_phone | boolean |  |  |  |
| visibility_settings.tracking | object |  |  |  |
| visibility_settings.tracking.realtime_tracking_enabled | boolean |  |  |  |
| visibility_settings.chat | object |  |  |  |
| visibility_settings.chat.support_chat_enabled | boolean |  |  |  |
| visibility_settings.chat.driver_chat_enabled | boolean |  |  |  |
| support_ticket_settings | object |  |  |  |
| support_ticket_settings.enabled | boolean |  |  |  |
| support_ticket_settings.allow_customer_close | boolean |  |  |  |
| support_ticket_settings.support_user_ids | array<string> |  |  |  |
| support_ticket_settings.default_priority | string |  |  |  |
| support_ticket_settings.auto_assign_on_first_reply | boolean |  |  |  |
| wallet_settings | object |  |  |  |
| wallet_settings.allowed_payout_user_types | array<string> |  |  |  |
| track_orders | object |  |  |  |
| track_orders.enable | boolean |  |  |  |
| track_orders.track_url | string |  |  |  |
| brand_settings | object |  |  |  |
| brand_settings.brand | object |  |  |  |
| brand_settings.brand.name | object |  |  |  |
| brand_settings.brand.name.en | string |  |  |  |
| brand_settings.brand.name.ar | string |  |  |  |
| brand_settings.brand.name.ku | string |  |  |  |
| brand_settings.brand.page_title | string |  |  |  |
| brand_settings.assets | object |  |  |  |
| brand_settings.assets.favicon_url | string |  |  |  |
| brand_settings.assets.logo_url | string |  |  |  |
| brand_settings.theme | object |  |  |  |
| brand_settings.theme.colors | object |  |  |  |
| brand_settings.theme.colors.primary | string |  |  |  |
| brand_settings.theme.colors.primary2 | string |  |  |  |
| brand_settings.theme.colors.misGrey | string |  |  |  |
| brand_settings.theme.colors.misGrey2 | string |  |  |  |
| brand_settings.theme.colors.blueBigOrder | string |  |  |  |
| brand_settings.theme.colors.orangeBgTransit | string |  |  |  |
| brand_settings.theme.colors.yellowBgPending | string |  |  |  |
| brand_settings.theme.colors.greenBgDelivered | string |  |  |  |
| brand_settings.theme.colors.redBgDropOff | string |  |  |  |
| brand_settings.theme.colors.selectedColor | string |  |  |  |
| brand_settings.theme.colors.activeTextColor | string |  |  |  |
| brand_settings.theme.colors.iconRestState | string |  |  |  |
| brand_settings.theme.colors.textRestState | string |  |  |  |
| brand_settings.theme.colors.characterDisabled | string |  |  |  |
| brand_settings.theme.colors.buttonTextColor | string |  |  |  |
| brand_settings.theme.colors.overlaysDefault | string |  |  |  |
| brand_settings.theme.colors.neutralStroke | string |  |  |  |
| brand_settings.theme.colors.backgroundColorWhite | string |  |  |  |
| brand_settings.theme.colors.borderColor | string |  |  |  |



### Request body (raw)

```json
{
  "pickup_settings": {
    "mode": "predefined_only",
    "default_pickup_address_id": "uuid",
    "allowed_pickup_address_ids": [
      "uuid"
    ]
  },
  "delivery_service_settings": {
    "default_delivery_service_id": "uuid",
    "allowed_delivery_service_ids": []
  },
  "cod_settings": {
    "enabled": true,
    "required": false,
    "allow_zero": true,
    "min": 0,
    "max": 500000
  },
  "delivery_creation_fields": {
    "reference_id_enabled": true,
    "invoicing_enabled": false,
    "custom_shipment_label_enabled": false
  },
  "package_settings": {
    "multipart_enabled": true
  },
  "visibility_settings": {
    "driver_info": {
      "show_driver_info": true,
      "show_driver_phone": false
    },
    "tracking": {
      "realtime_tracking_enabled": true
    },
    "chat": {
      "support_chat_enabled": true,
      "driver_chat_enabled": false
    }
  },
  "support_ticket_settings": {
    "enabled": true,
    "allow_customer_close": true,
    "support_user_ids": [
      "uuid",
      "uuid"
    ],
    "default_priority": "medium",
    "auto_assign_on_first_reply": true
  },
  "wallet_settings": {
    "allowed_payout_user_types": [
      "individual",
      "business"
    ]
  },
  "track_orders": {
    "enable": true,
    "track_url": "https://staging-app.ondi.io/tracking"
  },
  "brand_settings": {
    "brand": {
      "name": {
        "en": "Acme",
        "ar": "",
        "ku": ""
      },
      "page_title": "Acme Tracking"
    },
    "assets": {
      "favicon_url": "https://...",
      "logo_url": "https://..."
    },
    "theme": {
      "colors": {
        "primary": "#2B14DA",
        "primary2": "#000000",
        "misGrey": "#F7F8FA",
        "misGrey2": "#F3F5F9",
        "blueBigOrder": "#2B7FFF",
        "orangeBgTransit": "#FF6B2B",
        "yellowBgPending": "#E6C718",
        "greenBgDelivered": "#19B53B",
        "redBgDropOff": "#F40909",
        "selectedColor": "rgba(43, 20, 218, 0.05)",
        "activeTextColor": "rgba(0, 0, 0, 1)",
        "iconRestState": "rgba(105, 98, 98, 1)",
        "textRestState": "rgba(105, 98, 98, 1)",
        "characterDisabled": "rgba(0, 0, 0, 0.2)",
        "buttonTextColor": "rgba(255, 255, 255, 1)",
        "overlaysDefault": "rgba(0, 0, 0, 0.2)",
        "neutralStroke": "rgba(217, 217, 217, 1)",
        "backgroundColorWhite": "rgba(255, 255, 255, 1)",
        "borderColor": "rgba(243, 243, 243, 1)"
      }
    }
  }
}
```

### Response 200 Success



```json
{
  "success": true,
  "message": "Tenant Setting Updated Successfully",
  "data": {
    "options": {
      "pickup_settings": {
        "modes": [
          "fixed_default",
          "select_from_list",
          "predefined_only",
          "any_address"
        ],
        "allowed_address_types": [
          "pickup",
          "warehouse",
          "branch"
        ]
      },
      "wallet_settings": {
        "allowed_payout_user_types": [
          "individual",
          "business"
        ]
      }
    },
    "values": {
      "pickup_settings": {
        "mode": "predefined_only",
        "allow_change": true,
        "default_pickup_address_id": "uuid",
        "allowed_pickup_address_ids": [
          "uuid"
        ]
      },
      "brand_settings": {
        "brand": {
          "name": {
            "en": "Acme",
            "ar": "",
            "ku": ""
          },
          "page_title": "Acme Tracking"
        },
        "assets": {
          "favicon_url": "https://...",
          "logo_url": "https://..."
        },
        "theme": {
          "colors": {
            "primary": "#2B14DA",
            "primary2": "#000000",
            "misGrey": "#F7F8FA",
            "misGrey2": "#F3F5F9",
            "blueBigOrder": "#2B7FFF",
            "orangeBgTransit": "#FF6B2B",
            "yellowBgPending": "#E6C718",
            "greenBgDelivered": "#19B53B",
            "redBgDropOff": "#F40909",
            "selectedColor": "rgba(43, 20, 218, 0.05)",
            "activeTextColor": "rgba(0, 0, 0, 1)",
            "iconRestState": "rgba(105, 98, 98, 1)",
            "textRestState": "rgba(105, 98, 98, 1)",
            "characterDisabled": "rgba(0, 0, 0, 0.2)",
            "buttonTextColor": "rgba(255, 255, 255, 1)",
            "overlaysDefault": "rgba(0, 0, 0, 0.2)",
            "neutralStroke": "rgba(217, 217, 217, 1)",
            "backgroundColorWhite": "rgba(255, 255, 255, 1)",
            "borderColor": "rgba(243, 243, 243, 1)"
          }
        }
      }
    }
  }
}
```

## GET /delivery/financial-dashboard — Get Financial Dashboard (Overview)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/financial-dashboard-delivery#get-get-financial-dashboard-overview)

Returns the Financial Dashboard overview payload.

### Includes
- KPI summary cards (with trends and sparkline chart_data)
- Revenue chart (Delivery Fees vs COD Collected)
- COD breakdown donut values
- Customer breakdown (Individual vs Business)
- Wallet overview (total wallet balances, pending payouts, available after payouts)

### Notes
- `pending_payouts` and wallet overview are point-in-time (not date-range based), but respect customer filters.
- KPI totals and charts follow the date range.

### Permissions
- view:financial_reports OR view:finance:tenant OR manage:wallets OR manage:operations:tenant

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/financial-dashboard?start_date=&end_date=&customer_type=&customer_id=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| start_date | query string |  | Filter from date (ISO or YYYY-MM-DD). Default: last 56 days window. |  |
| end_date | query string |  | Filter to date (ISO or YYYY-MM-DD). Default: today (UTC day). |  |
| customer_type | query string |  | Filter by customer type: individual \| business |  |
| customer_id | query string |  | Filter by customer user id (UUID) |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



## GET /delivery/financial-dashboard/driver-cod — Get Driver COD Ranking

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/financial-dashboard-delivery#get-get-driver-cod-ranking)

Returns drivers ranked by COD collected in the date range.

### Each row includes
- total_cod_collected
- total_cod_remitted (includes settled_by_wallet)
- pending_cod
- deliveries_completed
- avg_cod_per_delivery
- remit_rate (%)

### Permissions
- view:financial_reports OR view:finance:tenant OR manage:wallets OR manage:operations:tenant

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/financial-dashboard/driver-cod?page=1&limit=10&start_date=&end_date=&customer_type=&customer_id=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 10, max: 100) | 10 |
| start_date | query string |  | Filter from date (ISO or YYYY-MM-DD). Default: last 56 days window. |  |
| end_date | query string |  | Filter to date (ISO or YYYY-MM-DD). Default: today (UTC day). |  |
| customer_type | query string | No | Optional customer type filter: individual \| business |  |
| customer_id | query string | No | Optional customer user id filter (UUID) |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



## GET /delivery/financial-dashboard/payout-requests — List Payout Requests

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/financial-dashboard-delivery#get-list-payout-requests-2)

Returns payout requests with filters and pagination.

### Includes per row
- customer_name / phone
- wallet_balance
- total_orders (lifetime count in tenant)
- requested_at / processed_at

Also returns top-level `counts` for the KPI strip:
- all
- pending
- approved
- rejected

### Notes
- `counts` respect search, date range, customer type, user, and tenant filters
- `counts` intentionally ignore the active `status` filter

### Permissions
- view:financial_reports OR view:finance:tenant OR manage:wallets OR manage:operations:tenant

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/financial-dashboard/payout-requests?status=&customer_type=&user_id=&search=&start_date=&end_date=&page=1&limit=10&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | query string |  | Filter by status: pending \| approved \| rejected (omit for all) |  |
| customer_type | query string |  | Filter by customer type: individual \| business (omit for all) |  |
| user_id | query string |  | Filter by customer user id (UUID) |  |
| search | query string |  | Search by customer name or phone |  |
| start_date | query string |  | Filter by requested_at from date (ISO or YYYY-MM-DD) |  |
| end_date | query string |  | Filter by requested_at to date (ISO or YYYY-MM-DD) |  |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 10, max: 100) | 10 |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



## POST /delivery/financial-dashboard/payout-requests/:id/approve — Approve Payout Request

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/financial-dashboard-delivery#post-approve-payout-request)

Approves a pending payout request and creates a wallet debit transaction. HTTP 200 returns a localized success `message`; an RPC failure returns a localized safe message and never exposes database error text.

### Permissions
- manage:wallets OR manage:operations:tenant

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/financial-dashboard/payout-requests/:id/approve?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Payout request ID | payout-request-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "notes": ""
}
```

## POST /delivery/financial-dashboard/payout-requests/:id/reject — Reject Payout Request

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/financial-dashboard-delivery#post-reject-payout-request)

Rejects a pending payout request (no wallet transaction is created). HTTP 200 returns a localized success `message`; an RPC failure returns a localized safe message and never exposes database error text.

Body is optional.
- If `reason` is omitted/empty, backend stores a default reason.

### Permissions
- manage:wallets OR manage:operations:tenant

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/financial-dashboard/payout-requests/:id/reject?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Payout request ID | payout-request-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "reason": ""
}
```

## POST /delivery/financial-dashboard/payouts — Create Manual Payout

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/financial-dashboard-delivery#post-create-manual-payout)

Creates a manual payout request with caller-provided status. Success returns a localized `message`: English is `The manual payout request was created successfully` for pending or `The manual payout was created successfully` for approved.

### Behavior
- pending: creates only the payout request
- approved: creates the payout request and immediately debits the wallet

### Permissions
- manage:wallets OR manage:operations:tenant

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/financial-dashboard/payouts?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| business_user_id | string | Yes | (required) |  |
| amount | number | Yes | (required) |  |
| status | string | Yes | (required: pending \| approved) |  |
| reference | string | No | (optional) |  |
| notes | string | No | (optional) |  |



### Request body (raw)

```json
{
  "business_user_id": "",
  "amount": 0,
  "status": "pending",
  "reference": "",
  "notes": ""
}
```

## POST /delivery/waybills/validate-items — Validate Warehouse Waybill Items

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-validate-warehouse-waybill-items)

Read-only mixed-item preflight for warehouse-to-warehouse Waybill creation. Destination may be omitted when the first compatible explicit or linked Draft delivery can lock it. Raw-only inventory requires an explicit destination. A staged P&D parent is accepted when its outbound child is Draft, or when it is Pending with one completed P&D pick, a complete warehouse reservation, no active assignment, route, or handoff, and no open delivery or warehouse-item exception. An accepted Pending child returns `preparation_will_be_superseded: true`. Response returns accepted deliveries/items, localized per-item rejections, `locked_destination_warehouse_id`, `requires_new_delivery_defaults`, and `can_create`. Important rejection codes include `staged_outbound_leg_already_assigned`, `staged_outbound_handoff_active`, `delivery_in_active_route_session`, `delivery_has_open_exception`, `warehouse_item_exception_open`, and `warehouse_preparation_inconsistent`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/validate-items

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| origin_warehouse_id | string |  |  |  |
| destination_warehouse_id | string |  |  |  |
| delivery_ids | array<string> |  |  |  |
| inventory_item_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "origin_warehouse_id": "{{source_warehouse_id}}",
  "destination_warehouse_id": "{{destination_warehouse_id}}",
  "delivery_ids": [
    "{{delivery_id}}"
  ],
  "inventory_item_ids": [
    "{{inventory_item_id}}"
  ]
}
```

## POST /delivery/waybills — Create Warehouse Waybill (Mixed Items)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-create-warehouse-waybill-mixed-items)

Atomically creates a Draft warehouse-to-warehouse Waybill from compatible deliveries and/or individually tracked inventory. `Idempotency-Key` is required. Defaults are required only for raw inventory without a reusable linked Draft delivery. When a selected staged P&D parent has an untouched Pending outbound child, the same transaction preserves its old P&D pick as Cancelled/superseded, releases its stock reservation, cancels pending auto-assignment work, restores that outbound child to Draft, and attaches the parent. Any later create failure rolls all of those changes back. Active assignment, route, handoff, custody movement, or an open exception blocks the swap. Sealing later creates the separate Waybill batch picking order; the old per-delivery P&D picking-order row is never reused. Request fields are unchanged.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| Idempotency-Key | header string |  |  | warehouse-waybill-{{$guid}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| origin_type | string |  |  |  |
| origin_warehouse_id | string |  |  |  |
| destination_type | string |  |  |  |
| destination_warehouse_id | string |  |  |  |
| carrier_type | string |  |  |  |
| delivery_ids | array<string> |  |  |  |
| inventory_item_ids | array<string> |  |  |  |
| new_delivery_defaults | object |  |  |  |
| new_delivery_defaults.customer_id | string |  |  |  |
| new_delivery_defaults.delivery_service_id | string |  |  |  |
| new_delivery_defaults.payer_type | string |  |  |  |
| new_delivery_defaults.payment_method | string |  |  |  |
| new_delivery_defaults.payment_method_type_id | string |  |  |  |
| new_delivery_defaults.delivery_fee | number |  |  |  |
| new_delivery_defaults.cod_amount | number |  |  |  |
| new_delivery_defaults.notes | string |  |  |  |



### Request body (raw)

```json
{
  "origin_type": "warehouse",
  "origin_warehouse_id": "{{source_warehouse_id}}",
  "destination_type": "warehouse",
  "destination_warehouse_id": "{{destination_warehouse_id}}",
  "carrier_type": "internal",
  "delivery_ids": [
    "{{delivery_id}}"
  ],
  "inventory_item_ids": [
    "{{inventory_item_id}}"
  ],
  "new_delivery_defaults": {
    "customer_id": "{{customer_id}}",
    "delivery_service_id": "{{delivery_service_id}}",
    "payer_type": "sender",
    "payment_method": "cash",
    "payment_method_type_id": "{{payment_method_type_id}}",
    "delivery_fee": 0,
    "cod_amount": 0,
    "notes": "Created from warehouse inventory"
  }
}
```

## POST /delivery/waybills/:id/receive — Receive Warehouse Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-receive-warehouse-waybill)

Destination warehouse receipt. `Idempotency-Key` is required. The frontend keeps one Waybill receipt action. Quick receipt still sends only `{ verification_mode: quick }`; verified receipt sends one of `present`, `missing`, or `damaged` for each delivery code. Overage is not supported. Unknown or extra delivery codes must be sent as `present`; the backend skips them and returns them in `operation_result.invalid_delivery_codes`. Do not send `delivery_id`, `cage_location_id`, or a separate `overages` array. Optional top-level `receiving_location_id` selects an active bin in the destination warehouse. When omitted, the backend uses that warehouse's active `default_receiving_location`; if neither exists the complete receipt rolls back with HTTP 409 `destination_receiving_location_required`. Each logical delivery receives its own destination receiving order with an exact outbound `delivery_id`, delivery-code `external_reference`, parcel item IDs, outcome, and execution state. Present parcels enter destination stock and are executable. Damaged parcels enter damaged/quarantine stock and are blocked. Missing parcels create no destination stock and are blocked. The original warehouse receiving order remains historical and returns `can_execute: false` with `execution_block_reason: receiving_order_custody_transferred`. Stock selection follows Waybill delivery membership and never depends on `is_single_item`. Receipt remains atomic and stops the Waybill at `received`; close remains separate after discrepancies are resolved.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/:id/receive

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| Idempotency-Key | header string |  |  | warehouse-receipt-{{$guid}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | <waybill_id> |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| verification_mode | string |  |  |  |
| manifest_version | number |  |  |  |
| receiving_location_id | string |  |  |  |
| deliveries | array<object> |  |  |  |
| deliveries.delivery_code | string |  |  |  |
| deliveries.outcome | string |  |  |  |
| proof_of_delivery | object |  |  |  |
| proof_of_delivery.notes | string |  |  |  |
| proof_of_delivery.photo_urls | array |  |  |  |



### Request body (raw)

```json
{
  "verification_mode": "verified",
  "manifest_version": 1,
  "receiving_location_id": "{{receiving_location_id}}",
  "deliveries": [
    {
      "delivery_code": "DEL1001",
      "outcome": "present"
    },
    {
      "delivery_code": "DEL1002",
      "outcome": "missing"
    },
    {
      "delivery_code": "DEL1003",
      "outcome": "damaged"
    },
    {
      "delivery_code": "DEL9999",
      "outcome": "present"
    }
  ],
  "proof_of_delivery": {
    "notes": "Receipt verification completed",
    "photo_urls": []
  }
}
```

### Response 200 Success - Verified Receipt

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill received successfully",
  "data": {
    "id": "{{waybill_id}}",
    "waybill_number": "WB00032",
    "status": "received",
    "destination_type": "warehouse",
    "destination_warehouse_id": "{{destination_warehouse_id}}",
    "operation_result": {
      "verification_mode": "verified",
      "present_count": 1,
      "missing_count": 1,
      "damaged_count": 1,
      "results": [
        {
          "delivery_code": "DEL1001",
          "outcome": "present",
          "success": true
        },
        {
          "delivery_code": "DEL1002",
          "outcome": "missing",
          "success": true
        },
        {
          "delivery_code": "DEL1003",
          "outcome": "damaged",
          "success": true
        }
      ],
      "invalid_delivery_codes": [
        {
          "delivery_code": "DEL9999",
          "outcome": "present",
          "reason": "delivery_not_found"
        }
      ]
    }
  }
}
```

### Response 400 Error - Unsupported Receipt Outcome

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "deliveries: The selected receipt outcome is not valid",
  "details": [
    {
      "field": "deliveries",
      "message": "The selected receipt outcome is not valid"
    }
  ]
}
```

### Response 409 Error - Expected Manifest Outcome Missing

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_receipt_manifest_mismatch",
  "message": "The submitted receipt does not match the current Waybill manifest",
  "details": {
    "expected_count": 3,
    "provided_valid_count": 2,
    "invalid_delivery_codes": [
      {
        "delivery_code": "DEL1003",
        "outcome": null,
        "reason": "expected_delivery_outcome_missing"
      }
    ]
  },
  "data": null
}
```

### Response 409 Error - Destination Receiving Location Required

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "destination_receiving_location_required",
  "message": "Set an active default receiving location for the destination warehouse or select a receiving location",
  "details": {
    "warehouse_id": "{{destination_warehouse_id}}",
    "receiving_location_id": null
  },
  "data": null
}
```

### Response 409 Error - Destination Warehouse Address Missing

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_destination_warehouse_address_missing",
  "message": "The destination warehouse does not have a valid address. Add an address before receiving this Waybill.",
  "details": null,
  "data": null
}
```

## POST /delivery/waybills/:id/discrepancies/:discrepancy_id/resolve — Resolve Waybill Receipt Discrepancy

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-resolve-waybill-receipt-discrepancy)

Resolves one open Waybill receipt discrepancy after a warehouse-destination Waybill is received. `Idempotency-Key` and Waybill-management permission are required. Recording `missing` or `damaged` during receipt never creates a reverse delivery. For a missing parcel, send `found` (with an active destination receiving location) or `lost`. `found` uses the parcel's destination receiving order as the receipt source of truth: it creates or updates destination-bin stock, records one idempotent `receive` inventory movement against that receiving order, changes the receipt outcome to `present`, makes the receiving order executable, and increases `summary.return_breakdown.reached_destination`. It does not depend on the origin stock-exit movement type and does not create a reverse delivery. `lost` closes the discrepancy as a terminal transport loss and creates no reverse delivery because no parcel exists to move. Because `lost` makes the actual delivery `returned`, the existing delivery-service return-refund policy is settled atomically and idempotently for that delivery. For a damaged parcel, record the merchant's decision in non-empty `merchant_response`, then send `continue_delivery` to change only that parcel's destination stock from `damaged` to `available`, change its current receipt outcome to `present` while retaining the original damage audit, and unlock its receiving order; it does not create a reverse delivery. Only `return_to_sender` creates the existing no-fee reverse delivery and returns both `data.operation_result.return_delivery_id` and `data.operation_result.return_delivery_code`; that physical reverse delivery does not refund the still-active original delivery. Every successful resolution sets `receipt_summary.discrepancies` to the remaining open discrepancy count. Its generic timeline event uses the selected action as `event_type`: `found`, `lost`, `continue_delivery`, or `return_to_sender`. `reason` is reserved for a separate human explanation and is omitted when none was supplied; `notes` remains staff comments. Event metadata includes `event_category=waybill_discrepancy_resolution`, `discrepancy_type`, `resolution`, `discrepancy_id`, `delivery_id`, and `waybill_id`. Matching historical events are normalized without deleting their audit history. Missing and damaged parcels remain blocked from normal assignment, status, and return actions while their discrepancy is open. No separate write-off or parallel refund implementation is created by this endpoint. Important HTTP 409 codes include `exception_not_found`, `exception_already_resolved`, `waybill_missing_resolution_not_allowed`, `waybill_damaged_resolution_not_allowed`, `receiving_order_not_found`, `destination_receiving_location_required`, `invalid_destination_receiving_location`, and `no_received_items_to_execute`; missing `merchant_response` returns HTTP 400 `merchant_response_required`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/:id/discrepancies/:discrepancy_id/resolve

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| Idempotency-Key | header string |  |  | waybill-receipt-resolution-{{$guid}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | <waybill_id> |
| discrepancy_id | path string | Yes |  | <receipt_discrepancy_id> |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| resolution | string |  |  |  |
| merchant_response | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "resolution": "continue_delivery",
  "merchant_response": "Merchant approved delivery despite the recorded damage",
  "notes": "Outer carton dented; contents inspected"
}
```

### Response 200 Success - Found

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Operation completed successfully",
  "data": {
    "id": "{{waybill_id}}",
    "status": "received",
    "receipt_summary": {
      "present": 2,
      "missing": 0,
      "resolved_missing": 1,
      "discrepancies": 0,
      "requires_reconciliation": false
    },
    "operation_result": {
      "success": true,
      "waybill_id": "{{waybill_id}}",
      "discrepancy_id": "{{missing_discrepancy_id}}",
      "delivery_id": "{{missing_delivery_id}}",
      "resolution": "found",
      "delivery_status": "staged_at_warehouse",
      "receiving_order_id": "{{receiving_order_id}}",
      "receiving_location_id": "{{receiving_location_id}}",
      "stock_row_count": 1,
      "stock_quantity": 1,
      "can_execute": true,
      "idempotent_replay": false
    }
  }
}
```

### Response 200 Success - Lost

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Operation completed successfully",
  "data": {
    "id": "{{waybill_id}}",
    "status": "received",
    "receipt_summary": {
      "missing": 0,
      "resolved_missing": 1,
      "discrepancies": 0,
      "requires_reconciliation": false
    },
    "operation_result": {
      "success": true,
      "waybill_id": "{{waybill_id}}",
      "discrepancy_id": "{{missing_discrepancy_id}}",
      "delivery_id": "{{missing_delivery_id}}",
      "resolution": "lost",
      "delivery_status": "returned",
      "receiving_order_id": "{{receiving_order_id}}",
      "return_delivery_created": false,
      "idempotent_replay": false
    }
  }
}
```

### Response 200 Success - Continue Damaged Delivery

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Operation completed successfully",
  "data": {
    "receipt_summary": {
      "present": 2,
      "damaged": 0,
      "resolved_damaged": 1,
      "discrepancies": 0,
      "requires_reconciliation": false
    },
    "operation_result": {
      "resolution_action": "continue_delivery",
      "receiving_order_id": "{{receiving_order_id}}",
      "can_execute": true,
      "stock_row_count": 1,
      "return_delivery_id": null,
      "return_delivery_code": null
    }
  }
}
```

### Response 200 Success - Return Damaged Parcel to Sender

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Operation completed successfully",
  "data": {
    "receipt_summary": {
      "damaged": 0,
      "resolved_damaged": 1,
      "discrepancies": 0,
      "requires_reconciliation": false
    },
    "operation_result": {
      "resolution_action": "return_to_sender",
      "receiving_order_id": "{{receiving_order_id}}",
      "can_execute": false,
      "stock_row_count": 0,
      "return_delivery_id": "{{return_delivery_id}}",
      "return_delivery_code": "{{return_delivery_code}}"
    }
  }
}
```

## GET /delivery/waybills — List Waybills

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#get-list-waybills)

List waybills with pagination and filters for dispatcher/admin screens. Supports warehouse_id + direction filters for warehouse-centric views. Response includes a `statuses` matrix for tab counts; it respects the active non-status filters and ignores only the current `status` filter. `item_count` is the current attached count for open Waybills and the preserved historical non-removed membership count for closed or cancelled Waybills. Response includes a top-level `has_empty_drafts` boolean; it is tenant-wide and not affected by pagination or filters, so the frontend can decide whether to show empty-draft cleanup actions.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/waybills?page=1&limit=20&status=&carrier_type=&driver_id=&from_date=&to_date=&waybill_number=&warehouse_id=&direction=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number | 1 |
| limit | query string |  | Items per page (max 100) | 20 |
| status | query string |  | Filter by status: drafted, sealed, assigned, picked_up, in_transit, awaiting_receipt, received, closed, cancelled |  |
| carrier_type | query string |  | Filter by carrier: internal, prime, external |  |
| driver_id | query string |  | Filter by assigned driver UUID |  |
| from_date | query string |  | Start date (YYYY-MM-DD) |  |
| to_date | query string |  | End date (YYYY-MM-DD) |  |
| waybill_number | query string |  | Search by waybill number |  |
| warehouse_id | query string |  | Filter by warehouse UUID — shows waybills where this warehouse is origin or destination (combine with direction for specificity) |  |
| direction | query string |  | Used with warehouse_id. 'outbound' = origin warehouse, 'inbound' = destination warehouse. Omit to match both. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybills retrieved successfully",
  "data": [
    {
      "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
      "tenant_id": "{{tenant_id}}",
      "waybill_number": "WB00001",
      "origin_type": "warehouse",
      "origin_warehouse_id": "{{warehouse_id}}",
      "origin_location": {
        "id": "{{warehouse_address_id}}",
        "full_address": "Main Warehouse, Erbil",
        "latitude": 36.191,
        "longitude": 44.01,
        "city": "Erbil",
        "state": null,
        "country": "Iraq",
        "postal_code": null,
        "country_code": "IQ",
        "contact_info": null
      },
      "destination_type": "location",
      "destination_warehouse_id": null,
      "destination_location": {
        "latitude": 36.1901,
        "longitude": 44.0091,
        "full_address": "Erbil, Kurdistan Region"
      },
      "carrier_type": "internal",
      "driver_id": "{{driver_id}}",
      "vehicle_id": "{{vehicle_id}}",
      "external_carrier_ref": null,
      "assigned_by": "{{user_id}}",
      "assignment_time": "2026-04-07T12:20:00Z",
      "status": "sealed",
      "sealed_at": "2026-04-07T12:15:00Z",
      "dispatched_at": null,
      "received_at": null,
      "received_by": null,
      "closed_at": null,
      "cancelled_at": null,
      "proof_of_delivery": null,
      "notes": "Batch for afternoon dispatch",
      "created_by": "{{user_id}}",
      "created_at": "2026-04-07T12:00:00Z",
      "updated_at": "2026-04-07T12:20:00Z",
      "item_count": 1,
      "origin_warehouse": {
        "id": "{{warehouse_id}}",
        "name": "Main Warehouse",
        "code": "WH-MAIN"
      },
      "destination_warehouse": null,
      "origin_zone": {
        "id": "{{origin_zone_id}}",
        "name": "Erbil Central",
        "code": "ERB-CENTRAL"
      },
      "destination_zone": {
        "id": "{{destination_zone_id}}",
        "name": "Erbil East",
        "code": "ERB-EAST"
      }
    }
  ],
  "statuses": [
    {
      "status": "drafted",
      "label": "Drafted",
      "count": 0
    },
    {
      "status": "sealed",
      "label": "Sealed",
      "count": 1
    },
    {
      "status": "assigned",
      "label": "Assigned",
      "count": 0
    },
    {
      "status": "picked_up",
      "label": "Picked Up",
      "count": 0
    },
    {
      "status": "in_transit",
      "label": "In Transit",
      "count": 0
    },
    {
      "status": "awaiting_receipt",
      "label": "Awaiting Receipt",
      "count": 0
    },
    {
      "status": "received",
      "label": "Received",
      "count": 0
    },
    {
      "status": "closed",
      "label": "Closed",
      "count": 0
    },
    {
      "status": "cancelled",
      "label": "Cancelled",
      "count": 0
    }
  ],
  "has_empty_drafts": true,
  "total": 1,
  "page": 1,
  "limit": 20
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions",
  "data": null
}
```

## POST /delivery/waybills — Create Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-create-waybill)

### Wallet Error Contract - Waybill Creation

when attached inventory-item delivery creation reaches a known wallet refusal, the response preserves that wallet code and its localized message instead of reporting waybill_service_unavailable. The request and HTTP status behavior are unchanged. The normal non-warehouse-batch create path now returns a compact authoritative Waybill so the caller does not need an immediate detail reload; the separately documented atomic warehouse-batch create flow keeps its existing response.

Create a draft waybill. You can attach deliveries at creation time or later while still drafted. A selected staged P&D parent may have an untouched Pending outbound child. In that case, creation atomically cancels the old per-delivery P&D pick as superseded, releases its reservation and pending auto-assignment, returns the outbound child to Draft, and attaches the parent. The old pick remains audit history and is not reused; sealing creates the Waybill batch pick. Active assignment, route, handoff, custody movement, or an open exception blocks the change.

The backend generates `waybill_number` atomically in PostgreSQL. New delivery Waybills use the format `WB00001` without a hyphen; existing `WB-00001` values remain valid and are not rewritten. Origin and destination warehouse validation is batched, and independent zone lookups run in parallel.

### Frontend contract
- The request payload is unchanged. Do not send `waybill_number`; it is server-generated.
- On success, use `data.id` as the entity ID and display `data.waybill_number` exactly as returned.
- The normal non-warehouse-batch success payload is a compact authoritative Waybill with `item_count`, `summary`, endpoint warehouse/location/zone context and, when deliveries were requested, `operation_result`. It intentionally omits embedded `deliveries`, receipt detail, discrepancies and timeline data. The separately documented atomic warehouse-batch create flow is unchanged. Use `GET /delivery/waybills/{id}` only when the detail screen needs those fields.
- On errors, use the top-level `code` for behavior and the localized `message` for display.
- Business and eligibility failures return localized 400, 404, or 409 responses. When `details` is an item array, each item includes its stable `code` and localized `message`.
- The create endpoint does not return HTTP 500. Database or unexpected availability failures return `waybill_service_unavailable` with HTTP 503 and `details.operation_id`; preserve the form so the user can retry and show the operation ID when contacting support.
- Important creation codes: `invalid_request_body` (400), `waybill_origin_warehouse_not_found` or `waybill_destination_warehouse_not_found` (404), `staged_outbound_leg_already_assigned`, `staged_outbound_handoff_active`, `delivery_in_active_route_session`, `delivery_has_open_exception`, `warehouse_item_exception_open`, `warehouse_preparation_inconsistent`, `waybill_items_rejected`, or `waybill_number_conflict` (409), and `waybill_service_unavailable` (503).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| origin_type | string |  |  |  |
| origin_warehouse_id | string |  |  |  |
| destination_type | string |  |  |  |
| destination_location | object |  |  |  |
| destination_location.latitude | number |  |  |  |
| destination_location.longitude | number |  |  |  |
| destination_location.full_address | string |  |  |  |
| carrier_type | string |  |  |  |
| notes | string |  |  |  |
| delivery_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "origin_type": "warehouse",
  "origin_warehouse_id": "{{warehouse_id}}",
  "destination_type": "location",
  "destination_location": {
    "latitude": 36.1901,
    "longitude": 44.0091,
    "full_address": "Erbil, Kurdistan Region"
  },
  "carrier_type": "internal",
  "notes": "Batch for afternoon dispatch",
  "delivery_ids": [
    "{{delivery_id}}"
  ]
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill created successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": {
      "id": "{{warehouse_address_id}}",
      "full_address": "Main Warehouse, Erbil",
      "latitude": 36.191,
      "longitude": 44.01
    },
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "status": "drafted",
    "item_count": 1,
    "summary": {
      "total_deliveries": 1,
      "total_unit_size": 1,
      "status_breakdown": {
        "pending": 1
      }
    },
    "operation_result": {
      "attached": 1,
      "attached_delivery_ids": [
        "{{delivery_id}}"
      ],
      "superseded_delivery_ids": [],
      "superseded_picking_order_ids": []
    }
  }
}
```

### Response 400 Error - Invalid Origin

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This field is required",
  "details": [
    {
      "field": "origin_warehouse_id",
      "message": "This field is required"
    }
  ]
}
```

### Response 409 Error - Delivery Already In Another Waybill

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_already_in_waybill",
  "message": "Delivery is already linked to another waybill",
  "data": null
}
```

### Response 503 Error - Waybill Service Unavailable

Service Unavailable

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_service_unavailable",
  "message": "The Waybill service is temporarily unavailable. Please try again. If the problem continues, contact support with the operation ID.",
  "details": {
    "operation_id": "request-or-generated-uuid"
  },
  "data": null
}
```

### Response 409 Error - Waybill Number Conflict

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_number_conflict",
  "message": "Unable to allocate a unique Waybill number. Please try again.",
  "data": null
}
```

### Response 400 Error - Invalid Request Body

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_request_body",
  "message": "Invalid request body",
  "details": null,
  "data": null
}
```

### Response 503 Wallet Error Contract - Waybill Creation

Service Unavailable

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit.",
  "details": {
    "operation_id": "request-operation-id"
  },
  "data": null
}
```

## GET /delivery/waybills/{{waybill_id}} — Get Waybill Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#get-get-waybill-detail)

Returns enriched Waybill detail. `received_by` is the receiver user ID and `received_by_user` is the matching user object (`id`, `full_name`, `email`, `phone`, `avatar_url`); both are null until the Waybill is received.

### Summary contract

`summary.return_breakdown.reached_destination` is always `0` before the Waybill reaches `received`. For a received or closed warehouse-destination Waybill, it counts the authoritative receipt outcomes `present` and `damaged`; an unresolved `missing` parcel is not counted even when that delivery still has a historical `staged_at_warehouse` status from the origin. Resolving the parcel as `found` changes its receipt item to `present`, so it starts counting as reached. Resolving it as `lost` keeps the internal terminal delivery status used by the operational and finance flow, but reporting counts it only in `summary.return_breakdown.lost`, never in `returned`; its embedded delivery has `is_lost=true` and `is_returned=false`. For non-warehouse destinations, the existing delivered/staged status rule remains. A requested, approved, or in-transit return does not count as returned. Only a physically completed return counts in `returned`; completed returns are excluded from `reached_destination`, and their embedded delivery has `is_returned=true`. `reached_destination`, `returned`, `lost`, `cancelled`, and `active` are mutually exclusive sibling counts and reconcile to `summary.total_deliveries`; `fully_returned` is true only when every child was physically returned.

### Receipt count contract

`receipt_summary.missing`, `receipt_summary.damaged`, and `receipt_summary.discrepancies` count only currently open discrepancies. `receipt_summary.resolved_missing` and `receipt_summary.resolved_damaged` retain historical resolved counts. `requires_reconciliation` remains true while any delivery discrepancy is open.

### Discrepancy detail contract

`discrepancies.present[]` contains `delivery_code` and `barcode` plus the internal `delivery_id`. `discrepancies.missing[]` keeps both open and resolved missing history and includes `resolution`, `resolution_notes`, `resolution_details`, and `available_resolutions`. `available_resolutions` is `[found, lost]` only while that missing discrepancy is open; otherwise it is empty. Missing and damaged rows also contain `delivery_code`, `barcode`, the raw stored `reason`, `notes`, `status`, `reported_by_user`, `resolved_by_user`, `created_at`, and `resolved_at`, while retaining internal IDs. User objects contain `id`, `full_name`, `email`, `phone`, and `avatar_url`. The canonical resolver property is `resolved_by_user`; `resolve_by_user` is never returned. Reasons are stable raw backend values such as `missing_on_receive` and `damaged_on_receive`; clients must not translate or reformat them. Dates are ISO 8601 timestamps. Open discrepancies may have null `resolved_by_user` and `resolved_at`. `available_actions` contains `resolve_missing_discrepancy` when a received Waybill has at least one open missing discrepancy. Frontend display must use `delivery_code || barcode` and must not depend on delivery IDs.

Historical receipts without an item-level receipt audit use the existing compatibility rule: manifest deliveries without an open missing receipt discrepancy count as reached; damaged deliveries count as physically reached. Before receipt, `discrepancies.present` is an empty array. `status_updates` is the chronological audit timeline for lifecycle, custody, and discrepancy resolution actions. Discrepancy resolution events use the selected action as `event_type`: `found`, `lost`, `continue_delivery`, or `return_to_sender`. `reason` is optional human context only. Each event contains `event_type`, previous/new status where applicable, timestamp, actor, optional reason/notes, and metadata.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill retrieved successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "status": "picked_up",
    "driver_id": "{{new_driver_id}}",
    "assignment_time": "2026-04-07T12:35:00Z",
    "status_updates": [
      {
        "event_type": "created",
        "previous_status": null,
        "new_status": "drafted",
        "timestamp": "2026-04-07T12:00:00Z",
        "actor_id": "{{user_id}}",
        "actor_role": "dispatcher",
        "actor": {
          "id": "{{user_id}}",
          "full_name": "Dispatcher User"
        },
        "metadata": {
          "previous_driver": null,
          "new_driver": null,
          "previous_vehicle": null,
          "new_vehicle": null
        }
      },
      {
        "event_type": "driver_switched",
        "previous_status": "picked_up",
        "new_status": "picked_up",
        "timestamp": "2026-04-07T12:35:00Z",
        "actor_id": "{{user_id}}",
        "actor_role": "dispatcher",
        "reason": "Previous driver had vehicle issue",
        "actor": {
          "id": "{{user_id}}",
          "full_name": "Dispatcher User"
        },
        "metadata": {
          "previous_driver_id": "{{driver_id}}",
          "new_driver_id": "{{new_driver_id}}",
          "previous_vehicle_id": "{{vehicle_id}}",
          "new_vehicle_id": "{{new_vehicle_id}}",
          "previous_driver": {
            "id": "{{driver_id}}",
            "user": {
              "full_name": "John Driver"
            }
          },
          "new_driver": {
            "id": "{{new_driver_id}}",
            "user": {
              "full_name": "Backup Driver"
            }
          },
          "previous_vehicle": {
            "id": "{{vehicle_id}}",
            "name": "Van 101"
          },
          "new_vehicle": {
            "id": "{{new_vehicle_id}}",
            "name": "Van 202"
          }
        }
      },
      {
        "event_type": "in_transit",
        "previous_status": "picked_up",
        "new_status": "in_transit",
        "timestamp": "2026-04-07T12:45:00Z",
        "actor_role": "driver",
        "actor": {
          "id": "{{driver_user_id}}",
          "full_name": "Backup Driver"
        },
        "metadata": {
          "previous_driver": null,
          "new_driver": null,
          "previous_vehicle": null,
          "new_vehicle": null
        }
      }
    ]
  }
}
```

### Response 200 Success - Received Waybill

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill retrieved successfully",
  "data": {
    "id": "{{waybill_id}}",
    "status": "received",
    "received_at": "2026-07-29T14:11:47.819096Z",
    "received_by": "{{receiver_user_id}}",
    "received_by_user": {
      "id": "{{receiver_user_id}}",
      "full_name": "Warehouse Receiver",
      "email": "receiver@example.com",
      "phone": "+9647522222222",
      "avatar_url": "https://example.com/users/receiver.jpg"
    },
    "receipt_summary": {
      "verification_mode": "verified",
      "expected": 3,
      "present": 1,
      "missing": 1,
      "damaged": 1,
      "discrepancies": 2,
      "requires_reconciliation": true
    },
    "discrepancies": {
      "present": [
        {
          "delivery_id": "{{present_delivery_id}}",
          "delivery_code": "DEL3231",
          "barcode": "3231"
        }
      ],
      "missing": [
        {
          "id": "{{missing_discrepancy_id}}",
          "delivery_id": "{{missing_delivery_id}}",
          "delivery_code": "DEL3232",
          "barcode": "3232",
          "reason": "missing_on_receive",
          "notes": null,
          "status": "open",
          "reported_by": "{{receiver_user_id}}",
          "resolved_by": null,
          "reported_by_user": {
            "id": "{{receiver_user_id}}",
            "full_name": "Amit Yadav",
            "email": "amit@example.com",
            "phone": "+911234567890",
            "avatar_url": "https://example.com/avatar.png"
          },
          "resolved_by_user": null,
          "created_at": "2026-07-29T14:11:47.819096Z",
          "resolved_at": null
        }
      ],
      "damaged": [
        {
          "id": "{{damaged_discrepancy_id}}",
          "delivery_id": "{{damaged_delivery_id}}",
          "delivery_code": "DEL3233",
          "barcode": "3233",
          "reason": "damaged_on_receive",
          "notes": "Package was damaged",
          "status": "open",
          "reported_by": "{{receiver_user_id}}",
          "resolved_by": null,
          "reported_by_user": {
            "id": "{{receiver_user_id}}",
            "full_name": "Amit Yadav",
            "email": "amit@example.com",
            "phone": "+911234567890",
            "avatar_url": "https://example.com/avatar.png"
          },
          "resolved_by_user": null,
          "created_at": "2026-07-29T14:11:47.819096Z",
          "resolved_at": null
        }
      ],
      "requires_reconciliation": true
    }
  }
}
```

### Response 200 Success - Lost and Physically Returned Parcels

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill retrieved successfully",
  "data": {
    "id": "{{waybill_id}}",
    "status": "received",
    "deliveries": [
      {
        "id": "{{lost_delivery_id}}",
        "status": "returned",
        "return_status": null,
        "is_returned": false,
        "is_lost": true
      },
      {
        "id": "{{returned_delivery_id}}",
        "status": "delivered",
        "return_status": "return_completed",
        "is_returned": true,
        "is_lost": false
      }
    ],
    "summary": {
      "total_deliveries": 2,
      "return_breakdown": {
        "reached_destination": 0,
        "returned": 1,
        "lost": 1,
        "cancelled": 0,
        "active": 0,
        "fully_returned": false
      }
    }
  }
}
```

### Response 404 Error - Waybill Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Waybill not found",
  "data": null
}
```

## GET /delivery/waybills/{{waybill_id}}/deliveries — List Waybill Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#get-list-waybill-deliveries)

Returns the durable Waybill manifest. A current `deliveries.waybill_id` link and the latest non-`removed` `delivery_waybill_history` record both establish membership, so a delivery remains listed after a terminal lost/returned flow clears its live link. A delivery explicitly removed while membership is editable is excluded. `total`, filtering, sorting, and pagination use this same manifest population. Each row exposes `membership_outcome`, `return_status`, `is_returned`, and `is_lost`. `is_lost` is true only for a resolved Missing → Lost discrepancy. `is_returned` is false for Lost and becomes true only after physical return completion or for a legacy terminal returned record without a Lost discrepancy. `returned_at` remains null for Lost and while a physical return is only requested, approved, or in transit. Membership is not implicitly restricted to the Waybill origin warehouse, so legacy rows remain visible when their warehouse fields are null. Send `warehouse_id` only when intentionally filtering the manifest by warehouse.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/deliveries?page=1&limit=20&sort_by=created_at&sort_order=desc&search=&column_filters={}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number, minimum 1 | 1 |
| limit | query string |  | Items per page, from 1 to 100 | 20 |
| sort_by | query string |  | delivery_code, status, barcode, external_reference_id, unit_size, created_at, or customer | created_at |
| sort_order | query string |  | asc or desc | desc |
| search | query string |  | Search delivery code, barcode, external reference, or customer name |  |
| warehouse_id | query string | No | Optional pickup or dropoff warehouse UUID |  |
| column_filters | query string |  | URL-encoded JSON. Supports delivery_code, status, barcode, external_reference_id, pickup, delivery, unit_size, payment_method, cod_amount, created_at, and customer. pickup matches address or pickup zone; delivery matches address or dropoff zone. unit_size and cod_amount accept an exact value or {"min":value,"max":value}. | {} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill deliveries retrieved successfully",
  "data": [
    {
      "id": "{{delivery_id}}",
      "delivery_code": "1533",
      "barcode": "DEL1533",
      "external_reference_id": "ORD-1533",
      "status": "delivered",
      "return_status": "return_approved",
      "membership_outcome": "active",
      "pickup_location": {
        "full_address": "Origin warehouse",
        "latitude": 36.191,
        "longitude": 44.01
      },
      "delivery_location": {
        "full_address": "Customer address",
        "latitude": 36.1901,
        "longitude": 44.0091
      },
      "unit_size": 1,
      "payment_method": "cod",
      "cod_amount": 12000,
      "notes": "Handle with care",
      "created_at": "2026-04-07T11:40:00Z",
      "pickup_zone_id": "{{origin_zone_id}}",
      "dropoff_zone_id": "{{destination_zone_id}}",
      "pickup_warehouse_id": "{{warehouse_id}}",
      "dropoff_warehouse_id": null,
      "is_returned": false,
      "return_reason": "damaged",
      "return_trigger_point": "rejected_at_destination",
      "returned_at": null,
      "pickup_warehouse": {
        "id": "{{warehouse_id}}",
        "name": "Main Warehouse",
        "code": "WH-MAIN"
      },
      "dropoff_warehouse": null,
      "pickup_zone": {
        "id": "{{origin_zone_id}}",
        "name": "Central Pickup Zone",
        "code": "PZ-CENTRAL"
      },
      "dropoff_zone": {
        "id": "{{destination_zone_id}}",
        "name": "Downtown Dropoff Zone",
        "code": "DZ-DOWNTOWN"
      },
      "customer": {
        "id": "{{customer_id}}",
        "full_name": "Customer Name",
        "avatar_url": null
      }
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20
}
```

### Response 200 Success - Lost Parcel Row

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill deliveries retrieved successfully",
  "data": [
    {
      "id": "{{lost_delivery_id}}",
      "status": "returned",
      "return_status": null,
      "is_returned": false,
      "is_lost": true,
      "returned_at": null
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20
}
```

### Response 404 Error - Waybill Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_not_found",
  "message": "Waybill not found",
  "data": null
}
```

## PUT /delivery/waybills/{{waybill_id}} — Update Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#put-update-waybill)

Update a drafted waybill only.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| notes | string |  |  |  |
| carrier_type | string |  |  |  |



### Request body (raw)

```json
{
  "notes": "Updated notes",
  "carrier_type": "internal"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill updated successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": {
      "id": "{{warehouse_address_id}}",
      "full_address": "Main Warehouse, Erbil",
      "latitude": 36.191,
      "longitude": 44.01,
      "city": "Erbil",
      "state": null,
      "country": "Iraq",
      "postal_code": null,
      "country_code": "IQ",
      "contact_info": null
    },
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "carrier_type": "internal",
    "driver_id": "{{driver_id}}",
    "vehicle_id": "{{vehicle_id}}",
    "external_carrier_ref": null,
    "assigned_by": "{{user_id}}",
    "assignment_time": "2026-04-07T12:20:00Z",
    "status": "sealed",
    "sealed_at": "2026-04-07T12:15:00Z",
    "dispatched_at": null,
    "received_at": null,
    "received_by": null,
    "closed_at": null,
    "cancelled_at": null,
    "proof_of_delivery": null,
    "notes": "Updated notes",
    "created_by": "{{user_id}}",
    "created_at": "2026-04-07T12:00:00Z",
    "updated_at": "2026-04-07T12:20:00Z"
  }
}
```

### Response 409 Error - Waybill Not Editable

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Waybill cannot be edited in its current status",
  "data": null
}
```

## GET /delivery/waybills/{{waybill_id}}/scan/{{scan_value}} — Scan Eligible Delivery for Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#get-scan-eligible-delivery-for-waybill)

Validate one scanned delivery without attaching it to the waybill. The path value is matched exactly in this order: barcode, external_reference_id, then delivery_code (with an optional DEL prefix). Success returns the same delivery field shape used in waybill details, including locations, payment, zones, warehouses, and return fields. A staged P&D parent is scannable with a Draft outbound child or an untouched Pending outbound preparation; attaching the latter atomically supersedes its old P&D pick and reservation. The frontend must add data.id to shared local selection state, handle duplicate scans locally, and submit all selected IDs once through POST /delivery/waybills/{id}/deliveries. Important errors return a machine-readable code: delivery_not_found (404), delivery_already_in_waybill, delivery_in_active_route_session, delivery_parent_not_eligible, delivery_status_not_eligible, delivery_has_active_assignment, staged_outbound_handoff_active, warehouse_preparation_inconsistent, or waybill_membership_locked (409).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/scan/{{scan_value}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |
| scan_value | path string | Yes | URL-encoded barcode, external_reference_id, or delivery_code | DEL1533 |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Eligible delivery retrieved successfully",
  "data": {
    "id": "3bebb14a-3179-4b34-8a65-6e52afa2870f",
    "delivery_code": "3033",
    "barcode": "DEL3033",
    "external_reference_id": "",
    "status": "pending",
    "pickup_location": {
      "id": "c8eed194-887e-4da9-b312-2d04d8edc4d8",
      "city": "Baghdad",
      "state": null,
      "country": "Iraq",
      "latitude": 33.29605487,
      "longitude": 44.3399507,
      "postal_code": null,
      "contact_info": null,
      "country_code": null,
      "full_address": "1234, Yarmouk, Baghdad, Baghdad Governorate, Iraq"
    },
    "delivery_location": {
      "id": "03fc2031-1778-4355-874f-4589287f3cde",
      "city": "Lajan",
      "state": null,
      "country": "Iraq",
      "latitude": 36.125664,
      "longitude": 43.79169851,
      "postal_code": null,
      "contact_info": null,
      "country_code": null,
      "full_address": "206, lajan, Lajan, Erbil Governorate, Iraq"
    },
    "unit_size": 1,
    "payment_method": "cash",
    "cod_amount": 10000,
    "notes": "",
    "created_at": "2026-05-22T12:33:54.61787+00:00",
    "pickup_zone_id": "23ba845d-faf0-4241-ad50-86a438d98853",
    "dropoff_zone_id": "4f04820c-e9de-4449-ad12-2cd3758ac09b",
    "pickup_warehouse_id": "3a577909-49a2-4819-a54b-0559e7dec3ce",
    "dropoff_warehouse_id": "8e2f5943-8500-49cc-8830-f12a3fc40d4e",
    "is_returned": false,
    "return_reason": null,
    "return_trigger_point": null,
    "returned_at": null
  }
}
```

### Response 404 Error - Delivery Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_not_found",
  "message": "Delivery not found",
  "data": null
}
```

### Response 409 Error - Delivery Not Eligible

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_status_not_eligible",
  "message": "Delivery status is not eligible for waybill attachment",
  "data": null
}
```

## GET /delivery/waybills/{{waybill_id}}/eligible-deliveries — List Eligible Deliveries for Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#get-list-eligible-deliveries-for-waybill)

Returns deliveries that can be attached to the target drafted Waybill. Direct outbound Draft/Pending deliveries keep their existing eligibility. A staged P&D parent is also included when its outbound child is Draft, or Pending with one completed P&D pick and full reservation, no assignment, route, handoff, custody movement, or open exception. The backend resolves that derived eligibility in one set-based database query before filtering, counting, sorting, and pagination. The response includes customer information. Frontend sends column_filters as URL-encoded JSON and handles 400 validation errors for invalid JSON, keys, value types, sort fields, or date values; 409 means Waybill membership is locked.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/eligible-deliveries?page=1&limit=25&sort_by=created_at&sort_order=desc&search=&column_filters={}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number, minimum 1 | 1 |
| limit | query string |  | Items per page, from 1 to 100 | 25 |
| sort_by | query string |  | Sort field: delivery_code, status, barcode, external_reference_id, unit_size, created_at, or customer | created_at |
| sort_order | query string |  | Sort direction: asc or desc | desc |
| search | query string |  | Global partial search by delivery code, barcode, external reference, or customer name |  |
| warehouse_id | query string | No | Optional warehouse UUID. Matches pickup or dropoff warehouse. |  |
| column_filters | query string |  | URL-encoded JSON. Supports delivery_code, status, barcode, external_reference_id, pickup, delivery, unit_size, payment_method, cod_amount, created_at, and customer. pickup matches address or pickup zone; delivery matches address or dropoff zone. unit_size and cod_amount accept an exact value or {"min":value,"max":value}. | {} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Eligible deliveries retrieved successfully",
  "data": [
    {
      "id": "{{delivery_id}}",
      "delivery_code": "1533",
      "barcode": "DEL1533",
      "external_reference_id": "ORD-1533",
      "status": "pending",
      "unit_size": 1,
      "payment_method": "cod",
      "cod_amount": 12000,
      "notes": "Handle with care",
      "pickup_location": {
        "full_address": "Origin warehouse",
        "latitude": 36.191,
        "longitude": 44.01
      },
      "delivery_location": {
        "full_address": "Customer address",
        "latitude": 36.1901,
        "longitude": 44.0091
      },
      "pickup_warehouse_id": "{{warehouse_id}}",
      "dropoff_warehouse_id": null,
      "pickup_warehouse": {
        "id": "{{warehouse_id}}",
        "name": "Main Warehouse",
        "code": "WH-MAIN"
      },
      "dropoff_warehouse": null,
      "pickup_zone": {
        "id": "{{pickup_zone_id}}",
        "name": "Central Pickup Zone",
        "code": "PZ-CENTRAL"
      },
      "dropoff_zone": {
        "id": "{{dropoff_zone_id}}",
        "name": "Downtown Dropoff Zone",
        "code": "DZ-DOWNTOWN"
      },
      "customer": {
        "id": "{{customer_id}}",
        "full_name": "Customer Name",
        "avatar_url": null
      },
      "created_at": "2026-04-07T11:40:00Z",
      "updated_at": "2026-04-07T11:45:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 25
}
```

### Response 409 Error - Waybill Membership Locked

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Deliveries cannot be added or removed after waybill is sealed",
  "data": null
}
```

## POST /delivery/waybills/{{waybill_id}}/deliveries — Add Deliveries to Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-add-deliveries-to-waybill)

Attach selected deliveries to a drafted Waybill. Use the eligible-deliveries endpoint first so the dispatcher sees only addable deliveries. A staged P&D parent may have a Draft outbound child or an untouched Pending outbound preparation. For Pending, attachment preserves the existing P&D picking order as Cancelled/superseded, releases its stock reservation, cancels pending auto-assignment, restores the outbound child to Draft, and attaches the parent in the same transaction. It never reuses that per-delivery pick: Waybill seal creates the single batch picking order. Active assignment, route, handoff, custody movement, or an open exception returns a localized 409 conflict.

For an eligible warehouse-to-warehouse Waybill, the first delivery attachment also creates the Waybill package-transfer projection in the same transaction. After success, the Waybill is discoverable through the source warehouse's Outgoing Package Transfers list and the destination warehouse's Incoming list. Empty Waybills, same-warehouse routes, and routes without a valid warehouse destination do not create that projection.

The database performs validation, attachment, transfer projection, summary calculation and realtime item-count synchronization atomically. The success payload is a compact authoritative Waybill with `item_count`, `summary`, endpoint context and `operation_result.attached` plus `operation_result.attached_delivery_ids`; it intentionally omits embedded delivery and timeline detail.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "delivery_ids": [
    "{{delivery_id}}"
  ]
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Deliveries added to waybill successfully",
  "data": {
    "id": "{{waybill_id}}",
    "waybill_number": "WB00001",
    "status": "drafted",
    "item_count": 2,
    "summary": {
      "total_deliveries": 2,
      "total_unit_size": 2,
      "status_breakdown": {
        "pending": 2
      }
    },
    "operation_result": {
      "attached": 1,
      "attached_delivery_ids": [
        "{{delivery_id}}"
      ],
      "superseded_delivery_ids": [],
      "superseded_picking_order_ids": []
    }
  }
}
```

### Response 409 Error - Delivery Has Active Assignment

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery has an active assignment. Unassign first before adding to waybill",
  "data": null
}
```

## DELETE /delivery/waybills/{{waybill_id}}/deliveries/{{delivery_id}} — Remove Delivery from Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#delete-remove-delivery-from-waybill)

Atomically remove one delivery from a drafted Waybill. The success payload is the compact authoritative Waybill with updated `item_count`, `summary`, endpoint context and `operation_result.removed_delivery_id`; it intentionally omits embedded delivery and timeline detail.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/deliveries/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery removed from waybill successfully",
  "data": {
    "id": "{{waybill_id}}",
    "waybill_number": "WB00001",
    "status": "drafted",
    "item_count": 1,
    "summary": {
      "total_deliveries": 1,
      "total_unit_size": 1,
      "status_breakdown": {
        "pending": 1
      }
    },
    "operation_result": {
      "removed_delivery_id": "{{delivery_id}}"
    }
  }
}
```

### Response 409 Error - Delivery Not In Waybill

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery is not linked to this waybill",
  "data": null
}
```

## POST /delivery/waybills/{{waybill_id}}/seal — Seal Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-seal-waybill)

Seal a drafted waybill. After this, membership is locked and no deliveries can be added/removed. For a warehouse origin, sealing creates or reuses the pending picking order; stock remains in the origin warehouse until physical carrier handoff.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/seal

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill sealed successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": null,
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "carrier_type": "internal",
    "driver_id": "{{driver_id}}",
    "vehicle_id": "{{vehicle_id}}",
    "external_carrier_ref": null,
    "assigned_by": "{{user_id}}",
    "assignment_time": "2026-04-07T12:20:00Z",
    "status": "sealed",
    "sealed_at": "2026-04-07T12:15:00Z",
    "dispatched_at": null,
    "received_at": null,
    "received_by": "{{receiver_user_id}}",
    "closed_at": null,
    "cancelled_at": null,
    "proof_of_delivery": null,
    "notes": "Batch for afternoon dispatch",
    "created_by": "{{user_id}}",
    "created_at": "2026-04-07T12:00:00Z",
    "updated_at": "2026-04-07T12:20:00Z",
    "driver": {
      "id": "{{driver_id}}",
      "status": "active",
      "user": {
        "id": "{{driver_user_id}}",
        "full_name": "John Driver",
        "email": "driver@example.com",
        "phone": "+9647500000000",
        "avatar_url": "https://example.com/driver.jpg"
      }
    },
    "vehicle": {
      "id": "{{vehicle_id}}",
      "name": "Van 101",
      "code": "VAN-101",
      "capacity_unit": 50,
      "image_url": "https://example.com/vehicles/van-101.jpg"
    },
    "origin_warehouse": {
      "id": "{{warehouse_id}}",
      "name": "Main Warehouse",
      "code": "WH-MAIN"
    },
    "destination_warehouse": null,
    "origin_zone": {
      "id": "{{origin_zone_id}}",
      "name": "Erbil Central",
      "code": "ERB-CENTRAL"
    },
    "destination_zone": {
      "id": "{{destination_zone_id}}",
      "name": "Erbil East",
      "code": "ERB-EAST"
    },
    "assigned_by_user": {
      "id": "{{user_id}}",
      "full_name": "Dispatcher User",
      "email": "dispatcher@example.com",
      "phone": "+9647511111111",
      "avatar_url": "https://example.com/users/dispatcher.jpg"
    },
    "received_by_user": {
      "id": "{{receiver_user_id}}",
      "full_name": "Warehouse Receiver",
      "email": "receiver@example.com",
      "phone": "+9647522222222",
      "avatar_url": "https://example.com/users/receiver.jpg"
    },
    "total_unit_size": 1,
    "deliveries": [
      {
        "id": "{{delivery_id}}",
        "delivery_code": 1533,
        "barcode": "DEL1533",
        "external_reference_id": "ORD-1533",
        "status": "pending",
        "pickup_location": {
          "full_address": "Origin warehouse",
          "latitude": 36.191,
          "longitude": 44.01
        },
        "delivery_location": {
          "full_address": "Customer address",
          "latitude": 36.1901,
          "longitude": 44.0091
        },
        "unit_size": 1,
        "payment_method": "cod",
        "cod_amount": 12000,
        "notes": "Handle with care",
        "created_at": "2026-04-07T11:40:00Z",
        "pickup_zone_id": "{{origin_zone_id}}",
        "dropoff_zone_id": "{{destination_zone_id}}",
        "pickup_warehouse_id": "{{warehouse_id}}",
        "dropoff_warehouse_id": null
      }
    ],
    "summary": {
      "total_deliveries": 1,
      "total_unit_size": 1,
      "status_breakdown": {
        "pending": 1
      }
    }
  }
}
```

### Response 409 Error - No Deliveries

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Waybill must have at least one delivery to seal",
  "data": null
}
```

## POST /delivery/waybills/{{waybill_id}}/assign — Assign Driver to Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-assign-driver-to-waybill)

Assign an internal sealed waybill to a driver using the driver primary id from `drivers.id`. Backend derives the assigned vehicle from the driver's primary vehicle assignment, stores both ids on the waybill, changes the waybill status to `assigned`, marks pending child deliveries as `assigned`, and marks child delivery assignment rows as `assigned` immediately. Waybills do not use the normal delivery accept/reject gate, so the driver can pick up the Waybill after assignment.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/assign

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | string |  |  |  |



### Request body (raw)

```json
{
  "driver_id": "{{driver_id}}"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill assigned to driver successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": null,
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "carrier_type": "internal",
    "driver_id": "{{driver_id}}",
    "vehicle_id": "{{vehicle_id}}",
    "external_carrier_ref": null,
    "assigned_by": "{{user_id}}",
    "assignment_time": "2026-04-07T12:20:00Z",
    "status": "assigned",
    "sealed_at": "2026-04-07T12:15:00Z",
    "dispatched_at": null,
    "received_at": null,
    "received_by": null,
    "closed_at": null,
    "cancelled_at": null,
    "proof_of_delivery": null,
    "notes": "Batch for afternoon dispatch",
    "created_by": "{{user_id}}",
    "created_at": "2026-04-07T12:00:00Z",
    "updated_at": "2026-04-07T12:20:00Z"
  }
}
```

### Response 409 Error - Driver Not Found

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not found",
  "data": null
}
```

### Response 409 Error - Waybill Not Assignable

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Waybill must be sealed before assigning a driver",
  "data": null
}
```

## POST /delivery/waybills/{{waybill_id}}/switch-driver — Switch Waybill Driver

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-switch-waybill-driver)

Creates one Waybill-level `waybill_driver_switched` notification per affected driver, never one per child delivery. Before pickup the previous driver receives a critical acknowledgement modal and the replacement receives a normal notification. After pickup/in transit the previous driver receives a critical modal with replacement-driver contact and coordination instructions; the replacement receives an important banner with previous-driver contact. Phone/avatar are nullable and copy/call actions are omitted without phone. Notification work is backgrounded.

Switch the assigned driver of an internal waybill. `reason` is mandatory, must be 1-500 characters, and is persisted in the `driver_switched` timeline event. Allowed in `assigned`, `picked_up`, and `in_transit` for dispatcher custody transfer. Backend derives the new vehicle from the new driver's primary vehicle assignment and records the previous/new driver and vehicle snapshots.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/switch-driver

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | string |  |  |  |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "driver_id": "{{new_driver_id}}",
  "reason": "Previous driver had vehicle issue"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill driver switched successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": null,
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "carrier_type": "internal",
    "driver_id": "{{new_driver_id}}",
    "vehicle_id": "{{new_vehicle_id}}",
    "external_carrier_ref": null,
    "assigned_by": "{{user_id}}",
    "assignment_time": "2026-04-07T12:35:00Z",
    "status": "picked_up",
    "sealed_at": "2026-04-07T12:15:00Z",
    "dispatched_at": "2026-04-07T12:30:00Z",
    "received_at": null,
    "received_by": null,
    "closed_at": null,
    "cancelled_at": null,
    "proof_of_delivery": null,
    "notes": "Batch for afternoon dispatch",
    "created_by": "{{user_id}}",
    "created_at": "2026-04-07T12:00:00Z",
    "updated_at": "2026-04-07T12:35:00Z",
    "driver": {
      "id": "{{new_driver_id}}",
      "status": "active",
      "user": {
        "id": "{{new_driver_user_id}}",
        "full_name": "Backup Driver",
        "email": "backup.driver@example.com",
        "phone": "+9647500000001",
        "avatar_url": "https://example.com/driver2.jpg"
      }
    },
    "vehicle": {
      "id": "{{new_vehicle_id}}",
      "name": "Van 202",
      "code": "VAN-202",
      "capacity_unit": 50,
      "image_url": "https://example.com/vehicles/van-202.jpg"
    },
    "origin_warehouse": {
      "id": "{{warehouse_id}}",
      "name": "Main Warehouse",
      "code": "WH-MAIN"
    },
    "destination_warehouse": null,
    "origin_zone": {
      "id": "{{origin_zone_id}}",
      "name": "Erbil Central",
      "code": "ERB-CENTRAL"
    },
    "destination_zone": {
      "id": "{{destination_zone_id}}",
      "name": "Erbil East",
      "code": "ERB-EAST"
    },
    "assigned_by_user": {
      "id": "{{user_id}}",
      "full_name": "Dispatcher User",
      "email": "dispatcher@example.com",
      "phone": "+9647511111111",
      "avatar_url": "https://example.com/users/dispatcher.jpg"
    },
    "received_by_user": null,
    "total_unit_size": 1,
    "deliveries": [
      {
        "id": "{{delivery_id}}",
        "delivery_code": 1533,
        "barcode": "DEL1533",
        "external_reference_id": "ORD-1533",
        "status": "picked_up",
        "pickup_location": {
          "full_address": "Origin warehouse",
          "latitude": 36.191,
          "longitude": 44.01
        },
        "delivery_location": {
          "full_address": "Customer address",
          "latitude": 36.1901,
          "longitude": 44.0091
        },
        "unit_size": 1,
        "payment_method": "cod",
        "cod_amount": 12000,
        "notes": "Handle with care",
        "created_at": "2026-04-07T11:40:00Z",
        "pickup_zone_id": "{{origin_zone_id}}",
        "dropoff_zone_id": "{{destination_zone_id}}",
        "pickup_warehouse_id": "{{warehouse_id}}",
        "dropoff_warehouse_id": null
      }
    ],
    "summary": {
      "total_deliveries": 1,
      "total_unit_size": 1,
      "status_breakdown": {
        "picked_up": 1
      }
    }
  }
}
```

### Response 400 Error - Switch Reason Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required",
  "details": [
    {
      "field": "reason",
      "message": "Field required"
    }
  ]
}
```

### Response 409 Error - Same Driver

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This driver is already assigned to the waybill",
  "data": null
}
```

## POST /delivery/waybills/{{waybill_id}}/unassign — Unassign Waybill Driver

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-unassign-waybill-driver)

The previous driver receives one Waybill-level `waybill_driver_unassigned` critical modal requiring acknowledgement. Notification work is backgrounded and does not block the API response.

Unassign the current driver from an internal waybill. Allowed only while the waybill is `assigned`. After pickup, dispatcher must use switch-driver instead of unassign.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/unassign

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Request body (raw)

```json
{}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill driver unassigned successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": null,
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "carrier_type": "internal",
    "driver_id": null,
    "vehicle_id": null,
    "external_carrier_ref": null,
    "assigned_by": null,
    "assignment_time": null,
    "status": "sealed",
    "sealed_at": "2026-04-07T12:15:00Z",
    "dispatched_at": null,
    "received_at": null,
    "received_by": null,
    "closed_at": null,
    "cancelled_at": null,
    "proof_of_delivery": null,
    "notes": "Batch for afternoon dispatch",
    "created_by": "{{user_id}}",
    "created_at": "2026-04-07T12:00:00Z",
    "updated_at": "2026-04-07T12:40:00Z",
    "driver": null,
    "vehicle": null,
    "origin_warehouse": {
      "id": "{{warehouse_id}}",
      "name": "Main Warehouse",
      "code": "WH-MAIN"
    },
    "destination_warehouse": null,
    "origin_zone": {
      "id": "{{origin_zone_id}}",
      "name": "Erbil Central",
      "code": "ERB-CENTRAL"
    },
    "destination_zone": {
      "id": "{{destination_zone_id}}",
      "name": "Erbil East",
      "code": "ERB-EAST"
    },
    "assigned_by_user": null,
    "received_by_user": null,
    "total_unit_size": 1,
    "deliveries": [
      {
        "id": "{{delivery_id}}",
        "delivery_code": 1533,
        "barcode": "DEL1533",
        "external_reference_id": "ORD-1533",
        "status": "pending",
        "pickup_location": {
          "full_address": "Origin warehouse",
          "latitude": 36.191,
          "longitude": 44.01
        },
        "delivery_location": {
          "full_address": "Customer address",
          "latitude": 36.1901,
          "longitude": 44.0091
        },
        "unit_size": 1,
        "payment_method": "cod",
        "cod_amount": 12000,
        "notes": "Handle with care",
        "created_at": "2026-04-07T11:40:00Z",
        "pickup_zone_id": "{{origin_zone_id}}",
        "dropoff_zone_id": "{{destination_zone_id}}",
        "pickup_warehouse_id": "{{warehouse_id}}",
        "dropoff_warehouse_id": null
      }
    ],
    "summary": {
      "total_deliveries": 1,
      "total_unit_size": 1,
      "status_breakdown": {
        "pending": 1
      }
    }
  }
}
```

### Response 409 Error - Cannot Unassign After Pickup

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unassigning after pickup is not allowed",
  "data": null
}
```

## POST /delivery/waybills/{{waybill_id}}/dispatch — Dispatch Waybill (External Carrier)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-dispatch-waybill-external-carrier)

Move a non-internal Waybill from sealed to in_transit. For a warehouse origin, this is the physical carrier handoff boundary: the picking order and outbound handoffs complete, stock is removed from the origin warehouse, and each parcel receives a `transfer_out` movement. Internal carrier Waybills should use driver-action instead.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/dispatch

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill dispatched successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": null,
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "carrier_type": "external",
    "driver_id": null,
    "vehicle_id": null,
    "external_carrier_ref": null,
    "assigned_by": "{{user_id}}",
    "assignment_time": "2026-04-07T12:20:00Z",
    "status": "in_transit",
    "sealed_at": "2026-04-07T12:15:00Z",
    "dispatched_at": "2026-04-07T12:30:00Z",
    "received_at": null,
    "received_by": null,
    "closed_at": null,
    "cancelled_at": null,
    "proof_of_delivery": null,
    "notes": "Batch for afternoon dispatch",
    "created_by": "{{user_id}}",
    "created_at": "2026-04-07T12:00:00Z",
    "updated_at": "2026-04-07T12:20:00Z"
  }
}
```

### Response 409 Error - Internal Carrier Must Use Driver Action

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Use driver action endpoint for internal carrier waybills",
  "data": null
}
```

## POST /delivery/waybills/{{waybill_id}}/receive — Receive Waybill (External Carrier, Location Destination)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-receive-waybill-external-carrier-location-destination)

Complete an external/prime carrier Waybill whose destination is a customer/location. `proof_of_delivery` is optional. Send it only when proof data exists. Warehouse destinations use the delivery-code verified receipt contract documented under Receive Warehouse Waybill. For internal customer/location flow, use driver-action instead.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/receive

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| proof_of_delivery | object |  |  |  |
| proof_of_delivery.signature_url | string |  |  |  |
| proof_of_delivery.photo_urls | array<string> |  |  |  |
| proof_of_delivery.notes | string |  |  |  |



### Request body (raw)

```json
{
  "proof_of_delivery": {
    "signature_url": "https://example.com/signature.png",
    "photo_urls": [
      "https://example.com/photo1.jpg"
    ],
    "notes": "Received in good condition"
  }
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill received successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": null,
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "carrier_type": "external",
    "driver_id": null,
    "vehicle_id": null,
    "external_carrier_ref": null,
    "assigned_by": "{{user_id}}",
    "assignment_time": "2026-04-07T12:20:00Z",
    "status": "received",
    "sealed_at": "2026-04-07T12:15:00Z",
    "dispatched_at": null,
    "received_at": "2026-04-07T14:00:00Z",
    "received_by": null,
    "closed_at": null,
    "cancelled_at": null,
    "proof_of_delivery": {
      "signature_url": "https://example.com/signature.png",
      "photo_urls": [
        "https://example.com/photo1.jpg"
      ],
      "notes": "Received in good condition"
    },
    "notes": "Batch for afternoon dispatch",
    "created_by": "{{user_id}}",
    "created_at": "2026-04-07T12:00:00Z",
    "updated_at": "2026-04-07T12:20:00Z"
  }
}
```

### Response 200 Success Response - Without Proof

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill received successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": null,
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "carrier_type": "external",
    "driver_id": null,
    "vehicle_id": null,
    "external_carrier_ref": null,
    "assigned_by": "{{user_id}}",
    "assignment_time": "2026-04-07T12:20:00Z",
    "status": "received",
    "sealed_at": "2026-04-07T12:15:00Z",
    "dispatched_at": null,
    "received_at": "2026-04-07T14:00:00Z",
    "received_by": null,
    "closed_at": null,
    "cancelled_at": null,
    "proof_of_delivery": null,
    "notes": "Batch for afternoon dispatch",
    "created_by": "{{user_id}}",
    "created_at": "2026-04-07T12:00:00Z",
    "updated_at": "2026-04-07T12:20:00Z"
  }
}
```

### Response 409 Error - Invalid Transition

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid waybill status transition",
  "data": null
}
```

## POST /delivery/waybills/{{waybill_id}}/close — Close Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-close-waybill)

Close a received Waybill after reconciliation is complete. Warehouse-transfer order history is created earlier, when present/damaged deliveries are marked received. `warehouse_transfer_completed` is written to the affected delivery's own event history and to its canonical parent's aggregate history; a direct/root member is written once. Close does not add a duplicate because the event is deduplicated by Waybill ID; the deployed close projection remains only as a fallback for legacy rows that reached close without a receipt event. `status_updates`, delivery/assignment status, custody, capacity, finance, realtime, notifications, and webhooks are unchanged by the order-history projection. HTTP 409 returns `waybill_not_closable` when the Waybill is not received or `waybill_open_discrepancies` with `details.open_discrepancy_count` when receipt discrepancies remain open.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/close

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill closed successfully",
  "data": {
    "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
    "tenant_id": "{{tenant_id}}",
    "waybill_number": "WB00001",
    "origin_type": "warehouse",
    "origin_warehouse_id": "{{warehouse_id}}",
    "origin_location": null,
    "destination_type": "location",
    "destination_warehouse_id": null,
    "destination_location": {
      "latitude": 36.1901,
      "longitude": 44.0091,
      "full_address": "Erbil, Kurdistan Region"
    },
    "carrier_type": "internal",
    "driver_id": "{{driver_id}}",
    "vehicle_id": "{{vehicle_id}}",
    "external_carrier_ref": null,
    "assigned_by": "{{user_id}}",
    "assignment_time": "2026-04-07T12:20:00Z",
    "status": "closed",
    "sealed_at": "2026-04-07T12:15:00Z",
    "dispatched_at": null,
    "received_at": "2026-04-07T14:00:00Z",
    "received_by": null,
    "closed_at": "2026-04-07T14:15:00Z",
    "cancelled_at": null,
    "proof_of_delivery": null,
    "notes": "Batch for afternoon dispatch",
    "created_by": "{{user_id}}",
    "created_at": "2026-04-07T12:00:00Z",
    "updated_at": "2026-04-07T12:20:00Z"
  }
}
```

### Response 409 Error - Invalid Transition

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_not_closable",
  "message": "Waybill must be received before it can be closed",
  "details": {
    "current_status": "awaiting_receipt",
    "required_status": "received"
  },
  "data": null
}
```

### Response 409 Error - Open Receipt Discrepancies

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_open_discrepancies",
  "message": "Resolve all open receipt discrepancies before closing the waybill",
  "details": {
    "open_discrepancy_count": 2
  },
  "data": null
}
```

## POST /delivery/waybills/bulk-delete-empty-drafts — Bulk Delete Empty Draft Waybills

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-bulk-delete-empty-draft-waybills)

Permanently delete empty drafted Waybills using exactly one scope.

### Selected scope

send `waybill_ids` with 1-100 IDs.

### All-empty scope

send `{ "all_empty_drafts": true }` to delete every Waybill in the authenticated tenant that is exactly `drafted` and has zero attached deliveries.

Do not send both fields. Missing both returns validation error `waybill_bulk_delete_scope_required`; sending both returns `waybill_bulk_delete_scope_conflict`. These messages and the success/conflict responses are localized for English, Arabic, Kurdish, and Persian through the existing request language.

### Frontend contract
- Ask the user to choose selected rows or all empty drafts and require confirmation before the request.
- Remove only IDs returned in `data.deleted_ids`.
- Use `data.scope` (`selected` or `all_empty_drafts`) to confirm which operation ran.
- For selected scope, keep rows returned in `data.skipped` and use `reason` (`not_drafted` or `has_deliveries`) for feedback.
- Treat `data.not_found` as stale selected rows.
- A 409 `waybill_bulk_delete_conflict` means eligibility changed during deletion; refresh before retrying.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/bulk-delete-empty-drafts

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "waybill_ids": [
    "{{waybill_id}}",
    "{{waybill_id_2}}"
  ]
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Empty drafted Waybills processed successfully",
  "data": {
    "scope": "selected",
    "deleted_count": 2,
    "deleted_ids": [
      "{{waybill_id}}",
      "{{waybill_id_2}}"
    ],
    "skipped": [
      {
        "id": "{{waybill_id_3}}",
        "reason": "has_deliveries"
      }
    ],
    "not_found": []
  }
}
```

### Response 200 Success Response - All Empty Drafts

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Empty drafted Waybills processed successfully",
  "data": {
    "scope": "all_empty_drafts",
    "deleted_count": 6,
    "deleted_ids": [
      "waybill-uuid-1",
      "waybill-uuid-2"
    ],
    "skipped": [
      {
        "id": "waybill-uuid-with-deliveries",
        "reason": "has_deliveries"
      }
    ],
    "not_found": []
  }
}
```

## POST /delivery/waybills/{{waybill_id}}/cancel — Cancel Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-cancel-waybill)

Cancellation delegates to the canonical atomic Waybill lifecycle owner. The assigned driver is captured inside that operation and receives one Waybill-level `waybill_cancelled` critical modal requiring acknowledgement. Push failure is recorded per channel without hiding the available in-app notification. The success payload is a compact authoritative Waybill with `status: cancelled`, `item_count`, `summary`, endpoint context and lifecycle `operation_result`; it intentionally omits embedded delivery and timeline detail.

Cancel a drafted, sealed, or assigned waybill before active transport starts.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/cancel

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill cancelled successfully",
  "data": {
    "id": "{{waybill_id}}",
    "waybill_number": "WB00001",
    "status": "cancelled",
    "driver_id": null,
    "vehicle_id": null,
    "cancelled_at": "2026-04-07T12:25:00Z",
    "item_count": 1,
    "summary": {
      "total_deliveries": 1,
      "total_unit_size": 1,
      "status_breakdown": {
        "pending": 1
      }
    },
    "operation_result": {
      "action": "cancel",
      "status": "cancelled"
    }
  }
}
```

### Response 409 Error - Waybill Not Cancellable

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Waybill cannot be cancelled in its current status",
  "data": null
}
```

## PATCH /delivery/waybills/{{waybill_id}}/status — Change Waybill Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#patch-change-waybill-status)

Changes a Waybill directly to an allowed non-receipt target status. Operational shortcuts write one final Waybill transition and atomically calculate the final child-delivery, child-assignment, driver-capacity, custody, and audit state; intermediate Waybill statuses are not persisted.

### Method and authorization

`PATCH /delivery/waybills/{waybill_id}/status`. Dispatcher/admin or a user with Waybill management permission can perform direct target transitions. An authenticated assigned driver can advance their own already-assigned internal Waybill. A driver cannot self-assign while crossing the assignment boundary. Origin warehouse staff may seal their drafted warehouse-origin Waybill. Destination warehouse staff may close their received warehouse-destination Waybill.

### Direct internal targets

`drafted` may target `sealed`, `assigned`, `picked_up`, `in_transit`, `awaiting_receipt` for a warehouse destination, or `cancelled`; `sealed` may target `assigned`, `picked_up`, `in_transit`, `awaiting_receipt` for a warehouse destination, or `cancelled`; `assigned` may target `sealed`, `picked_up`, `in_transit`, `awaiting_receipt` for a warehouse destination, or `cancelled`; `picked_up` may target `in_transit` or warehouse `awaiting_receipt`; `in_transit` may target warehouse `awaiting_receipt`; `received` may target `closed`. `received` is never an accepted target of this endpoint.

### External/prime targets

`drafted` or `sealed` may target `in_transit`. External/prime carriers do not use `assigned`, `picked_up`, or `awaiting_receipt`.

### Conditional request context

`status` and a non-blank `reason` are always required. `reason` is limited to 500 characters. Optional `notes` is limited to 2000 characters. Both values are returned on the matching Waybill status timeline event; retries cannot replace existing audit details. When an internal transition starts before `assigned` (`drafted` or `sealed`) and targets `assigned`, `picked_up`, `in_transit`, or `awaiting_receipt`, `driver_id` is required. Every new transition to `awaiting_receipt` requires at least one of `signature_url` or a non-empty `photo_urls`; a same-status retry does not require proof again. The backend derives the target delivery and assignment statuses; the client never sends child statuses or an internal action name.

### Direct target effects

A jump from `drafted` crosses the seal boundary and creates the warehouse picking order when the origin is a warehouse, without persisting `sealed`; stock remains in the origin warehouse at this reservation step. Crossing assignment validates the active driver, primary vehicle, vehicle capacity, and zones, reserves capacity, and creates or updates child assignments directly to their final target state. Crossing pickup directly updates active children and assignment timestamps, performs idempotent sender-cash pickup accounting for a location origin, and for a warehouse origin atomically completes picking/handoffs, removes origin stock, records `transfer_out`, and moves custody to transit. `awaiting_receipt` leaves moving children `in_transit`, completes child assignments, records handover proof, and releases capacity once. Same-target retries are successful only with matching reason/notes and retry idempotent finance/paperwork without adding another Waybill timeline event.

### Dedicated receipt flows

Do not send `status: received` here. Warehouse staff and external-carrier receipt use `POST /delivery/waybills/{id}/receive`, including the manifest verification and `Idempotency-Key` contract documented there. An assigned internal driver completing a customer/location Waybill uses `POST /delivery/waybills/{id}/driver-action` with `action: received`. These dedicated flows retain their existing delivery, payment, proof, receipt, discrepancy, custody, and earning side effects.

### Response contract

The enriched Waybill detail returns top-level `allowed_next_statuses` and `status_transition_options`, and neither collection advertises `received`. Each transition option contains `status`, `required_fields`, `required_headers`, `required_one_of`, and `conditional_fields`; every option includes `reason` in `required_fields`. `data.operation_result` preserves action-specific results and adds `previous_status`, `requested_status`, authoritative `status`, `reason`, `notes`, `allowed_next_statuses`, and `status_transition_options`.

### Important errors

HTTP 400 includes missing/invalid `reason`, invalid input, `waybill_target_driver_required`, and `waybill_handover_proof_required`; HTTP 403 means the actor cannot perform the target; HTTP 404 includes `waybill_not_found` and `driver_not_found`; HTTP 409 `invalid_waybill_transition` is returned for `received` and other unavailable targets, while other conflicts include `waybill_status_context_conflict`, `waybill_driver_mismatch`, and capacity/zone/driver conflicts. Unexpected service failures are logged and return HTTP 503 with localized code `waybill_status_change_failed` plus `details.operation_id`; the endpoint does not expose raw database errors.

### Frontend integration contract

Render target choices from `data.status_transition_options`; `received` will not appear. Before submitting a target, collect every `required_fields` value, every `required_headers` value, one value from each `required_one_of` group, and fields activated by `conditional_fields`. Send the desired non-receipt `status` and required `reason`, not an action. Send optional `notes` when staff provide more context. Use the dedicated receipt endpoint or driver action for `received`. After success replace local Waybill detail with `data`, treat `data.status` as authoritative, and display `reason` plus optional `notes` from `data.status_updates`. Branch on stable `code` and display the localized `message`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| reason | string |  |  |  |
| driver_id | string |  |  |  |
| signature_url | string |  |  |  |
| photo_urls | array<string> |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "status": "awaiting_receipt",
  "reason": "Destination warehouse handover completed",
  "driver_id": "{{driver_id}}",
  "signature_url": "https://example.com/signatures/handover.png",
  "photo_urls": [
    "https://example.com/photos/handover.jpg"
  ],
  "notes": "Driver handed the Waybill to the destination warehouse"
}
```

### Response 200 Success - Direct Awaiting Receipt

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill status updated successfully",
  "data": {
    "id": "{{waybill_id}}",
    "waybill_number": "WB00001",
    "status": "awaiting_receipt",
    "carrier_type": "internal",
    "destination_type": "warehouse",
    "driver_id": "{{driver_id}}",
    "status_updates": [
      {
        "old_status": "sealed",
        "new_status": "awaiting_receipt",
        "reason": "Destination warehouse handover completed",
        "notes": "Driver handed the Waybill to the destination warehouse",
        "actor_id": "{{actor_user_id}}",
        "timestamp": "2026-07-30T09:15:00.000Z"
      }
    ],
    "allowed_next_statuses": [],
    "status_transition_options": [],
    "operation_result": {
      "previous_status": "sealed",
      "requested_status": "awaiting_receipt",
      "status": "awaiting_receipt",
      "reason": "Destination warehouse handover completed",
      "notes": "Driver handed the Waybill to the destination warehouse",
      "driver_id": "{{driver_id}}",
      "delivery_status": "in_transit",
      "assignment_status": "completed",
      "reserved_capacity": 2,
      "released_capacity": 2,
      "crossed_seal": false,
      "crossed_assignment": true,
      "crossed_pickup": true,
      "crossed_in_transit": true,
      "crossed_handover": true,
      "direct_transition": true,
      "idempotent_replay": false,
      "allowed_next_statuses": [],
      "status_transition_options": []
    }
  }
}
```

### Response 409 Error - Invalid Transition

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_waybill_transition",
  "message": "Invalid waybill status transition",
  "details": {
    "current_status": "awaiting_receipt",
    "requested_status": "received",
    "allowed_next_statuses": []
  },
  "data": null
}
```

### Response 400 Error - Target Driver Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_target_driver_required",
  "message": "Select a driver before moving this Waybill to the requested status",
  "details": {
    "current_status": "drafted",
    "requested_status": "in_transit",
    "required_field": "driver_id"
  },
  "data": null
}
```

### Response 400 Error - Handover Proof Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_handover_proof_required",
  "message": "Add a signature or at least one photo for this warehouse handover",
  "details": {
    "current_status": "sealed",
    "requested_status": "awaiting_receipt",
    "required_one_of": [
      "signature_url",
      "photo_urls"
    ]
  },
  "data": null
}
```

### Response 400 Error - Reason Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "validation_failed",
  "message": "Reason is required",
  "details": {
    "field": "reason"
  },
  "data": null
}
```

### Response 503 Error - Service Unavailable

Service Unavailable

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_status_change_failed",
  "message": "Unable to update the Waybill status. Please try again.",
  "details": {
    "operation_id": "019fb373-c4e9-7fba-b583-98a841a888e7"
  },
  "data": null
}
```

## POST /delivery/waybills/{{waybill_id}}/driver-action — Driver Action on Waybill

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-driver-action-on-waybill)

Driver-only endpoint for internal waybill execution. Allowed actions: `picked_up`, `in_transit`, `warehouse_handover`, `received`. Active child deliveries include `pending`, `assigned`, `picked_up`, and `in_transit`, and cascade with the waybill: `picked_up` -> `picked_up`, `in_transit` -> `in_transit`, and `received` -> `delivered` only for customer/location destinations. For a warehouse origin, `picked_up` is the physical carrier handoff boundary: the picking order and outbound handoffs complete, warehouse stock becomes zero, a `transfer_out` movement is recorded per parcel, and custody becomes in transit. Transit stock belongs to neither warehouse. For warehouse destinations, `warehouse_handover` moves the Waybill to `awaiting_receipt`, completes the driver's child assignments, and leaves child deliveries `in_transit` until warehouse staff complete authoritative receipt. The `awaiting_receipt` Waybill remains the transport custody owner during that boundary. Returned and cancelled children are not overwritten. On `received`, `waybill.received_by` stores the authenticated driver's user ID (`users.id`), not the driver profile ID (`drivers.id`); Waybill Detail resolves it as `received_by_user`. Cash side effects follow child delivery payer rules: location-origin `picked_up` creates a fee-only pickup collection when the sender pays cash; final location `received` creates a COD-only dropoff collection or one combined COD+fee dropoff collection when the receiver pays both, records the COD payment as successful, and credits the delivery customer/merchant wallet immediately. Warehouse-origin pickup does not create sender cash. Final `received` credits one idempotent driver earning wallet transaction per delivered child using `tenant_settings.driver_compensation`. For final `received`, proof fields are optional. A known moving-delivery custody conflict returns HTTP 409 with code `moving_delivery_requires_assigned_driver` and a localized message. Other unexpected atomic database failures return HTTP 409 with stable code `waybill_driver_action_failed` and a localized display message; the transaction is rolled back and the action can be retried.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/{{waybill_id}}/driver-action

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| waybill_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| action | string |  |  |  |
| current_latitude | number |  |  |  |
| current_longitude | number |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "action": "picked_up",
  "current_latitude": 36.1901,
  "current_longitude": 44.0091,
  "notes": "Picked up from warehouse"
}
```

### Response 200 Success - Picked Up

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill action completed successfully",
  "data": {
    "waybill": {
      "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
      "tenant_id": "{{tenant_id}}",
      "waybill_number": "WB00001",
      "origin_type": "warehouse",
      "origin_warehouse_id": "{{warehouse_id}}",
      "origin_location": null,
      "destination_type": "location",
      "destination_warehouse_id": null,
      "destination_location": {
        "latitude": 36.1901,
        "longitude": 44.0091,
        "full_address": "Erbil, Kurdistan Region"
      },
      "carrier_type": "internal",
      "driver_id": "{{driver_id}}",
      "vehicle_id": "{{vehicle_id}}",
      "external_carrier_ref": null,
      "assigned_by": "{{user_id}}",
      "assignment_time": "2026-04-07T12:20:00Z",
      "status": "picked_up",
      "sealed_at": "2026-04-07T12:15:00Z",
      "dispatched_at": "2026-04-07T12:35:00Z",
      "received_at": null,
      "received_by": null,
      "closed_at": null,
      "cancelled_at": null,
      "proof_of_delivery": null,
      "notes": "Batch for afternoon dispatch",
      "created_by": "{{user_id}}",
      "created_at": "2026-04-07T12:00:00Z",
      "updated_at": "2026-04-07T12:20:00Z"
    },
    "deliveries": [
      {
        "id": "a1b2c3d4-0001-0000-0000-000000000001",
        "delivery_code": 1001,
        "external_reference_id": "EXT-001",
        "status": "picked_up",
        "unit_size": 2,
        "pickup_location": {
          "id": "{{address_id}}",
          "full_address": "Warehouse A, Erbil",
          "latitude": 36.19,
          "longitude": 44.01
        },
        "delivery_location": {
          "id": "{{address_id}}",
          "full_address": "Customer Street 12, Erbil",
          "latitude": 36.2,
          "longitude": 44.02
        },
        "customer": {
          "id": "{{customer_id}}",
          "full_name": "Ahmed Ali",
          "phone": "+9647501234567"
        }
      }
    ]
  }
}
```

### Response 200 Success - Final Receive

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill action completed successfully",
  "data": {
    "waybill": {
      "id": "d0bb9c6b-25db-4e28-a5c1-1b709e3d11f1",
      "tenant_id": "{{tenant_id}}",
      "waybill_number": "WB00001",
      "origin_type": "warehouse",
      "origin_warehouse_id": "{{warehouse_id}}",
      "origin_location": null,
      "destination_type": "location",
      "destination_warehouse_id": null,
      "destination_location": {
        "latitude": 36.1901,
        "longitude": 44.0091,
        "full_address": "Erbil, Kurdistan Region"
      },
      "carrier_type": "internal",
      "driver_id": "{{driver_id}}",
      "vehicle_id": "{{vehicle_id}}",
      "external_carrier_ref": null,
      "assigned_by": "{{user_id}}",
      "assignment_time": "2026-04-07T12:20:00Z",
      "status": "received",
      "sealed_at": "2026-04-07T12:15:00Z",
      "dispatched_at": "2026-04-07T12:35:00Z",
      "received_at": "2026-04-07T13:15:00Z",
      "received_by": null,
      "closed_at": null,
      "cancelled_at": null,
      "proof_of_delivery": {
        "signature_url": "https://example.com/signature.png",
        "photo_urls": [
          "https://example.com/photo1.jpg"
        ],
        "notes": "Delivered successfully",
        "timestamp": "2026-04-07T13:15:00Z"
      },
      "notes": "Batch for afternoon dispatch",
      "created_by": "{{user_id}}",
      "created_at": "2026-04-07T12:00:00Z",
      "updated_at": "2026-04-07T12:20:00Z"
    },
    "deliveries": [
      {
        "id": "a1b2c3d4-0001-0000-0000-000000000001",
        "delivery_code": 1001,
        "external_reference_id": "EXT-001",
        "status": "delivered",
        "unit_size": 2,
        "pickup_location": {
          "id": "{{address_id}}",
          "full_address": "Warehouse A, Erbil",
          "latitude": 36.19,
          "longitude": 44.01
        },
        "delivery_location": {
          "id": "{{address_id}}",
          "full_address": "Customer Street 12, Erbil",
          "latitude": 36.2,
          "longitude": 44.02
        },
        "customer": {
          "id": "{{customer_id}}",
          "full_name": "Ahmed Ali",
          "phone": "+9647501234567"
        }
      }
    ]
  }
}
```

### Response 409 Error - Driver Mismatch

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "You are not the assigned driver for this waybill",
  "data": null
}
```

### Response 409 Error - Moving Delivery Has No Custody Owner

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "moving_delivery_requires_assigned_driver",
  "message": "A moving delivery must always have an assigned driver",
  "details": {
    "database_code": "P0001"
  },
  "data": null
}
```

### Response 409 Error - Driver Action Failed

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_driver_action_failed",
  "message": "Unable to update the Waybill. Please try again.",
  "details": {
    "database_code": "42704"
  },
  "data": null
}
```

## GET /delivery/driver-waybills — Get Driver Waybills

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#get-get-driver-waybills)

Driver-only list. Each waybill appears as one grouped job in the driver app. Without `status`, closed and cancelled waybills are excluded. Provide `status=closed` or `status=cancelled` to retrieve those terminal waybills explicitly. The response includes a complete `statuses` matrix for all waybill statuses, including statuses with a zero count. Matrix counts are for all waybills assigned to the authenticated driver and are not affected by the active `status` filter.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver-waybills?page=1&limit=20&status=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number | 1 |
| limit | query string |  | Items per page | 20 |
| status | query string |  | Filter by waybill status |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybills retrieved successfully",
  "data": [
    {
      "id": "{{waybill_id}}",
      "waybill_number": "WB00001",
      "origin_type": "warehouse",
      "origin_warehouse_id": "{{warehouse_id}}",
      "origin_location": null,
      "destination_type": "location",
      "destination_warehouse_id": null,
      "destination_location": {
        "latitude": 36.1901,
        "longitude": 44.0091,
        "full_address": "Erbil, Kurdistan Region"
      },
      "status": "sealed",
      "carrier_type": "internal",
      "assignment_time": "2026-04-07T12:20:00Z",
      "notes": "Batch for afternoon dispatch",
      "created_at": "2026-04-07T12:00:00Z",
      "updated_at": "2026-04-07T12:20:00Z",
      "item_count": 1
    }
  ],
  "statuses": [
    {
      "status": "drafted",
      "label": "Drafted",
      "count": 0
    },
    {
      "status": "sealed",
      "label": "Sealed",
      "count": 1
    },
    {
      "status": "assigned",
      "label": "Assigned",
      "count": 0
    },
    {
      "status": "picked_up",
      "label": "Picked Up",
      "count": 0
    },
    {
      "status": "in_transit",
      "label": "In Transit",
      "count": 0
    },
    {
      "status": "awaiting_receipt",
      "label": "Awaiting Receipt",
      "count": 0
    },
    {
      "status": "received",
      "label": "Received",
      "count": 0
    },
    {
      "status": "closed",
      "label": "Closed",
      "count": 0
    },
    {
      "status": "cancelled",
      "label": "Cancelled",
      "count": 0
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20
}
```

### Response 404 Error - Driver Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not found",
  "data": null
}
```

## GET /delivery/driver-waybills/status-summary — Get Driver Waybill Status Summary

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#get-get-driver-waybill-status-summary)

Driver-only status counts for waybill tabs/sections in the app. Returns the complete ordered status matrix with labels, including statuses with a zero count.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver-waybills/status-summary

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill status summary retrieved successfully",
  "statuses": [
    {
      "status": "drafted",
      "label": "Drafted",
      "count": 0
    },
    {
      "status": "sealed",
      "label": "Sealed",
      "count": 2
    },
    {
      "status": "assigned",
      "label": "Assigned",
      "count": 0
    },
    {
      "status": "picked_up",
      "label": "Picked Up",
      "count": 1
    },
    {
      "status": "in_transit",
      "label": "In Transit",
      "count": 3
    },
    {
      "status": "awaiting_receipt",
      "label": "Awaiting Receipt",
      "count": 0
    },
    {
      "status": "received",
      "label": "Received",
      "count": 4
    },
    {
      "status": "closed",
      "label": "Closed",
      "count": 0
    },
    {
      "status": "cancelled",
      "label": "Cancelled",
      "count": 0
    }
  ]
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions",
  "data": null
}
```

## POST /delivery/waybills/:id/return-children — Return Waybill Children (Partial Return)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#post-return-waybill-children-partial-return)

When initiated by another actor, the affected driver receives one Waybill-level `waybill_returned` important persistent banner, not one notification per returned child. A driver performing their own return does not receive a redundant notification. Replaying the same successful `Idempotency-Key` does not emit another notification.

Idempotently return one or more child deliveries. `Idempotency-Key` is required. Before warehouse receipt, the existing partial-return behavior remains unchanged. After a warehouse-destination Waybill is `received`, each physically present or damaged child creates a separate internal reverse delivery from the Waybill destination back to its origin. The original forward delivery keeps its primary status and current Waybill membership; only `return_status` changes to `return_approved`. The reverse delivery is linked through `return_requests`, starts as `pending`, and always has `delivery_fee=0`, `cod_amount=0`, `payment_method=wallet`, and `payment_status=paid`, so no customer wallet charge or refund is created. Inventory remains at the destination while the reverse order is pending or moving and is moved back only when that return delivery reaches `delivered` or `staged_at_warehouse`. A child with an open `missing_on_receive` discrepancy cannot be returned through this endpoint and returns HTTP 409 `delivery_has_open_exception`. Destination staff must first use the dedicated discrepancy resolver with `found` or `lost`; `lost` resolves the discrepancy, marks the actual delivery returned without creating a physical reverse order, and settles that delivery through its existing service return-refund policy. The response is enriched Waybill detail plus `operation_result.return_orders`, with original IDs, return-request IDs, return-delivery IDs/codes/statuses, and exception IDs. Frontend must navigate to `return_delivery_id` when staff need to manage the reverse order.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/waybills/:id/return-children

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| Idempotency-Key | header string |  |  | waybill-return-{{$guid}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Waybill UUID | <waybill_id> |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_ids | array<string> |  |  |  |
| reason | string |  |  |  |
| notes | string |  |  |  |
| photo_urls | array<string> |  |  |  |



### Request body (raw)

```json
{
  "delivery_ids": [
    "<delivery_id_1>",
    "<delivery_id_2>"
  ],
  "reason": "refused",
  "notes": "Customer refused on doorstep",
  "photo_urls": [
    "https://example.com/photo1.jpg"
  ]
}
```

### Response 200 Success - Partial Return

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill children returned successfully",
  "data": {
    "id": "wb-uuid",
    "waybill_number": "WB000123",
    "status": "received",
    "operation_result": {
      "waybill_id": "wb-uuid",
      "status": "received",
      "return_order_count": 1,
      "return_requested_delivery_ids": [
        "d1"
      ],
      "return_orders": [
        {
          "original_delivery_id": "d1",
          "original_delivery_code": "1001",
          "return_request_id": "return-request-uuid",
          "return_delivery_id": "return-delivery-uuid",
          "return_delivery_code": "1002",
          "return_delivery_status": "pending",
          "exception_id": "exception-uuid"
        }
      ],
      "missing_resolved_delivery_ids": [],
      "missing_resolved_count": 0,
      "financial_reversal": false,
      "idempotent_replay": false
    }
  }
}
```

### Response 409 Error - Missing Receipt Discrepancy Is Open

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_has_open_exception",
  "message": "Delivery has an open exception. Resolve it before moving the delivery forward.",
  "details": {
    "database_code": "P0001"
  },
  "data": null
}
```

### Response 409 Error - Waybill Not Returnable

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Waybill is not in a returnable state",
  "data": null
}
```

### Response 409 Error - Delivery Has Not Reached Pickup

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_not_picked_up_for_return",
  "message": "This order has not been picked up yet. Cancel it instead.",
  "details": {
    "database_code": "P0001"
  },
  "data": null
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions",
  "data": null
}
```

## GET /delivery/waybills/:id/returns — List Waybill Returns

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/waybills#get-list-waybill-returns)

Paginated Waybill return history. Each row contains the return exception, evidence and enriched actors, the original forward delivery, the linked return request, and the separate reverse delivery with its latest assignment, driver, and vehicle. Historical rows created before return-order linkage remain visible with null `return_request` and `return_delivery`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/waybills/:id/returns?page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Waybill UUID | <waybill_id> |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill returns retrieved successfully",
  "data": [
    {
      "id": "exception-uuid",
      "delivery_id": "original-delivery-uuid",
      "exception_type": "refused",
      "reason": "refused",
      "notes": "Return to origin",
      "status": "resolved",
      "resolution_type": "returned",
      "trigger_point": "rejected_at_destination",
      "photo_urls": [
        "https://example.com/return.jpg"
      ],
      "proof": null,
      "reported_at": "2026-07-31T10:00:00.000Z",
      "resolved_at": "2026-07-31T10:00:00.000Z",
      "reported_by_user": {
        "id": "user-uuid",
        "full_name": "Dispatcher User",
        "email": "dispatcher@example.com",
        "phone": "+9647500000000",
        "avatar_url": null
      },
      "resolved_by_user": {
        "id": "user-uuid",
        "full_name": "Dispatcher User",
        "email": "dispatcher@example.com",
        "phone": "+9647500000000",
        "avatar_url": null
      },
      "original_delivery": {
        "id": "original-delivery-uuid",
        "delivery_code": "DEL1001",
        "barcode": "DEL1001",
        "status": "delivered",
        "return_status": "return_approved",
        "current_warehouse_id": "destination-warehouse-uuid"
      },
      "delivery": {
        "id": "original-delivery-uuid",
        "delivery_code": "DEL1001",
        "status": "delivered",
        "return_status": "return_approved"
      },
      "return_request": {
        "id": "return-request-uuid",
        "status": "approved",
        "return_reason": "refused",
        "notes": "Return to origin",
        "requested_at": "2026-07-31T10:00:00.000Z",
        "processed_at": "2026-07-31T10:00:00.000Z",
        "received_at": null,
        "warehouse": {
          "id": "origin-warehouse-uuid",
          "name": "Origin Warehouse",
          "code": "ORG"
        },
        "processed_by_user": {
          "id": "user-uuid",
          "full_name": "Dispatcher User",
          "email": "dispatcher@example.com",
          "phone": "+9647500000000",
          "avatar_url": null
        }
      },
      "return_delivery": {
        "id": "return-delivery-uuid",
        "delivery_code": "DEL1002",
        "barcode": "DEL1002",
        "status": "pending",
        "pickup_warehouse_id": "destination-warehouse-uuid",
        "dropoff_warehouse_id": "origin-warehouse-uuid",
        "delivery_fee": 0,
        "cod_amount": 0,
        "payment_method": "wallet",
        "payment_status": "paid",
        "assignment": null
      }
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 20
}
```

## GET /delivery/trips — List Trips

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#get-list-trips)

List trips with optional filters, column-level filters, sorting, and pagination. Returns item_count per trip.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/trips?page=1&limit=20&status=planned&driver_id=&trip_type=&trip_number=&from_date=&to_date=&sort_by=created_at&sort_order=desc&column_filters=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number | 1 |
| limit | query string |  | Items per page (max 100) | 20 |
| status | query string |  | Filter by status: planned, in_progress, completed, cancelled | planned |
| driver_id | query string |  | Filter by driver UUID |  |
| trip_type | query string |  | Filter by type: multi_pickup, multi_dropoff, mixed |  |
| trip_number | query string |  | Search by trip number (partial) |  |
| from_date | query string |  | Start date YYYY-MM-DD (normalized to 00:00:00) |  |
| to_date | query string |  | End date YYYY-MM-DD (normalized to 23:59:59) |  |
| sort_by | query string |  | Sort by: trip_number, status, trip_type, notes, created_at, updated_at, started_at, completed_at | created_at |
| sort_order | query string |  | Sort order: asc or desc | desc |
| column_filters | query string |  | JSON object. Supported keys: trip_number, status, trip_type, driver_id, vehicle_id, notes, created_at, updated_at, started_at, completed_at. Date keys accept {"from":"...","to":"..."}. |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trips retrieved successfully",
  "data": {
    "trips": [
      {
        "id": "trip-uuid",
        "trip_number": "TRIP-00001",
        "status": "planned",
        "trip_type": "multi_pickup",
        "notes": "Morning run",
        "started_at": null,
        "completed_at": null,
        "created_at": "2026-04-11T10:00:00Z",
        "updated_at": "2026-04-11T10:00:00Z",
        "item_count": 3,
        "driver": {
          "id": "driver-uuid",
          "full_name": "Ahmed Hassan",
          "phone": "+9647501234567"
        },
        "vehicle": {
          "id": "vehicle-uuid",
          "name": "Toyota Hiace",
          "code": "VH-001"
        }
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20
  }
}
```

## POST /delivery/trips — Create Trip

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#post-create-trip)

### Realtime mobile contract

a newly created Trip assignment is immediately `assigned`. Its existing `delivery_realtime_assignments` notification therefore emits `assignment_status: "assigned"` and `requires_driver_response: false`. Mobile displays it as information and does not show Accept/Reject. Missing or unknown actionability is also non-actionable.

Create a new trip. Metadata fields are optional when creating an empty trip. `driver_id` is required when `deliveries` is non-empty.

- `trip_type`: multi_pickup | multi_dropoff | mixed (optional, informational)
- `deliveries`: optional array of {delivery_id, sequence?}. Sequence auto-assigned if not provided.
- All submitted deliveries are validated before the trip row or assignment rows are created. Validation results follow request order.
- Unassigned pending deliveries are assigned immediately to the trip driver using the normal driver, primary-vehicle, capacity, zone, team, service-policy, audit, realtime, and notification rules. Shared driver and policy context is loaded once, and all missing assignments are written through the atomic batch-assignment transaction.
- Newly assigned rows use `status=assigned`; route calculation is scheduled asynchronously after commit. One bulk realtime summary and one bulk driver notification represent the newly assigned rows, with `assignment_status=assigned` and `requires_driver_response=false`.
- A delivery already assigned to the same driver is reused. A delivery assigned to another driver is rejected and is never automatically reassigned.
- Eligible delivery statuses: pending, assigned. Only internal operational deliveries are accepted.
- Deliveries linked to any Waybill, route, route session, or another trip are rejected.
- P&D parent orders and inactive draft legs are rejected; attach only the currently active internal child leg.

### Frontend integration contract
- Select a trip driver before sending a non-empty `deliveries` array.
- After success, use the returned deliveries; newly assigned deliveries have `status=assigned`.
- On HTTP 409 with `code=trip_delivery_validation_failed`, keep the form and show each `data.results[].error_message` against its matching `delivery_id`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/trips

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | string |  |  |  |
| vehicle_id | string |  |  |  |
| trip_type | string |  |  |  |
| notes | string |  |  |  |
| deliveries | array<object> |  |  |  |
| deliveries.delivery_id | string |  |  |  |
| deliveries.sequence | number |  |  |  |



### Request body (raw)

```json
{
  "driver_id": "driver-uuid",
  "vehicle_id": "vehicle-uuid",
  "trip_type": "multi_pickup",
  "notes": "Morning delivery run",
  "deliveries": [
    {
      "delivery_id": "delivery-uuid-1",
      "sequence": 1
    },
    {
      "delivery_id": "delivery-uuid-2",
      "sequence": 2
    }
  ]
}
```

### Response 201 Success

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip created successfully",
  "data": {
    "id": "trip-uuid",
    "trip_number": "TRIP-00001",
    "status": "planned",
    "trip_type": "multi_pickup",
    "notes": "Morning delivery run",
    "driver": {
      "id": "driver-uuid",
      "full_name": "Ahmed Hassan",
      "phone": "+9647501234567"
    },
    "vehicle": {
      "id": "vehicle-uuid",
      "name": "Toyota Hiace",
      "code": "VH-001"
    },
    "deliveries": [
      {
        "id": "delivery-uuid-1",
        "delivery_code": 1001,
        "barcode": "BC001",
        "status": "assigned",
        "trip_sequence": 1
      },
      {
        "id": "delivery-uuid-2",
        "delivery_code": 1002,
        "barcode": "BC002",
        "status": "assigned",
        "trip_sequence": 2
      }
    ],
    "summary": {
      "total": 2,
      "planned": 2,
      "in_progress": 0,
      "delivered": 0,
      "returned": 0,
      "cancelled": 0,
      "with_return_request": 0
    },
    "driver_execution": {
      "stop_list": [
        {
          "stop_type": "pickup",
          "location": {
            "latitude": 33.3,
            "longitude": 44.4
          },
          "deliveries": [
            {
              "delivery_id": "delivery-uuid-1",
              "delivery_code": 1001,
              "barcode": "BC001",
              "status": "assigned",
              "trip_sequence": 1
            }
          ],
          "is_complete": false
        }
      ],
      "current_stop": {
        "stop_type": "pickup",
        "location": {},
        "deliveries": []
      },
      "next_stop": null
    }
  }
}
```

### Response 409 Ordered delivery validation failed

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_delivery_validation_failed",
  "message": "One or more deliveries cannot be added to this trip",
  "data": {
    "results": [
      {
        "delivery_id": "delivery-uuid-1",
        "delivery_code": 1001,
        "valid": true,
        "assignment_action": "assign"
      },
      {
        "delivery_id": "delivery-uuid-2",
        "delivery_code": 1002,
        "valid": false,
        "error_key": "delivery_assigned_to_different_driver",
        "error_message": "Delivery is already assigned to a different driver"
      }
    ],
    "total_valid": 1,
    "total_failed": 1
  }
}
```

## GET /delivery/trips/{{trip_id}} — Get Trip Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#get-get-trip-detail)

### Get full trip detail including
- Trip header (number, status, type, general notes) and concurrency token `updated_at`
- `status_updates`: status-only timeline with previous/new status, timestamp, reason, and notes
- `trip_events`: complete append-only staff history for users with trip-management permission
- `available_actions`: manager actions currently allowed (`cancel`, `switch_driver`)
- Driver and vehicle info
- Ordered deliveries and `deliveries_mode` (`live` or `historical`)
- Summary counts (total, per-status)
- `driver_execution`: coordinate-grouped stop_list, current_stop, next_stop

Live deliveries and stop delivery references include `assignment_status` plus the literal `next_action`: `start_delivery`, `pickup`, `in_transit`, `complete`, or null. `bulk_eligible` is true only when one of those actions is available. Each stop includes the stable `stop_key` that must be returned unchanged to stop-order or bulk-action requests. Assignment enrichment is one batched query for the whole trip, never one query per delivery.

`trip_events[]` records `trip_created`, `trip_updated`, `driver_assigned`, `driver_unassigned`, `driver_changed`, `deliveries_added`, `deliveries_removed`, `deliveries_reordered`, `stops_reordered`, `trip_started`, `trip_completed`, `trip_cancelled`, `deliveries_moved_out`, and `deliveries_moved_in`. Each event returns `event_type`, `timestamp`, actor fields (`actor_id`, `actor_name`, `actor_role`), status context, reason/notes, and optional changes. New move events also return `source_trip_id`, `source_trip_number`, `target_trip_id`, and `target_trip_number`; older move events may contain only the IDs. Historical events created before this feature may have null actor fields because the previous data did not preserve the actor; the API never invents one. Driver-only callers do not receive `trip_events`, `available_actions`, or historical delivery snapshots.

For a cancelled trip, the manager response returns the cancellation-time snapshot through `data.deliveries`, sets `data.deliveries_mode=historical`, calculates `summary` from that snapshot, returns no available actions, and keeps `driver_execution.stop_list`, `current_stop`, and `next_stop` empty. Each historical delivery contains `id`, `delivery_code`, `barcode`, `pickup_location`, `dropoff_location`, `unit_size`, cancellation-time `status`, `payment_method`, `cod_amount`, `external_reference_id`, and `trip_sequence`. Cancelled trips created before snapshot storage may return an empty historical list because former membership is not invented without persisted evidence.

### Stop ordering rules
- Pickup locations are grouped only by exact valid latitude/longitude coordinates.
- Drop-off locations are grouped only by exact valid latitude/longitude coordinates.
- Pickup and drop-off remain separate stop types even when their coordinates match.
- Locations without valid coordinates are not grouped.
- Address IDs, labels, names, and formatted addresses do not determine grouping.
- A drop-off is placed only after every pickup stop required by the deliveries inside that drop-off.
- A saved dispatcher stop order is preferred; obsolete keys are ignored and new keys are inserted in a valid deterministic position.
- A pickup or drop-off shared by every current order remains first or last respectively.
- Without a saved stop order, delivery `trip_sequence` remains the preference whenever it does not violate pickup-before-drop-off.
- `current_stop` is the first incomplete stop in the effective order; `next_stop` is the following stop.

### Frontend integration contract
- Build the Trip Events section from `data.trip_events` in array order, using `event_type` as the machine-readable display mapping key.
- Show actor name/role and timestamp for each event; show reason, notes, and changes only when present. For move events, display `source_trip_number` and `target_trip_number` when present, falling back to their IDs for older history.
- Show Cancel Trip and Switch Driver only when their literal values are present in `data.available_actions`.
- Render `data.deliveries` in the existing Deliveries tab. When `data.deliveries_mode=historical`, keep the list read-only.
- Keep `data.status_updates` only for the compact status timeline. Do not treat top-level `data.notes` as an event note.
- Render `data.driver_execution.stop_list`, `current_stop`, and `next_stop` directly. Use `stop_key` and select only deliveries whose `next_action` equals the group button action.

### G2G origin display

each live `data.deliveries[]` row includes nullable `source_transfer_manifest_id` and `g2g_origin`. A non-null `g2g_origin` contains `{ "branchName": string|null, "branchAddress": string|null }` from a valid saved `manifest_origin_v1` snapshot; staged children inherit usable origin from their same-tenant, non-deleted parent when needed only if their manifest ID is absent or matches the parent. Normal orders and orders without usable saved origin return null. When `source_transfer_manifest_id` is non-null, render a separate read-only Original G2G location line on the delivery card, using any available branch name or address; if `g2g_origin` is null, show Origin unavailable. Hide the line only when the manifest ID is null or absent. Keep `pickup_location`, `dropoff_location`, `driver_execution`, stop grouping, maps, and action targets unchanged. The same batched delivery query returns the metadata; do not fetch per-card details or current branch addresses. Historical cancelled-trip snapshots do not acquire these new fields and their origin line remains hidden.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip retrieved successfully",
  "data": {
    "id": "trip-uuid",
    "trip_number": "TRIP-00001",
    "status": "in_progress",
    "updated_at": "2026-09-10T10:00:00.000Z",
    "deliveries": [
      {
        "id": "d1",
        "delivery_code": 1001,
        "status": "picked_up",
        "trip_sequence": 1,
        "assignment_status": "picked_up",
        "next_action": "in_transit",
        "source_transfer_manifest_id": "manifest-uuid",
        "g2g_origin": {
          "branchName": "Original branch",
          "branchAddress": "Original branch address"
        }
      },
      {
        "id": "d2",
        "delivery_code": 1002,
        "status": "en_route_to_pickup",
        "trip_sequence": 2,
        "assignment_status": "en_route_to_pickup",
        "next_action": "pickup",
        "source_transfer_manifest_id": null,
        "g2g_origin": null
      }
    ],
    "driver_execution": {
      "stop_list": [
        {
          "stop_key": "pickup:33.3,44.4",
          "stop_type": "pickup",
          "location": {
            "latitude": 33.3,
            "longitude": 44.4
          },
          "deliveries": [
            {
              "delivery_id": "d1",
              "delivery_code": 1001,
              "status": "picked_up",
              "trip_sequence": 1,
              "assignment_status": "picked_up",
              "next_action": null,
              "bulk_eligible": false
            },
            {
              "delivery_id": "d2",
              "delivery_code": 1002,
              "status": "en_route_to_pickup",
              "trip_sequence": 2,
              "assignment_status": "en_route_to_pickup",
              "next_action": "pickup",
              "bulk_eligible": true
            }
          ],
          "is_complete": false
        }
      ],
      "current_stop": {
        "stop_key": "pickup:33.3,44.4",
        "stop_type": "pickup",
        "is_complete": false
      },
      "next_stop": null
    }
  }
}
```

### Response 200 Success - Status Timeline

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip retrieved successfully",
  "data": {
    "id": "trip-uuid",
    "status": "cancelled",
    "notes": "General trip note",
    "status_updates": [
      {
        "previous_status": "planned",
        "new_status": "cancelled",
        "timestamp": "2026-08-17T09:00:00.000Z",
        "reason": "Trip cancelled by dispatcher",
        "notes": "Vehicle issue reported before pickup"
      }
    ],
    "trip_events": [
      {
        "event_type": "trip_cancelled",
        "timestamp": "2026-08-17T09:00:00.000Z",
        "actor_id": "user-uuid",
        "actor_name": "Sara Dispatcher",
        "actor_role": "dispatcher",
        "previous_status": "planned",
        "new_status": "cancelled",
        "reason": "Trip cancelled by dispatcher",
        "notes": "Vehicle issue reported before pickup"
      }
    ],
    "deliveries_mode": "historical",
    "deliveries": [
      {
        "id": "delivery-uuid-1",
        "delivery_code": "2226A",
        "barcode": "BC2226A",
        "pickup_location": {
          "latitude": 36.19,
          "longitude": 44.01
        },
        "dropoff_location": {
          "latitude": 36.2,
          "longitude": 44.03
        },
        "unit_size": 1,
        "status": "assigned",
        "payment_method": "cod",
        "cod_amount": 30000,
        "external_reference_id": "ORDER-2226",
        "trip_sequence": 1
      }
    ],
    "summary": {
      "total": 1,
      "planned": 1,
      "in_progress": 0,
      "delivered": 0,
      "returned": 0,
      "cancelled": 0,
      "with_return_request": 0
    },
    "driver_execution": {
      "stop_list": [],
      "current_stop": null,
      "next_stop": null
    }
  }
}
```

### Response 200 Success - Move Event Trip Numbers

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip retrieved successfully",
  "data": {
    "id": "target-trip-uuid",
    "trip_number": "TRIP-00021",
    "trip_events": [
      {
        "event_type": "deliveries_moved_in",
        "timestamp": "2026-09-09T09:22:33.877Z",
        "actor_id": "user-uuid",
        "actor_name": "Rakesh Mavai",
        "actor_role": "dispatcher",
        "reason": "Correct trip assignment",
        "source_trip_id": "source-trip-uuid",
        "source_trip_number": "TRIP-00018",
        "target_trip_id": "target-trip-uuid",
        "target_trip_number": "TRIP-00021"
      }
    ]
  }
}
```

### Response 404 Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Trip not found",
  "data": null
}
```

## PATCH /delivery/trips/{{trip_id}} — Update Trip

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#patch-update-trip)

Update planned-trip metadata only. Supported fields are optional `trip_type` and `notes`. Driver and vehicle changes are intentionally rejected with HTTP 409 `code=trip_driver_switch_endpoint_required`; use `POST /delivery/trips/{id}/switch-driver` so assignment, custody, capacity, audit, and both-driver notifications stay consistent.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_type | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "trip_type": "multi_dropoff",
  "notes": "Updated notes"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip updated successfully",
  "data": {
    "id": "trip-uuid",
    "status": "planned"
  }
}
```

### Response 409 Not in planned status

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_not_editable",
  "message": "Trip cannot be edited in its current status",
  "data": null
}
```

## POST /delivery/trips/{{trip_id}}/switch-driver — Switch Trip Driver

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#post-switch-trip-driver)

Changes the driver of a `planned` or `in_progress` trip. Requires trip-management permission. `driver_id` and `reason` are required; `notes` is optional. The backend selects the new driver's primary active vehicle.

### Business rules
- The new driver must be active and available, have an active primary vehicle with enough remaining capacity, and satisfy every active trip delivery's zone, team, and service-vehicle rules.
- Completed, failed, cancelled, and returned deliveries are not reassigned. Every non-terminal member must have exactly one active assignment to the current trip driver.
- The switch is atomic and preserves trip membership, trip sequence, stop order, delivery progress, pickup/delivery timestamps, and trip status. Active assignment ownership, custody, vehicle, and capacity move immediately to the new driver. Pre-pickup assignment acceptance follows tenant driver settings.
- Repeating the request with the current driver returns `changed=false` and creates no duplicate event or notification.
- One in-app and push notification is created for each affected driver. The previous driver receives a critical modal requiring acknowledgement. The new driver receives a normal toast before pickup or an important persistent banner after pickup. Post-pickup notifications include the counterparty contact actions. Notification acknowledgement does not delay the custody switch.

Success data contains `id`, `trip_number`, authoritative `status`, `changed`, `previous_driver_id`, `driver_id`, `vehicle_id`, `delivery_count`, and `picked_up_delivery_count`.

### Important errors
- HTTP 400 `code=validation_error` for missing/invalid fields.
- HTTP 403 `code=insufficient_permissions` or `assignment_change_disabled`.
- HTTP 404 `code=trip_not_found` or `driver_not_found`.
- HTTP 409 `code=trip_driver_switch_not_allowed`, `trip_driver_required`, `new_driver_not_active`, `new_driver_has_no_vehicle`, `delivery_assigned_to_different_driver`, `invalid_delivery_status_for_driver_switch`, zone/team/service-vehicle errors, or `insufficient_vehicle_capacity_for_new_driver`.

### Frontend integration contract
- Show Switch Driver only when the trip has a driver and status is `planned` or `in_progress`.
- Send only `driver_id`, `reason`, and optional `notes`; do not send `vehicle_id`.
- On success, replace the displayed driver and vehicle using the returned IDs or reload trip detail.
- Branch on `code` and display the localized `message`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/switch-driver

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | string |  |  |  |
| reason | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "driver_id": "new-driver-uuid",
  "reason": "Driver shift changed",
  "notes": "Coordinate the package handover with dispatch"
}
```

### Response 200 Success - Post Pickup

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip driver changed successfully",
  "data": {
    "id": "trip-uuid",
    "trip_number": "TRIP-00016",
    "status": "in_progress",
    "changed": true,
    "previous_driver_id": "old-driver-uuid",
    "driver_id": "new-driver-uuid",
    "vehicle_id": "vehicle-uuid",
    "delivery_count": 3,
    "picked_up_delivery_count": 2
  }
}
```

### Response 409 Trip status blocks switch

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_driver_switch_not_allowed",
  "message": "The trip driver cannot be changed in its current status",
  "data": null
}
```

## PATCH /delivery/trips/{{trip_id}}/status — Change Trip Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#patch-change-trip-status)

Starts, completes, or cancels an Eaglepost trip. Requires `manage:operations:tenant`, `manage:module:delivery`, or system-admin access.

### Start rules
- `in_progress` requires a `planned` trip with a driver and at least one delivery.
- Every member may be `assigned`, `en_route_to_pickup`, `picked_up`, or `in_transit` and must have exactly one active assignment to the trip driver. This allows a planned trip to start after receiving already-active orders through the move endpoint without resetting their progress.
- Starting changes only the trip status and start timestamp; it does not reset order or assignment progress.
- Repeating `in_progress` is idempotent with `changed=false`.

### Completion rules
- `completed` is accepted only for an `in_progress` or already `completed` trip.
- Existing `delivered`, `failed`, `cancelled`, and `returned` members are retained as successful unchanged results.
- Every remaining member is marked `delivered` through the same individual delivery-status flow used by `POST /delivery/deliveries/{deliveryId}/status`. This preserves assignment completion, payment/COD handling, package outcomes, capacity, earnings, notifications, audit history, P&D child handling, and the individual flow's validation behavior. The trip driver is supplied if that flow must recreate a missing assignment.
- After delivery processing, the endpoint atomically rechecks current membership. When every current member is terminal, it explicitly completes the trip and records its completion timestamp/history without changing order state, assignment, driver, or order history. This also handles a source trip left with only terminal orders after a move.
- Delivery processing uses bounded concurrency. Results are returned in trip sequence order.
- A fully successful retry of an already completed trip is idempotent.
- If any member fails, successful member changes remain persisted, the API returns HTTP 409 `code=trip_deliveries_not_completed`, and `data.status` remains authoritative.

### Move interaction
- Moving orders never starts or completes either trip. Only this later explicit status request may change the trip status.

### Cancellation rules
- `cancelled` delegates to the same atomic cancellation service as `POST /delivery/trips/{id}/cancel`; the dedicated endpoint remains backward compatible.
- Every current member must still be `pending`, `assigned`, or `en_route_to_pickup`. A picked-up, in-transit, delivered, cancelled, returned, exception, or otherwise non-cancellable member blocks the whole trip cancellation.
- Cancellation releases active assignments and driver capacity, returns every member to `pending`, and clears `trip_id` and `trip_sequence`.
- Repeating cancellation is idempotent and creates no duplicate notification. A real cancellation sends the assigned driver one critical modal requiring acknowledgement plus push notification.

Completion response data contains `id`, `previous_status`, authoritative `status`, `changed`, timestamps, `available_actions`, order-wise `results`, `total_success`, `total_changed`, and `total_failed`. Cancellation returns the existing cancellation result including release counts and authoritative status.

### Important errors
- HTTP 400 `code=validation_error` for a missing/invalid status or invalid trip UUID. Missing completion or cancellation reason returns HTTP 400 `error=reason_required` and `field=reason`.
- HTTP 403 `code=insufficient_permissions`.
- HTTP 404 `code=trip_not_found`.
- HTTP 409 `code=invalid_trip_status_transition`, `trip_has_no_deliveries`, `trip_deliveries_not_ready_to_start`, `trip_deliveries_not_completed`, or `trip_deliveries_not_releasable`.

### Frontend integration contract
- Start sends `{ "status": "in_progress" }`.
- Complete sends `{ "status": "completed", "reason": "...", "notes": "..." }`.
- Cancel may now send `{ "status": "cancelled", "reason": "..." }` to this PATCH endpoint. The dedicated POST cancel endpoint remains supported.
- Never send `planned` to this endpoint.
- On HTTP 200, use the authoritative `data.status` and timestamps. Completion also returns `data.results`.
- On completion HTTP 409, show each failed `data.results[]` entry's localized `message`. Branch on machine codes, never message text.
- No navigation action is returned.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string | Yes | `in_progress`, `completed`, or `cancelled`. |  |
| reason | string |  | (required for `completed` and `cancelled`). |  |
| notes | string | No | is optional. Successful status changes persist `reason` in the Trip's append-only `status_updates` timeline. Completion also applies them to each delivery status update. |  |



### Request body (raw)

```json
{
  "status": "cancelled",
  "reason": "Trip cancelled by dispatcher",
  "notes": "Vehicle issue reported before pickup"
}
```

### Response 200 Started

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip status updated successfully",
  "data": {
    "id": "trip-uuid",
    "previous_status": "planned",
    "status": "in_progress",
    "changed": true,
    "started_at": "2026-07-29T12:30:00Z",
    "completed_at": null,
    "released_delivery_count": 0,
    "released_assignment_count": 0,
    "available_actions": [
      "cancel"
    ]
  }
}
```

### Response 200 Completed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip status updated successfully",
  "data": {
    "id": "trip-uuid",
    "previous_status": "in_progress",
    "status": "completed",
    "changed": true,
    "started_at": "2026-07-29T12:30:00Z",
    "completed_at": "2026-07-29T15:00:00Z",
    "available_actions": [],
    "results": [
      {
        "delivery_id": "delivery-uuid",
        "delivery_code": "2226A",
        "previous_status": "in_transit",
        "status": "delivered",
        "success": true,
        "changed": true,
        "message": "Delivery status updated successfully"
      }
    ],
    "total_success": 1,
    "total_changed": 1,
    "total_failed": 0
  }
}
```

### Response 200 Completed - Existing terminal orders unchanged

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip status updated successfully",
  "data": {
    "id": "source-trip-uuid",
    "previous_status": "in_progress",
    "status": "completed",
    "changed": true,
    "started_at": "2026-09-09T08:00:00Z",
    "completed_at": "2026-09-09T10:00:00Z",
    "available_actions": [],
    "results": [
      {
        "delivery_id": "delivered-order-uuid",
        "delivery_code": 3343,
        "previous_status": "delivered",
        "status": "delivered",
        "success": true,
        "changed": false,
        "message": "Delivery status updated successfully"
      }
    ],
    "total_success": 1,
    "total_changed": 0,
    "total_failed": 0
  }
}
```

### Response 200 Cancelled through status

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip status updated successfully",
  "data": {
    "id": "trip-uuid",
    "previous_status": "planned",
    "status": "cancelled",
    "changed": true,
    "started_at": null,
    "completed_at": null,
    "released_delivery_count": 2,
    "released_assignment_count": 2,
    "available_actions": []
  }
}
```

### Response 409 Delivery completion failed

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_deliveries_not_completed",
  "message": "The trip cannot be completed until every delivery is delivered, failed, cancelled, or returned",
  "data": {
    "id": "trip-uuid",
    "previous_status": "in_progress",
    "status": "in_progress",
    "changed": false,
    "started_at": "2026-07-29T12:30:00Z",
    "completed_at": null,
    "available_actions": [],
    "results": [
      {
        "delivery_id": "delivery-uuid",
        "delivery_code": "2226A",
        "previous_status": "in_transit",
        "status": "in_transit",
        "success": false,
        "changed": false,
        "code": "payment_failed",
        "message": "Payment failed"
      }
    ],
    "total_success": 0,
    "total_changed": 0,
    "total_failed": 1
  }
}
```

### Response 409 Invalid transition

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_trip_status_transition",
  "message": "This trip status change is not allowed",
  "data": null
}
```

## GET /delivery/trips/eligible-deliveries — List Eligible Deliveries for Trip

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#get-list-eligible-deliveries-for-trip)

Returns operational deliveries that can be added to a trip.

### Eligibility rules
- Status is `pending` or `assigned`
- Carrier is `internal`
- Not deleted
- Not linked to a Waybill, route, route session, or trip
- Not a P&D parent order
- For staged P&D, only the currently active child leg is returned: the pending/assigned inbound leg before staging, or the pending/assigned outbound leg after staging. Draft future legs are excluded.

Supports indexed search by delivery code, barcode, or external_reference_id and indexed newest-first pagination. The same rules are revalidated by POST; the picker response is not trusted as authorization.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/trips/eligible-deliveries?page=1&limit=20&search=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number | 1 |
| limit | query string |  | Items per page (max 100) | 20 |
| search | query string |  | Search by barcode or external_reference_id |  |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Eligible deliveries retrieved successfully",
  "data": [
    {
      "id": "delivery-uuid-1",
      "delivery_code": 1001,
      "barcode": "BC001",
      "status": "pending",
      "external_reference_id": null,
      "payment_method": "cod",
      "cod_amount": 15000,
      "unit_size": 1,
      "pickup_location": {
        "latitude": 33.3,
        "longitude": 44.4
      },
      "delivery_location": {
        "latitude": 33.5,
        "longitude": 44.6
      },
      "pickup_warehouse": {
        "id": "wh-uuid",
        "name": "Main WH",
        "code": "WH01"
      },
      "dropoff_warehouse": null,
      "pickup_zone": {
        "id": "zone-uuid",
        "name": "Zone A",
        "code": "ZA"
      },
      "dropoff_zone": null
    }
  ],
  "total": 5,
  "page": 1,
  "limit": 20
}
```

## POST /delivery/trips/deliveries/scan-validate — Validate Scanned Trip Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#post-validate-scanned-trip-delivery)

Read-only pre-create validation for trip scan mode. Use this when a hardware scanner or camera returns the printed shipment-label value such as `DEL2993`.

- `barcode` is required. The backend trims and uppercases it, then resolves by exact stored `barcode` or by `delivery_code` after stripping the printed `DEL` prefix.

### Business rules
- This endpoint performs no writes and does not reserve capacity.
- Driver, delivery, staged-child, and active-assignment context is resolved in one database call per scan.
- The requested driver must exist for the tenant and be active.
- If the scanned label resolves to a staged/P&D parent order, the backend resolves that parent to the currently active child delivery leg and validates the child. The response `data.delivery` is always the delivery row the trip should add.
- Valid means the resolved delivery currently passes trip membership rules and any active assignment already belongs to the requested driver.
- It rejects deleted deliveries, deliveries attached to a Waybill, route, route session, or another trip, non-internal carriers, non-pending/non-assigned statuses, active assignments for a different driver, and staged parents with no active child leg.
- The final `POST /delivery/trips` request remains authoritative and repeats validation before creating the trip and assigning deliveries.

### Frontend integration contract
- Call this endpoint for each scan after a driver is selected.
- On `data.valid=true`, always add/check duplicates by `data.delivery.id`. If the scanned parent was resolved, `data.resolved_from_parent` identifies the scanned parent and child, but the frontend must submit the returned child `data.delivery.id`.
- If `data.delivery.id` is already in the local unsaved trip list, show the duplicate warning and do not add another row. Otherwise add it to the same local delivery list used by searched rows and display `data.message`.
- On `data.valid=false`, display `data.message` and branch on `data.code` only if UI behavior needs a machine reason.
- Submit searched and scanned rows together to `POST /delivery/trips` as `deliveries: [{ delivery_id, sequence }]`. No navigation action is returned.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/trips/deliveries/scan-validate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | string | Yes | is required. It is the intended trip driver UUID. |  |
| barcode | string |  |  |  |



### Request body (raw)

```json
{
  "driver_id": "driver-uuid",
  "barcode": "DEL2993"
}
```

### Response 200 Valid scan

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Scanned delivery validated",
  "data": {
    "scanned_code": "DEL4108",
    "normalized_delivery_code": "4108",
    "valid": true,
    "assignment_action": "assign",
    "message": "Delivery is ready to add to this trip",
    "resolved_from_parent": {
      "parent_delivery_id": "parent-delivery-uuid",
      "parent_delivery_code": "4108",
      "delivery_id": "child-delivery-uuid",
      "delivery_code": "4108B"
    },
    "delivery": {
      "id": "child-delivery-uuid",
      "delivery_code": "4108B",
      "barcode": "DEL4108B",
      "external_reference_id": "ORDER-4108",
      "status": "pending",
      "unit_size": 1,
      "payment_method": "cod",
      "cod_amount": 15000,
      "pickup_location": {
        "latitude": 33.3,
        "longitude": 44.4
      },
      "dropoff_location": {
        "latitude": 33.5,
        "longitude": 44.6
      }
    }
  }
}
```

### Response 200 Invalid scan

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Scanned delivery validated",
  "data": {
    "scanned_code": "DEL2993",
    "normalized_delivery_code": "2993",
    "valid": false,
    "delivery_id": "delivery-uuid",
    "delivery_code": "2993",
    "code": "delivery_assigned_to_different_driver",
    "message": "Delivery is already assigned to a different driver"
  }
}
```

### Response 400 Invalid request

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "validation_error",
  "message": "barcode: Field required",
  "details": [
    {
      "field": "barcode",
      "message": "Field required"
    }
  ]
}
```

## POST /delivery/trips/{{trip_id}}/deliveries/validate — Validate Trip Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#post-validate-trip-deliveries)

Read-only preflight for adding deliveries to a planned trip. The trip must have a driver. Results are returned in the exact submitted order and contain `delivery_id`, `delivery_code`, `valid`, and either `assignment_action` or localized `error_key`/`error_message`.

### Assignment actions
- `assign`: pending delivery has no active assignment and will be assigned to the trip driver during sync.
- `keep`: active assignment already belongs to the trip driver.
- An active assignment for another driver returns `delivery_assigned_to_different_driver`; the backend never reassigns it automatically.

This endpoint performs no writes. The final create/sync endpoint repeats the same validation. Frontend should show each error beside the matching delivery and submit only when every selected delivery is valid.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/deliveries/validate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveries | array<object> |  |  |  |
| deliveries.delivery_id | string |  |  |  |
| deliveries.sequence | number |  |  |  |



### Request body (raw)

```json
{
  "deliveries": [
    {
      "delivery_id": "delivery-uuid-1",
      "sequence": 1
    },
    {
      "delivery_id": "delivery-uuid-2",
      "sequence": 2
    }
  ]
}
```

### Response 200 Validation completed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip delivery validation completed",
  "data": {
    "results": [
      {
        "delivery_id": "delivery-uuid-1",
        "delivery_code": 1001,
        "valid": true,
        "assignment_action": "assign"
      },
      {
        "delivery_id": "delivery-uuid-2",
        "delivery_code": 1002,
        "valid": false,
        "error_key": "delivery_assigned_to_different_driver",
        "error_message": "Delivery is already assigned to a different driver"
      }
    ],
    "total_valid": 1,
    "total_failed": 1
  }
}
```

## POST /delivery/trips/{{trip_id}}/deliveries — Sync Trip Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#post-sync-trip-deliveries)

### Realtime mobile contract

each newly attached unassigned delivery is immediately assigned to the Trip driver. The existing realtime assignment notification emits `assignment_status: "assigned"` and `requires_driver_response: false`; it is informational and never asks the driver to accept an already assigned Trip delivery. Use `POST /delivery/trips/{trip_id}/remove-deliveries` instead when removal must preserve order status and driver assignment.

Sync the full delivery membership of a trip. Only allowed while trip is `planned`.

This is a **replace** operation — the array sent becomes the exact set of deliveries on the trip. Delivery attachment, sequence changes, removals, assignment release, capacity release, and the batched `deliveries_added`, `deliveries_removed`, or `deliveries_reordered` trip events commit atomically. `sequence` is optional; if omitted, position index (1-based) is used. Send an empty array `[]` to remove all deliveries.

### Assignment behavior
- The trip must have a driver for a non-empty membership.
- All incoming deliveries are validated before assignment or membership writes; failures are returned in request order.
- Pending deliveries without active assignments are assigned through one shared preflight and the atomic batch-assignment transaction; existing validation results are reused instead of querying every assignment twice.
- Newly assigned rows use `status=assigned`; route calculation is scheduled asynchronously after commit. One bulk realtime summary and one bulk driver notification represent the newly assigned rows, with `assignment_status=assigned` and `requires_driver_response=false`.
- An existing active assignment is accepted only when it belongs to the same driver. A different-driver assignment is rejected and never automatically reassigned.
- Removed members are unassigned, capacity is released, status returns to `pending`, and trip fields are cleared.

### Eligibility rules for incoming deliveries
- Internal delivery with status `pending` or `assigned`
- Not deleted and not linked to a Waybill, route, route session, or another trip
- Not a P&D parent; only the currently active internal child leg may be attached

### Frontend integration contract
- Use this sync endpoint for add/remove membership; send the complete desired replacement array.
- Call the validate endpoint to display ordered per-delivery results before sync.
- Ensure the trip has `driver_id` before submitting a non-empty array.
- Replace local trip data with `data` from a successful response.
- On HTTP 409 with `code=trip_delivery_validation_failed`, keep previous membership and show each `data.results[].error_message` against its `delivery_id`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveries | array<object> |  |  |  |
| deliveries.delivery_id | string |  |  |  |
| deliveries.sequence | number |  |  |  |



### Request body (raw)

```json
{
  "deliveries": [
    {
      "delivery_id": "delivery-uuid-1",
      "sequence": 1
    },
    {
      "delivery_id": "delivery-uuid-3",
      "sequence": 2
    },
    {
      "delivery_id": "delivery-uuid-4",
      "sequence": 3
    }
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip deliveries synced successfully",
  "data": {
    "id": "trip-uuid",
    "summary": {
      "total": 3
    }
  }
}
```

### Response 409 Ordered delivery validation failed

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_delivery_validation_failed",
  "message": "One or more deliveries cannot be added to this trip",
  "data": {
    "results": [
      {
        "delivery_id": "delivery-uuid-1",
        "delivery_code": 1001,
        "valid": true,
        "assignment_action": "keep"
      },
      {
        "delivery_id": "delivery-uuid-3",
        "delivery_code": 1003,
        "valid": false,
        "error_key": "delivery_attached_to_waybill",
        "error_message": "Delivery is already attached to a waybill and cannot be added to a trip"
      }
    ],
    "total_valid": 1,
    "total_failed": 1
  }
}
```

## POST /delivery/trips/{{trip_id}}/remove-deliveries — Remove Orders from Trip

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#post-remove-orders-from-trip)

Atomically removes one or more displayed orders from a trip. Requires bearer authentication plus `manage:operations:tenant`, `manage:module:delivery`, or system-admin access. Tenant scope comes from the authenticated request; `Accept-Language` controls the localized message.

- Send one ID for single removal or multiple IDs for bulk removal.
- Duplicate IDs are accepted once; the first occurrence defines response order.

### Business rules
- Every selected ID and its displayed membership in the source trip are validated before any write. A missing or stale member rolls back the whole request.
- Every trip status and every order status are allowed.
- For active, completed, or planned live membership, only `trip_id` and `trip_sequence` are cleared. Order status, active assignment, assigned driver, capacity, and order history remain unchanged, so the driver continues to receive the order through normal driver-order APIs.
- The trip status and all unselected orders remain unchanged, including when the trip becomes empty. Maintained counts update transactionally; trip detail derives its displayed order list and stops from the remaining members.
- For cancelled trips, only matching entries in `historical_deliveries` are removed. Current order membership and any newer trip or driver assignment are never changed. Existing audit history is preserved and one `deliveries_removed` trip event records this action.

Success returns HTTP 200 only after the RPC transaction commits. `data.removed_delivery_ids` contains the requested unique IDs in first-seen order, `removed_count` is its length, and `summary.total` is the remaining displayed order count.

Important errors all use `{ success:false, code, message, data:null }`:
- HTTP 400 `delivery_ids_required` or `invalid_uuid`.
- HTTP 401/403 for bearer authentication or trip-management permission failure.
- HTTP 404 `trip_not_found` or `delivery_not_found`.
- HTTP 409 `delivery_not_in_trip` when membership changed.
- HTTP 500 `request_processing_failed` after the database operation rolls back.

### Frontend integration contract

call `POST /functions/v1/delivery/trips/{trip_id}/remove-deliveries` with the literal `{ "delivery_ids": [...] }` body. Treat the response fields as required. Branch on `code`, display `message`, and make no follow-up mutation to order status or driver assignment. No navigation action is returned.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/remove-deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| Accept-Language | header string |  |  | en |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_ids | array<string> | Yes | is a required non-empty array of UUIDs. |  |



### Request body (raw)

```json
{
  "delivery_ids": [
    "11111111-1111-4111-8111-111111111111",
    "22222222-2222-4222-8222-222222222222"
  ]
}
```

### Response 200 Success - Single Removal

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Orders removed from the trip successfully.",
  "data": {
    "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "removed_delivery_ids": [
      "11111111-1111-4111-8111-111111111111"
    ],
    "removed_count": 1,
    "summary": {
      "total": 7
    }
  }
}
```

### Response 200 Success - Bulk Removal to Empty Trip

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Orders removed from the trip successfully.",
  "data": {
    "id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "removed_delivery_ids": [
      "11111111-1111-4111-8111-111111111111",
      "22222222-2222-4222-8222-222222222222"
    ],
    "removed_count": 2,
    "summary": {
      "total": 0
    }
  }
}
```

### Response 400 Invalid Input

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_uuid",
  "message": "Enter a valid ID",
  "data": null
}
```

### Response 401 Authentication Required

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "unauthorized",
  "message": "Unauthorized",
  "data": null
}
```

### Response 403 Insufficient Permission

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_permissions",
  "message": "Insufficient Permissions",
  "data": null
}
```

### Response 404 Order Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_not_found",
  "message": "Delivery not found",
  "data": null
}
```

### Response 409 Membership Changed - Full Rollback

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_not_in_trip",
  "message": "One or more selected orders no longer belong to this trip.",
  "data": null
}
```

### Response 500 Rolled-back Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "request_processing_failed",
  "message": "Request processing failed",
  "data": null
}
```

## POST /delivery/trips/{{source_trip_id}}/move-deliveries — Move Orders Between Trips

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#post-move-orders-between-trips)

Atomically moves one or more orders from a source trip to an existing destination trip and that trip's actual driver. Requires bearer authentication plus `manage:operations:tenant`, `manage:module:delivery`, or system-admin access. Tenant scope comes only from the authenticated request; `Accept-Language` selects the localized display message.

### Business rules
- One RPC locks both trip rows in stable order, then locks every selected order, P&D preparation resource, assignment, and affected driver capacity row. Every ID, current source membership, order status, destination state, and destination driver is rechecked before the first write. Any failure rolls back the whole selection.
- `delivered` and `cancelled` orders are blocked. Other order statuses and the source trip's status are allowed. The destination must be a different `planned` or `in_progress` trip with an assigned driver.
- The locked destination trip's actual driver must equal `target_driver_id`. A mismatch returns HTTP 409 `target_trip_changed` and changes nothing. No new driver/trip lookup API is required; use the existing `GET /delivery/drivers` and `GET /delivery/trips` selectors.
- Orders retain their delivery status and prior history. Existing source and destination sequences are unchanged; moved orders are appended after the destination's current maximum sequence in their source order.
- When drivers differ, the current assignment, custody, capacity, P&D handoff projection, and assignment-backed driver list move to the receiving driver. A different destination trip with the same driver changes only trip membership.
- Both trip lifecycle statuses remain unchanged, including an empty source. Maintained counts update inside the transaction, and trip details derive their order lists and stops from the committed membership.
- For a cancelled source, displayed membership comes from its historical snapshot. Selected snapshot entries are removed only if each current order remains outside any newer trip, route, route session, or live Waybill; a newer live assignment is never silently detached.
- Both trips receive append-only move events containing actor, timestamp, trimmed reason, source/target trip IDs, immutable source/target trip-number snapshots, old/new driver IDs, order IDs, and source/target sequence values.

Success returns HTTP 200 only after commit. Every response field is required. `moved_delivery_ids` contains exactly the requested unique IDs in first-seen order; counts are the final displayed source and destination totals.

Important errors all use `{ success:false, code, message, data:null }`:
- HTTP 400 for missing fields, malformed UUIDs, an empty selection, or a reason outside 1–500 characters.
- HTTP 401/403 for bearer authentication or trip-management permission failure.
- HTTP 404 for a missing source trip, destination trip, order, or required driver record.
- HTTP 409 for stale source membership, blocked order status, an unavailable/same destination, missing destination driver/vehicle assignment, capacity conflict, missing current order assignment, or `target_trip_changed`.
- HTTP 500 `request_processing_failed` for a rolled-back unexpected server error.

### Frontend integration contract

call `POST /functions/v1/delivery/trips/{source_trip_id}/move-deliveries` with the literal four-field body above. Treat every success field as required. Branch on stable `code`, display the localized `message`, and refresh selectors before reconfirming `target_trip_changed`. New move-history entries are available through trip detail with `source_trip_number` and `target_trip_number`; fall back to the corresponding IDs for older events. No navigation action is returned.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/trips/{{source_trip_id}}/move-deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| Accept-Language | header string |  |  | en |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| source_trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_ids | array<string> | Yes | non-empty UUID array. Single and bulk moves use the same endpoint. Duplicate IDs are accepted once and returned in first-seen request order. |  |
| target_trip_id | string | Yes | destination trip UUID; it must differ from the source. |  |
| target_driver_id | string | Yes | the destination driver UUID shown in the frontend confirmation. |  |
| reason | string | Yes | trimmed, 1–500 characters. |  |



### Request body (raw)

```json
{
  "delivery_ids": [
    "11111111-1111-4111-8111-111111111111",
    "22222222-2222-4222-8222-222222222222"
  ],
  "target_trip_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
  "target_driver_id": "dddddddd-dddd-4ddd-8ddd-dddddddddddd",
  "reason": "Correct trip assignment"
}
```

### Response 200 Success - Single Move

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Orders moved successfully.",
  "data": {
    "source_trip_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "target_trip_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
    "moved_delivery_ids": [
      "11111111-1111-4111-8111-111111111111"
    ],
    "moved_count": 1,
    "source_summary": {
      "total": 7
    },
    "target_summary": {
      "total": 3
    }
  }
}
```

### Response 200 Success - Bulk Move Empties Source

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Orders moved successfully.",
  "data": {
    "source_trip_id": "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
    "target_trip_id": "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
    "moved_delivery_ids": [
      "11111111-1111-4111-8111-111111111111",
      "22222222-2222-4222-8222-222222222222"
    ],
    "moved_count": 2,
    "source_summary": {
      "total": 0
    },
    "target_summary": {
      "total": 5
    }
  }
}
```

### Response 400 Invalid Input

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "reason_required",
  "message": "Reason is required",
  "data": null
}
```

### Response 401 Authentication Required

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "unauthorized",
  "message": "Unauthorized",
  "data": null
}
```

### Response 403 Insufficient Permission

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_permissions",
  "message": "Insufficient Permissions",
  "data": null
}
```

### Response 404 Destination Trip Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "target_trip_not_found",
  "message": "The destination trip was not found.",
  "data": null
}
```

### Response 409 Destination Driver Changed - Full Rollback

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "target_trip_changed",
  "message": "The destination trip's driver changed. Refresh and confirm again.",
  "data": null
}
```

### Response 409 Order Status Conflict - Full Rollback

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_move_status_not_allowed",
  "message": "Delivered or cancelled orders cannot be moved between trips.",
  "data": null
}
```

### Response 409 Source Membership Changed - Full Rollback

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_not_in_trip",
  "message": "One or more selected orders no longer belong to this trip.",
  "data": null
}
```

### Response 500 Rolled-back Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "request_processing_failed",
  "message": "Request processing failed",
  "data": null
}
```

## DELETE /delivery/trips/{{trip_id}}/deliveries/{{delivery_id}} — Remove Delivery from Trip

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#delete-remove-delivery-from-trip)

Compatibility endpoint for removing one delivery from a trip. Requires bearer authentication plus `manage:operations:tenant`, `manage:module:delivery`, or system-admin access. It now uses the same atomic preservation workflow as `POST /delivery/trips/{trip_id}/remove-deliveries`: every trip and order status is allowed, only `trip_id` and `trip_sequence` are cleared, and the trip status, order status, active assignment, assigned driver, driver capacity, and histories remain unchanged. The response retains the existing trip-detail shape for compatibility. New dispatcher and tenant-admin flows should use the POST endpoint for its single/bulk contract. This endpoint does not delete the delivery.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/deliveries/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery removed from trip successfully",
  "data": {
    "id": "trip-uuid",
    "summary": {
      "total": 1
    }
  }
}
```

## PATCH /delivery/trips/{{trip_id}}/reorder — Reorder Trip Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#patch-reorder-trip-deliveries)

Reorder deliveries in the trip by providing all delivery IDs in the desired order.

- Must include all current trip deliveries — no missing, no extra
- trip_sequence will be reassigned as 1, 2, 3...
- Only allowed while trip is `planned`
- The returned `driver_execution.stop_list` uses the requested delivery order as a preference, but moves a grouped drop-off after every pickup required by the parcels inside it
- Stop grouping uses only exact valid latitude/longitude coordinates; address IDs and text are ignored

### Frontend integration contract
- Render the returned `data.driver_execution.stop_list`, `current_stop`, and `next_stop` directly.
- Do not assume each delivery row produces adjacent pickup and drop-off stops.
- No navigation behavior changes.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/reorder

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "delivery_ids": [
    "delivery-uuid-2",
    "delivery-uuid-1",
    "delivery-uuid-3"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip stop order updated successfully",
  "data": {
    "id": "trip-uuid",
    "status": "planned"
  }
}
```

### Response 409 Membership mismatch

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Reorder list must contain all trip deliveries — no more, no less",
  "data": null
}
```

## PATCH /delivery/trips/{{trip_id}}/stops/order — Save Trip Stop Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#patch-save-trip-stop-order)

Saves the dispatcher-defined order of whole stops for a planned trip. Requires bearer authentication plus `manage:operations:tenant`, `manage:module:delivery`, or system-admin access. Send every `stop_key` from the latest trip detail exactly once and send that detail response's unchanged `updated_at`.

The database locks the trip, rechecks tenant ownership, planned status, timestamp, and the complete current stop set, then saves in one transaction. Every delivery's pickup must precede its drop-off. A pickup shared by every current order must be first; a drop-off shared by every current order must be last. Independent pickup/drop-off pairs may interleave. Invalid input is rejected without partial writes or silent rearrangement. A successful save appends one `stops_reordered` audit event and changes only `stop_order`, `trip_events`, and `updated_at`; trip status, delivery status, driver assignment, delivery membership, delivery sequence, and stop membership remain unchanged.

Trip detail for both dispatchers and drivers uses the saved order. Starting the trip preserves it. When membership or copied delivery addresses change, the trip timestamp advances; effective reads ignore obsolete keys, preserve surviving priorities, and insert new stops in a deterministic dependency-safe position. The older delivery reorder endpoint clears the custom stop order.

### Important errors
- HTTP 400 `invalid_request`: malformed trip ID/body/timestamp, duplicate or unknown keys, or an incomplete stop set.
- HTTP 403 `insufficient_permissions`: caller cannot manage trips.
- HTTP 404 `trip_not_found`: the tenant-scoped trip does not exist.
- HTTP 409 `trip_changed`: `expected_updated_at` is stale.
- HTTP 409 `trip_not_reorderable`: the trip is no longer planned.
- HTTP 422 `invalid_stop_order`: pickup/drop-off dependency or shared-boundary rule failed.
- HTTP 500 `save_failed`: the transaction rolled back because of an unexpected server error.

### Frontend integration contract

drag locally and call this endpoint only on Apply. On HTTP 200, replace the Stops tab with `data.driver_execution` and store `data.updated_at`. On `trip_changed`, keep the draft but require Reset to latest before another Apply. For all failures, branch on `code` and display the localized `message`. No navigation action is returned.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/stops/order

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| ordered_stop_keys | array<string> |  |  |  |
| expected_updated_at | string |  |  |  |



### Request body (raw)

```json
{
  "ordered_stop_keys": [
    "pickup:36.19,44.01",
    "dropoff:36.2,44.02",
    "pickup:36.21,44.03",
    "dropoff:36.22,44.04"
  ],
  "expected_updated_at": "2026-09-10T10:00:00.000Z"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Stop order updated.",
  "data": {
    "trip_id": "source-trip-uuid",
    "status": "planned",
    "updated_at": "2026-09-10T10:05:00.000Z",
    "driver_execution": {
      "stop_list": [
        {
          "stop_key": "pickup:36.19,44.01",
          "stop_type": "pickup",
          "location_label": null,
          "location": {
            "latitude": 36.19,
            "longitude": 44.01
          },
          "deliveries": [
            {
              "delivery_id": "order-uuid-1",
              "delivery_code": "3341A",
              "barcode": "DEL3341A",
              "status": "assigned",
              "trip_sequence": 1,
              "assignment_status": "assigned",
              "next_action": "start_delivery",
              "bulk_eligible": true
            }
          ],
          "is_complete": false
        }
      ],
      "current_stop": {
        "stop_key": "pickup:36.19,44.01",
        "stop_type": "pickup",
        "location_label": null,
        "location": {
          "latitude": 36.19,
          "longitude": 44.01
        },
        "deliveries": [
          {
            "delivery_id": "order-uuid-1",
            "delivery_code": "3341A",
            "barcode": "DEL3341A",
            "status": "assigned",
            "trip_sequence": 1,
            "assignment_status": "assigned",
            "next_action": "start_delivery",
            "bulk_eligible": true
          }
        ],
        "is_complete": false
      },
      "next_stop": null
    }
  }
}
```

### Response 400 Invalid Request

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_request",
  "message": "Invalid request",
  "data": null
}
```

### Response 403 Insufficient Permission

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_permissions",
  "message": "Insufficient Permissions",
  "data": null
}
```

### Response 404 Trip Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_not_found",
  "message": "Trip not found",
  "data": null
}
```

### Response 409 Trip Changed

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_changed",
  "message": "The trip changed. Reset to the latest version and try again.",
  "data": null
}
```

### Response 409 Trip No Longer Planned

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_not_reorderable",
  "message": "Trip cannot be reordered in its current status",
  "data": null
}
```

### Response 422 Invalid Stop Dependency

Unprocessable Entity

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_stop_order",
  "message": "Every drop-off must follow the pickup stops required by its orders.",
  "data": null
}
```

### Response 500 Rolled-back Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "save_failed",
  "message": "The stop order could not be saved.",
  "data": null
}
```

## POST /delivery/trips/{{trip_id}}/cancel — Cancel Trip

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#post-cancel-trip)

Cancels a `planned` or `in_progress` trip only when every current delivery is still `pending`, `assigned`, or `en_route_to_pickup`. Any picked-up, in-transit, delivered, cancelled, returned, exception, or otherwise non-cancellable member blocks the whole action. Accepts optional `reason` and optional `notes` (each max 2000 characters) and requires trip-management permission.

### The operation is atomic

the trip becomes `cancelled`; status history is appended; a `trip_cancelled` actor event is appended; the ordered delivery snapshot is stored on the trip; active assignments become `unassigned`; driver capacity is released; deliveries return to `pending`; and live trip fields/stops are cleared. Repeating cancellation is idempotent and does not append another event or notification. On a real cancellation, the assigned driver receives one critical in-app modal requiring acknowledgement plus push notification; the notification tells the driver not to continue and links to the trip.

### Frontend integration contract

after success, load `GET /delivery/trips/{id}`. Render the cancellation story from `data.trip_events`; render `data.deliveries` as read-only when `data.deliveries_mode=historical`. `current_stop`, `next_stop`, and `stop_list` remain empty.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/cancel

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| reason | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "reason": "Trip plan cancelled before pickup",
  "notes": "Vehicle issue reported before pickup"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip cancelled successfully",
  "data": {
    "id": "trip-uuid",
    "previous_status": "in_progress",
    "status": "cancelled",
    "changed": true,
    "started_at": "2026-07-29T12:30:00Z",
    "completed_at": null,
    "released_delivery_count": 3,
    "released_assignment_count": 3,
    "available_actions": []
  }
}
```

### Response 409 Not cancellable

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_deliveries_not_releasable",
  "message": "The trip cannot be cancelled after parcel custody has started",
  "data": null
}
```

## GET /delivery/driver-trips — Driver: List My Trips

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#get-driver-list-my-trips)

### Driver-facing

list trips assigned to the authenticated driver.

Driver sees trip_number, status, item_count, and timestamps. They can then call GET /delivery/trips/:id to get the full stop list for execution. The response is bounded to the requested page (maximum 100); item_count and the all-status summary are database-maintained/aggregated and do not load every matching row into the API runtime.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver-trips?page=1&limit=20&status=in_progress

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number | 1 |
| limit | query string |  | Items per page (max 100) | 20 |
| status | query string |  | Filter by status | in_progress |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trips retrieved successfully",
  "data": {
    "trips": [
      {
        "id": "trip-uuid",
        "trip_number": "TRIP-00001",
        "status": "in_progress",
        "trip_type": "multi_pickup",
        "item_count": 3,
        "started_at": "2026-04-11T08:00:00Z",
        "completed_at": null
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20
  }
}
```

## GET /delivery/trips/{{trip_id}}/returns — Get Trip Partial Returns

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#get-get-trip-partial-returns)

Convenience endpoint — returns only the deliveries in this trip that have one or more return requests.

Useful for a focused 'returns' tab on the trip detail screen without loading the full trip payload.

### Each item includes
- delivery_id, delivery_code, barcode, delivery_status, return_status, trip_sequence
- return_requests array with id, status, return_reason, return_delivery_id, requested_at, notes

### Note

return requests are created via the existing POST /delivery/returns endpoint — no new endpoint needed for creation.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/returns

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success — with returns

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip returns retrieved successfully",
  "data": [
    {
      "delivery_id": "uuid-d2",
      "delivery_code": 1002,
      "barcode": "BC002",
      "delivery_status": "returned",
      "return_status": "return_approved",
      "trip_sequence": 2,
      "return_requests": [
        {
          "id": "rr-uuid",
          "status": "approved",
          "return_reason": "Customer refused delivery",
          "return_delivery_id": "uuid-return-delivery",
          "requested_at": "2026-04-11T10:30:00Z",
          "notes": null
        }
      ]
    },
    {
      "delivery_id": "uuid-d5",
      "delivery_code": 1005,
      "barcode": "BC005",
      "delivery_status": "returned",
      "return_status": "return_requested",
      "trip_sequence": 5,
      "return_requests": [
        {
          "id": "rr-uuid-2",
          "status": "pending",
          "return_reason": "Wrong address",
          "return_delivery_id": null,
          "requested_at": "2026-04-11T11:00:00Z",
          "notes": null
        }
      ]
    }
  ]
}
```

### Response 200 Success — no returns yet

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip returns retrieved successfully",
  "data": []
}
```

### Response 404 Trip not found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Trip not found",
  "data": null
}
```

## POST /delivery/trips/{{trip_id}}/stops/bulk-action — Driver: Execute Trip Stop Bulk Action

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#post-driver-execute-trip-stop-bulk-action)

Driver-facing group action for selected deliveries at one derived trip stop. The authenticated user must have the driver role, must be online/active/available, and must be the trip's assigned driver. Send the exact `stop_key` returned by GET trip detail plus 1 to 200 unique delivery UUIDs. Every ID is revalidated against tenant, trip, stop, driver assignment, current action, exceptions, returns, terminal state, carrier, and active Waybill rules; client selection is never trusted as authorization.

### Actions
- `start_delivery`: pickup stop; `assigned` or `accepted` assignment becomes `en_route_to_pickup`, using the same canonical transition and side effects as the individual sheet.
- `pickup`: pickup stop; proofless single-parcel deliveries become `picked_up`. A delivery needing pickup proof or parcel review returns `outcome=individual_action_required` without mutation.
- `in_transit`: drop-off stop; `picked_up` becomes assignment `en_route_to_delivery` and delivery `in_transit`.
- `complete`: drop-off stop; every valid delivery returns `outcome=individual_action_required` without mutation so the app opens that order's existing completion/proof screen. No proof fields are accepted by this endpoint and proof is never shared.

The valid request returns HTTP 200 even when individual orders fail. `data.results` remains in trip sequence order and each row contains delivery ID/code, `outcome` (`applied`, `individual_action_required`, or `failed`), localized `message`, machine `code` when relevant, and authoritative status fields. Successful orders are not rolled back when another order fails. Fixed validation reads are batched. Eligible pickups are sent through one service-role database RPC; that RPC invokes the canonical guarded transition in request order and isolates expected per-delivery business failures. Unexpected database faults abort the outer transaction. Already-paid deliveries cause no payment write, while unpaid zero-fee deliveries use one set-based status update; positive sender-cash fees keep the existing idempotent COD/payment owner. Other actions retain bounded concurrency 10. Trip navigation does not fan out external route-calculation requests per order.

### Important request errors
- HTTP 400: missing stop key, invalid action, invalid/duplicate IDs, more than 200 IDs, or invalid UUID.
- HTTP 403: not a driver, trip belongs to another driver, or driver is offline/inactive.
- HTTP 404: trip or driver not found.
- HTTP 409: trip is no longer planned/in progress.

### Frontend integration contract
- Use `data.driver_execution.stop_list[].stop_key`. Show selection only for deliveries whose literal `next_action` matches the button action.
- Send one common action and selected delivery IDs. The button text includes the selection count, for example `Start 7 deliveries`.
- Apply `outcome=applied`, display every failed delivery by `delivery_code`, and continue processing the remaining rows.
- For `individual_action_required`, open each delivery's existing pickup or completion screen sequentially in result order. Refresh the trip once after the group/queue finishes.
- Keep individual-card actions and per-order exception actions available. Branch on codes, never localized message text. No navigation action is returned by the backend.

### Event actor contract

new driver-performed status events use the validated assigned `drivers.id` as `actor_id`, including grouped pickup. The authenticated user ID remains available to backend authorization and custody logic; clients do not send an actor ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/trips/{{trip_id}}/stops/bulk-action

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| trip_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| stop_key | string |  |  |  |
| action | string |  |  |  |
| delivery_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "stop_key": "pickup:33.3,44.4",
  "action": "start_delivery",
  "delivery_ids": [
    "00000000-0000-4000-8000-000000000001",
    "00000000-0000-4000-8000-000000000002"
  ]
}
```

### Response 200 Partial success with per-order proof queue

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip stop action completed",
  "data": {
    "trip_id": "trip-uuid",
    "stop_key": "dropoff:33.3,44.4",
    "action": "complete",
    "results": [
      {
        "delivery_id": "00000000-0000-4000-8000-000000000001",
        "delivery_code": "ORD-1",
        "outcome": "individual_action_required",
        "success": true,
        "previous_assignment_status": "en_route_to_delivery",
        "assignment_status": "en_route_to_delivery",
        "delivery_status": "in_transit",
        "code": "trip_stop_individual_action_required",
        "message": "Continue with this delivery individually to review parcels or add proof"
      },
      {
        "delivery_id": "00000000-0000-4000-8000-000000000002",
        "delivery_code": "ORD-2",
        "outcome": "failed",
        "success": false,
        "previous_assignment_status": "en_route_to_delivery",
        "assignment_status": "en_route_to_delivery",
        "delivery_status": "in_transit",
        "code": "delivery_has_open_exception",
        "message": "Resolve the active delivery exception before continuing"
      }
    ],
    "requested_count": 2,
    "applied_count": 0,
    "individual_action_required_count": 1,
    "failed_count": 1
  }
}
```

### Response 403 Trip belongs to another driver

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "trip_not_assigned_to_driver",
  "message": "This trip is not assigned to you",
  "data": null
}
```

## GET /delivery/driver-trips/status-summary — Driver: Trip Status Summary

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/trips#get-driver-trip-status-summary)

### Driver-facing

returns database-aggregated counts per trip status for the authenticated driver without loading individual trip rows into the API runtime.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver-trips/status-summary

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Trip status summary retrieved successfully",
  "data": {
    "planned": 2,
    "in_progress": 1,
    "completed": 15,
    "cancelled": 0
  }
}
```

## GET /delivery/routes — List Routes

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#get-list-routes)

Dispatcher-only list of reusable route lanes. Excludes inactive routes by default. Each row includes optional `driver_id` and compact `driver` scheduling configuration plus read-only `next_session_at`. The route driver is not operational ownership. Driver users obtain route context through their own `/delivery/route-sessions` rows.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/routes?page=1&limit=20&search=&include_inactive=false

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number | 1 |
| limit | query string |  | Items per page (max 100) | 20 |
| search | query string |  | Partial match on route_name |  |
| include_inactive | query string |  | Include soft-deleted routes | false |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Routes retrieved successfully",
  "data": {
    "routes": [
      {
        "id": "route-uuid",
        "route_name": "Warehouse A → Zone 5",
        "driver_id": "driver-uuid",
        "driver": {
          "id": "driver-uuid",
          "full_name": "Scheduled Driver",
          "avatar_url": null
        },
        "origin_type": "warehouse",
        "origin_warehouse_id": "wh-uuid",
        "destination_type": "location",
        "destination_location": {
          "lat": 33.5,
          "lng": 44.2,
          "address": "Zone 5 Hub"
        },
        "is_active": true,
        "schedule_time": "08:00:00",
        "schedule_days": [
          1,
          2,
          3,
          4,
          5
        ],
        "start_date": "2026-04-15",
        "next_session_at": "2026-04-15T05:00:00Z",
        "notes": null,
        "created_at": "2026-04-14T10:00:00Z",
        "active_orders_count": 8
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20
  }
}
```

## POST /delivery/routes — Create Route

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#post-create-route)

Create a reusable route lane.

- `origin_type` / `destination_type`: warehouse | location
- If type is `warehouse`: provide `*_warehouse_id`
- If type is `location`: provide `*_location` as `{lat, lng, address}`
- `driver_id` is optional and is used only to create future sessions automatically from this route's schedule. It does not own the route and is ignored by manual assign-deliveries requests.
- `driver_id`, `schedule_time`, `schedule_days`, and `start_date` must be provided together or all omitted/null.
- The driver must be active and belong to the authenticated tenant.
- Scheduled creation uses the tenant IANA timezone and returns read-only `next_session_at`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/routes

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| route_name | string |  |  |  |
| origin_type | string |  |  |  |
| origin_warehouse_id | string |  |  |  |
| destination_type | string |  |  |  |
| destination_location | object |  |  |  |
| destination_location.lat | number |  |  |  |
| destination_location.lng | number |  |  |  |
| destination_location.address | string |  |  |  |
| driver_id | string |  |  |  |
| schedule_time | string |  |  |  |
| schedule_days | array<number> |  |  |  |
| start_date | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "route_name": "Warehouse A → Zone 5",
  "origin_type": "warehouse",
  "origin_warehouse_id": "wh-uuid",
  "destination_type": "location",
  "destination_location": {
    "lat": 33.5,
    "lng": 44.2,
    "address": "Zone 5 Hub"
  },
  "driver_id": "driver-uuid",
  "schedule_time": "08:00:00",
  "schedule_days": [
    1,
    2,
    3,
    4,
    5
  ],
  "start_date": "2026-04-15",
  "notes": "Morning route"
}
```

### Response 201 Success

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route created successfully",
  "data": {
    "id": "route-uuid",
    "route_name": "Warehouse A → Zone 5",
    "driver_id": "driver-uuid",
    "driver": {
      "id": "driver-uuid",
      "full_name": "Scheduled Driver",
      "avatar_url": null
    },
    "is_active": true,
    "schedule_time": "08:00:00",
    "schedule_days": [
      1,
      2,
      3,
      4,
      5
    ],
    "start_date": "2026-04-15",
    "next_session_at": "2026-04-15T05:00:00Z",
    "created_at": "2026-04-14T10:00:00Z"
  }
}
```

### Response 400 Incomplete schedule configuration

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "route_schedule_configuration_incomplete",
  "message": "Driver, schedule time, schedule days, and start date must be provided together",
  "data": null
}
```

### Response 400 Invalid route schedule

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Route schedule must include a valid time, start date, and unique weekdays from 0 to 6",
  "data": null
}
```

### Response 409 Duplicate name

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Route name already exists",
  "data": null
}
```

## GET /delivery/routes/:routeId — Get Route

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#get-get-route)

Dispatcher-only route detail. Returns the fixed lane, optional reserved scheduling `driver_id` and compact `driver`, read-only `next_session_at`, warehouse/location information, and aggregate `session_counts`. It does not return an ambiguous singular current session.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/routes/:routeId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| routeId | path string | Yes |  | route-uuid |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route retrieved successfully",
  "data": {
    "id": "route-uuid",
    "route_name": "Warehouse A → Zone 5",
    "driver_id": "driver-uuid",
    "driver": {
      "id": "driver-uuid",
      "full_name": "Scheduled Driver",
      "avatar_url": null
    },
    "origin_type": "warehouse",
    "origin_warehouse": {
      "id": "wh-uuid",
      "name": "Warehouse A",
      "code": "WH-001"
    },
    "destination_type": "location",
    "destination_location": {
      "lat": 33.5,
      "lng": 44.2,
      "address": "Zone 5 Hub"
    },
    "schedule_time": "08:00:00",
    "schedule_days": [
      1,
      2,
      3,
      4,
      5
    ],
    "start_date": "2026-04-15",
    "next_session_at": "2026-04-15T05:00:00Z",
    "session_counts": {
      "active": 2,
      "completed": 1,
      "total": 3
    },
    "is_active": true
  }
}
```

### Response 403 Driver route not assigned to authenticated driver

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions",
  "data": null
}
```

## PUT /delivery/routes/:routeId — Update Route

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#put-update-route)

Partial route-lane update. Route `driver_id` is optional reserved configuration used only for future scheduled sessions. Changing it never changes an existing session. When any scheduling field is changed, the resulting `driver_id`, `schedule_time`, `schedule_days`, and `start_date` must all be present or all null. To disable scheduling, send all four fields as null in the same request. Manual assign-deliveries behavior is unchanged.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/routes/:routeId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| routeId | path string | Yes |  | route-uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| route_name | string |  |  |  |
| notes | string |  |  |  |
| driver_id | string |  |  |  |
| schedule_time | string |  |  |  |
| schedule_days | array<number> |  |  |  |
| start_date | string |  |  |  |



### Request body (raw)

```json
{
  "route_name": "Warehouse A → Zone 6",
  "notes": "Updated route",
  "driver_id": "driver-uuid",
  "schedule_time": "07:00:00",
  "schedule_days": [
    1,
    3,
    5
  ],
  "start_date": "2026-04-15"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route updated successfully",
  "data": {
    "id": "route-uuid",
    "route_name": "Warehouse A → Zone 6",
    "driver_id": "driver-uuid",
    "driver": {
      "id": "driver-uuid",
      "full_name": "Scheduled Driver",
      "avatar_url": null
    },
    "schedule_time": "07:00:00",
    "schedule_days": [
      1,
      3,
      5
    ],
    "start_date": "2026-04-15",
    "next_session_at": "2026-04-15T04:00:00Z",
    "updated_at": "2026-04-14T11:00:00Z"
  }
}
```

## DELETE /delivery/routes/:routeId — Delete Route (Deactivate)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#delete-delete-route-deactivate)

Soft-deactivate a route (sets is_active=false). Rejected if the route has open/started sessions with active deliveries.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/routes/:routeId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| routeId | path string | Yes |  | route-uuid |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route deactivated successfully",
  "data": null
}
```

### Response 409 Active sessions exist

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot deactivate route with active sessions that have deliveries",
  "data": null
}
```

## POST /delivery/routes/:routeId/assign-deliveries — Assign Deliveries to Route

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#post-assign-deliveries-to-route)

Prepared P&D/operational-return deliveries use the same assignment owner as direct assignment. In the route transaction, each eligible row keeps its completed pick and reserved warehouse stock and receives one pending handoff for the route driver. Waybill and unrelated deliveries keep their existing route behavior. Realtime mobile contract: the existing consolidated `delivery_realtime_assignments` row emits `assignment_status: "assigned"` and `requires_driver_response: false`. Mobile keeps the current informational notification and does not show Accept/Reject. Missing or unknown actionability is non-actionable.

Assign deliveries to a route. The operation is all-or-nothing and accepts at most 100 UUIDs.

### Exactly one session mode is required
- `driver_id`: reuses the newest open or started session for the same route and driver and returns `assignment_outcome: existing_session`; if none exists, it intentionally creates one open session and returns `new_session_created`.
- `session_id`: appends to that exact session when it is open or started and returns `existing_session`. If it is past started or otherwise cannot accept orders, the backend creates exactly one replacement session for the batch and returns `fallback_session_created`.

For a fallback, `requested_session` identifies what the dispatcher requested, `session` identifies where every delivery in the successful batch was assigned, and `fallback_reason.code` is authoritative. In `driver_id` mode, `requested_session` is always null because no session ID was requested; `session` is the reused or newly created target. Supported reason codes are `SESSION_NOT_ACCEPTING_ORDERS`, `SESSION_COMPLETED`, `SESSION_CANCELLED`, and `SESSION_ROUTE_MISMATCH`. If the immutable driver cannot be used, HTTP 409 returns `SESSION_DRIVER_UNAVAILABLE` and no replacement session remains.

The selected driver must be active/available, have a primary active vehicle, have route-zone access, and have enough capacity. Deliveries must be pending internal child/standalone deliveries with no trip, Waybill, route, route session, or active assignment. The route/session locks, one optional session insert, batch assignments, delivery membership, and capacity update commit atomically in one database transaction; complete failure rolls everything back. Realtime and push notification remain best effort after commit.

### Frontend integration contract
- Send `{delivery_ids, driver_id}` to reuse-or-create for that route and driver, or `{delivery_ids, session_id}` to request an exact session. Never send both.
- Branch only on `data.assignment_outcome`. `existing_session` may have `requested_session: null` when the request used `driver_id`; always use `data.session` as the actual target. For `fallback_session_created`, show the localized `data.fallback_reason.message`, name/link `data.session`, and retain `data.requested_session` only as context.
- Do not compare IDs or parse localized message text. `data.fallback_reason.code` is the stable reason contract.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/routes/:routeId/assign-deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| routeId | path string | Yes |  | route-uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_ids | array<string> |  |  |  |
| driver_id | string |  |  |  |



### Request body (raw)

```json
{
  "delivery_ids": [
    "delivery-uuid-1",
    "delivery-uuid-2",
    "delivery-uuid-3"
  ],
  "driver_id": "driver-uuid"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Orders assigned successfully",
  "data": {
    "assignment_outcome": "fallback_session_created",
    "requested_session": {
      "id": "requested-session-uuid",
      "session_number": 1,
      "status": "in_transit"
    },
    "session": {
      "id": "new-session-uuid",
      "session_number": 2,
      "status": "open",
      "route_id": "route-uuid",
      "driver_id": "driver-uuid",
      "tenant_id": "tenant-uuid",
      "created_at": "2026-08-06T10:00:00Z",
      "started_at": null,
      "updated_at": "2026-08-06T10:00:00Z",
      "cancelled_at": null,
      "completed_at": null,
      "picked_up_at": null,
      "proof_requirements": null,
      "proof_of_pickup": null,
      "proof_of_delivery": null,
      "driver": {
        "id": "driver-uuid",
        "full_name": "Ahmed Hassan",
        "avatar_url": null
      }
    },
    "fallback_reason": {
      "code": "SESSION_NOT_ACCEPTING_ORDERS",
      "message": "The requested session can no longer accept orders."
    },
    "assigned_count": 2
  }
}
```

### Response 200 Existing session success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Orders added to the session successfully",
  "data": {
    "assignment_outcome": "existing_session",
    "requested_session": {
      "id": "session-uuid",
      "session_number": 1,
      "status": "open"
    },
    "session": {
      "id": "session-uuid",
      "session_number": 1,
      "status": "open",
      "route_id": "route-uuid",
      "driver_id": "driver-uuid"
    },
    "fallback_reason": null,
    "assigned_count": 2
  }
}
```

### Response 200 Driver-mode reuse success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Orders added to the session successfully",
  "data": {
    "assignment_outcome": "existing_session",
    "requested_session": null,
    "session": {
      "id": "reused-session-uuid",
      "session_number": 3,
      "status": "started",
      "route_id": "route-uuid",
      "driver_id": "driver-uuid"
    },
    "fallback_reason": null,
    "assigned_count": 2
  }
}
```

### Response 200 Intentional new session success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Session created and orders assigned successfully",
  "data": {
    "assignment_outcome": "new_session_created",
    "requested_session": null,
    "session": {
      "id": "new-session-uuid",
      "session_number": 2,
      "status": "open",
      "route_id": "route-uuid",
      "driver_id": "driver-uuid"
    },
    "fallback_reason": null,
    "assigned_count": 2
  }
}
```

### Response 409 Delivery plan conflict

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "route_delivery_validation_failed",
  "message": "One or more deliveries cannot be added to this route",
  "data": {
    "results": [
      {
        "delivery_id": "delivery-uuid-1",
        "delivery_code": 2567,
        "valid": false,
        "error_key": "delivery_already_in_trip",
        "error_message": "Delivery is already assigned to another trip"
      }
    ],
    "total_valid": 0,
    "total_failed": 1
  }
}
```

### Response 400 Missing or ambiguous session mode

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "route_session_selection_required",
  "message": "Provide either a driver to create a new session or a session to add deliveries to",
  "data": null
}
```

### Response 409 Capacity exceeded

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver does not have enough capacity for these deliveries",
  "data": null
}
```

## POST /delivery/routes/:routeId/remove-deliveries — Remove Deliveries from Route

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#post-remove-deliveries-from-route)

Remove deliveries from the route session. Only allowed if the session is open or started. The submitted batch is one transaction. Each current assignment is preserved as `unassigned` audit history, route/session membership is cleared, delivery status returns to `pending`, and driver capacity is released. For prepared P&D work, the completed pick and stock reservation remain; the same pending handoff keeps its identity but clears assignment and driver context until reassignment.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/routes/:routeId/remove-deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| routeId | path string | Yes |  | route-uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "delivery_ids": [
    "delivery-uuid-1"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Deliveries removed from route successfully",
  "data": {
    "removed_count": 1
  }
}
```

### Response 409 Session picked_up

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot remove deliveries from a session that is picked_up or beyond",
  "data": null
}
```

## GET /delivery/routes/:routeId/sessions — List Sessions for Route

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#get-list-sessions-for-route)

Dispatcher view — paginated sessions for a route ordered newest first. Every session includes its immutable `driver_id` and driver summary. The `statuses` matrix is unaffected by the active status filter. `orders_count` is current membership for active sessions or snapshot size for cancelled sessions; `released_orders_count` reports released cancellation history.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/routes/:routeId/sessions?status=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| routeId | path string | Yes |  | route-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | query string |  | Filter: open \| started \| picked_up \| completed \| cancelled |  |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 20, max: 100) | 20 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route sessions retrieved successfully",
  "data": {
    "sessions": [
      {
        "id": "session-uuid-2",
        "session_number": 2,
        "status": "open",
        "driver_id": "driver-b",
        "driver": {
          "id": "driver-b",
          "full_name": "Driver B",
          "avatar_url": null
        },
        "orders_count": 3,
        "released_orders_count": 0,
        "created_at": "2026-04-14T05:00:00Z"
      },
      {
        "id": "session-uuid",
        "session_number": 1,
        "status": "completed",
        "driver_id": "driver-a",
        "driver": {
          "id": "driver-a",
          "full_name": "Driver A",
          "avatar_url": null
        },
        "orders_count": 4,
        "released_orders_count": 0,
        "created_at": "2026-04-13T07:55:00Z"
      }
    ],
    "statuses": [
      {
        "status": "open",
        "label": "Open",
        "count": 1
      },
      {
        "status": "completed",
        "label": "Completed",
        "count": 1
      }
    ],
    "total": 2,
    "page": 1,
    "limit": 20
  }
}
```

## GET /delivery/route-sessions — List Driver Route Sessions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#get-list-driver-route-sessions)

Driver-facing. Driver-role callers receive only sessions whose immutable `driver_id` matches their authenticated driver record; query parameters cannot widen that scope. Dispatcher-only callers can list all tenant sessions and filter by driver_id or route_id. Rows include route context, immutable `driver_id`, driver summary, and order counts.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/route-sessions?status=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | query string |  | Filter: open \| started \| picked_up \| completed \| cancelled |  |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 20, max: 100) | 20 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route sessions retrieved successfully",
  "data": {
    "sessions": [
      {
        "id": "session-uuid",
        "session_number": 2,
        "status": "started",
        "driver_id": "driver-uuid",
        "driver": {
          "id": "driver-uuid",
          "full_name": "Ahmed Hassan",
          "avatar_url": null
        },
        "orders_count": 4,
        "released_orders_count": 0,
        "created_at": "2026-04-14T05:00:00Z",
        "route": {
          "id": "route-uuid",
          "route_name": "Warehouse A → Zone 5",
          "origin_type": "warehouse",
          "destination_type": "location",
          "destination_location": {
            "lat": 33.5,
            "lng": 44.2,
            "address": "Zone 5 Hub"
          }
        }
      }
    ],
    "statuses": [
      {
        "status": "started",
        "label": "Started",
        "count": 1
      }
    ],
    "total": 1,
    "page": 1,
    "limit": 20
  }
}
```

## GET /delivery/route-sessions/:sessionId — Get Route Session Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#get-get-route-session-detail)

Returns session detail with route info and summary. This endpoint does not include delivery rows or the raw cancellation order snapshot.

`session.proof_requirements` is the stored strictest requirement calculated when deliveries are added or removed. `session.proof_of_pickup` and `session.proof_of_delivery` expose the shared proof after capture for both driver and dispatcher/tenant-admin screens.

For a cancelled session, `session.cancellation` contains `cancelled_at`, `reason`, and the authenticated `cancelled_by` actor snapshot. `session.orders_count` and `session.released_orders_count` and `summary.total` come from the immutable cancellation snapshot. Its collection summary is zero because released orders no longer belong operationally to that run.

Driver-role callers can access only sessions assigned to their own driver record. Dispatcher-only callers can access any session in the tenant.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/route-sessions/:sessionId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sessionId | path string | Yes |  | session-uuid |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route session retrieved successfully",
  "data": {
    "session": {
      "id": "session-uuid",
      "session_number": 2,
      "status": "started",
      "driver_id": "driver-uuid",
      "driver": {
        "id": "driver-uuid",
        "full_name": "Ahmed Hassan",
        "avatar_url": null
      },
      "started_at": "2026-04-14T05:05:00Z",
      "picked_up_at": null,
      "proof_requirements": {
        "pickup": {
          "photo_required": true,
          "signature_required": false
        },
        "delivery": {
          "photo_required": true,
          "signature_required": true
        }
      },
      "proof_of_pickup": null,
      "proof_of_delivery": null
    },
    "route": {
      "id": "route-uuid",
      "route_name": "Warehouse A → Zone 5",
      "origin_type": "warehouse",
      "destination_type": "location"
    },
    "summary": {
      "total": 5,
      "status_breakdown": {
        "assigned": 3,
        "picked_up": 2
      },
      "collection_summary": {
        "total_to_collect": 15000,
        "pickup_total_to_collect": 0,
        "delivery_total_to_collect": 15000,
        "deliveries_to_collect_count": 1,
        "delivery_collection_required_count": 1,
        "requires_collection_confirmation": true
      }
    }
  }
}
```

### Response 200 Cancelled session

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route session retrieved successfully",
  "data": {
    "session": {
      "id": "session-uuid",
      "status": "cancelled",
      "cancelled_at": "2026-08-03T10:00:00Z",
      "orders_count": 4,
      "released_orders_count": 4,
      "cancellation": {
        "cancelled_at": "2026-08-03T10:00:00Z",
        "reason": "Driver unavailable",
        "cancelled_by": {
          "id": "user-uuid",
          "full_name": "Admin Name"
        }
      }
    },
    "route": {
      "id": "route-uuid",
      "route_name": "Warehouse A → Zone 5"
    },
    "summary": {
      "total": 4,
      "status_breakdown": {
        "released": 4
      },
      "collection_summary": {
        "total_to_collect": 0,
        "pickup_total_to_collect": 0,
        "delivery_total_to_collect": 0,
        "deliveries_to_collect_count": 0,
        "delivery_collection_required_count": 0,
        "requires_collection_confirmation": false
      }
    }
  }
}
```

### Response 403 Driver session not assigned to authenticated driver

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions",
  "data": null
}
```

## GET /delivery/route-sessions/:sessionId/deliveries — List Route Session Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#get-list-route-session-deliveries)

Paginated deliveries for a route session. Driver-role callers can access only sessions assigned to their own driver record; dispatcher-only callers can access any tenant session. Search is a case-insensitive partial match over delivery code, barcode, and external reference.

Active sessions return currently linked deliveries with `released_orders_count=0`. Cancelled sessions return immutable snapshot rows in the same `deliveries` array. A released historical row has `status=pending` (its release-time operational state), `status_at_cancellation`, `session_membership_status=released`, and `released_at`; it is not labelled cancelled unless the delivery itself was genuinely cancelled. `orders_count` is the unfiltered session membership total, while `total` is the filtered result count. Released-session collection totals are zero. Existing `page`, `limit`, `status`, and `search` behavior remains available.

### G2G origin display

every live `data.deliveries[]` row includes nullable `source_transfer_manifest_id` and `g2g_origin`. A non-null origin contains `{ "branchName": string|null, "branchAddress": string|null }` from a valid saved `manifest_origin_v1` snapshot. A staged child uses its own usable snapshot first, then its same-tenant, non-deleted parent only when the child manifest ID is absent or matches the parent. Origin fields are inherited together; partial fields are not combined from different snapshots. Normal orders return null for both fields. A known manifest-linked order with no usable saved origin keeps its manifest ID and returns `g2g_origin=null`. Frontend integration: show a separate read-only Original G2G location line for known manifest orders, displaying any genuine branch name/address available or Origin unavailable when origin is null. Hide it for non-G2G orders. Keep pickup/delivery locations, customer, route navigation, collection amounts, actions, pagination, and totals unchanged. The existing paginated delivery query reads the origin snapshot and parent together; no per-card lookup is needed. Cancelled-session historical snapshots do not acquire these fields.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/route-sessions/:sessionId/deliveries?page=1&limit=50&status=&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sessionId | path string | Yes |  | session-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 50, max: 100) | 50 |
| status | query string | No | Optional delivery status filter, including the terminal `failed` status |  |
| search | query string | No | Optional search by delivery_code, barcode, or external_reference_id |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route session deliveries retrieved successfully",
  "data": {
    "deliveries": [
      {
        "id": "del-uuid",
        "delivery_code": "3213",
        "barcode": "DEL3213",
        "status": "assigned",
        "payment_method": "cod",
        "payer_type": "receiver",
        "cod_amount": 10000,
        "delivery_fee": 5000,
        "feeToCollectAtPickup": 0,
        "feeToCollectAtDelivery": 15000,
        "amountToCollect": 15000,
        "source_transfer_manifest_id": "manifest-uuid",
        "g2g_origin": {
          "branchName": "Original branch",
          "branchAddress": "Original branch address"
        }
      }
    ],
    "orders_count": 1,
    "released_orders_count": 0,
    "summary": {
      "total_to_collect": 15000,
      "pickup_total_to_collect": 0,
      "delivery_total_to_collect": 15000,
      "deliveries_to_collect_count": 1,
      "delivery_collection_required_count": 1,
      "requires_collection_confirmation": true
    },
    "proof_requirements": {
      "pickup": {
        "photo_required": true,
        "signature_required": false
      },
      "delivery": {
        "photo_required": true,
        "signature_required": true
      }
    },
    "proof_of_pickup": null,
    "proof_of_delivery": null,
    "total": 1,
    "page": 1,
    "limit": 50,
    "has_more": false
  }
}
```

### Response 200 Cancelled session history

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route session deliveries retrieved successfully",
  "data": {
    "deliveries": [
      {
        "id": "delivery-uuid",
        "delivery_code": "DEL-123",
        "status": "pending",
        "status_at_cancellation": "assigned",
        "session_membership_status": "released",
        "released_at": "2026-08-03T10:00:00Z"
      }
    ],
    "orders_count": 4,
    "released_orders_count": 4,
    "summary": {
      "total_to_collect": 0,
      "pickup_total_to_collect": 0,
      "delivery_total_to_collect": 0,
      "deliveries_to_collect_count": 0,
      "delivery_collection_required_count": 0,
      "requires_collection_confirmation": false
    },
    "total": 4,
    "page": 1,
    "limit": 50,
    "has_more": false
  }
}
```

## POST /delivery/route-sessions/:sessionId/start — Start Session (Driver)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#post-start-session-driver)

Driver action. Transitions session: open → started and sets `started_at`. The session must contain at least one active non-terminal delivery.

Returns `409 session_has_no_deliveries` when the session is empty. Blocked with `delivery_has_open_exception` when any non-terminal delivery in the session has an open exception.

### Frontend integration contract
- Disable the Start action when the session delivery total is `0`.
- If the API returns HTTP 409 with the localized empty-session message, keep the session open and show that message.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/route-sessions/:sessionId/start

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sessionId | path string | Yes |  | session-uuid |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route session started successfully",
  "data": {
    "id": "session-uuid",
    "status": "started",
    "started_at": "2026-04-14T08:05:00Z"
  }
}
```

### Response 409 No active deliveries

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "session_has_no_deliveries",
  "message": "Add at least one active delivery before starting the route session",
  "data": null
}
```

### Response 409 Wrong status

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot start a session with status 'picked_up'",
  "data": null
}
```

## POST /delivery/route-sessions/:sessionId/pickup — Pickup Session (Driver)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#post-pickup-session-driver)

Driver action. Transitions session: started → picked_up and keeps one pickup button for the route session.

Read `proof_requirements.pickup` from the session response. Submit `photo_urls` when `photo_required=true` and `signature_url` when `signature_required=true`; `notes` is optional. If both flags are false, the request body may be empty and pickup remains one-click.

The shared proof is stored in `route_sessions.proof_of_pickup`, copied to `deliveries.proof_of_pickup` for every included delivery, and normalized photo/signature rows are created in `delivery_proofs`.

For lifecycle-managed P&D/operational-return members, the route action uses the same atomic warehouse Pickup owner: existing preparation and current assignment/handoff are required; stock leaves the warehouse, a `pick` movement is recorded, custody becomes `in_transit`, and delivery/assignment/handoff/picking statuses advance together. Ordinary members keep the established route transition. A session with no eligible deliveries still preserves the existing session pickup behavior. Pickup conflicts return HTTP 409 with the same localized stable code as individual Pickup, including `pickup_picking_order_required`, `pickup_handoff_required`, `delivery_has_open_exception`, `warehouse_item_exception_open`, and `pickup_stock_unavailable`; the complete session transaction remains unchanged.

### Payment behavior

each zero-fee delivery is marked paid without a payment transaction. A sender-paid cash delivery records its fee-only pickup collection and delivery-fee payment exactly once. Retrying after the session reached `picked_up` repairs any missing payment side effect without changing the session again.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/route-sessions/:sessionId/pickup

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sessionId | path string | Yes |  | session-uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| signature_url | string |  |  |  |
| photo_urls | array<string> |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "signature_url": "https://example.com/signature.png",
  "photo_urls": [
    "https://example.com/pickup.jpg"
  ],
  "notes": "Shared route pickup proof"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route session picked up successfully",
  "data": {
    "id": "session-uuid",
    "status": "picked_up",
    "picked_up_at": "2026-04-14T08:20:00Z",
    "proof_requirements": {
      "pickup": {
        "photo_required": true,
        "signature_required": false
      },
      "delivery": {
        "photo_required": true,
        "signature_required": true
      }
    },
    "proof_of_pickup": {
      "signature": null,
      "photos": [
        "https://example.com/pickup.jpg"
      ],
      "notes": "Shared route pickup proof"
    },
    "proof_of_delivery": null
  }
}
```

### Response 409 Required pickup photo missing

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Photo URLs are required",
  "data": null
}
```

### Response 409 Session deliveries changed

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "session_deliveries_changed",
  "message": "Route session deliveries changed; refresh and try again",
  "data": null
}
```

### Response 409 Warehouse item exception blocks pickup

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "warehouse_item_exception_open",
  "message": "Resolve the open warehouse item exception before continuing",
  "data": null
}
```

## POST /delivery/route-sessions/:sessionId/deliver — Deliver Session (Driver)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#post-deliver-session-driver)

Driver action. Transitions outstanding session deliveries to delivered with one shared route-session proof capture.

Read `proof_requirements.delivery` from the session response. Submit `photo_urls` when `photo_required=true` and `signature_url` when `signature_required=true`; `notes` is optional. If both flags are false, the request body may be empty.

The shared proof is stored in `route_sessions.proof_of_delivery`, copied to `deliveries.proof_of_delivery` for every included delivery, and normalized photo/signature rows are created in `delivery_proofs`.

### Payment behavior

receiver cash combines goods COD and delivery fee in one physical dropoff collection; receiver wallet charges the receiver at completion; sender wallet was settled at creation; sender cash was settled at pickup; zero-fee deliveries are paid automatically. COD audit/payout and one driver earning per delivered child are idempotent. Retrying a completed session repairs missing payment or earning side effects without another charge.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/route-sessions/:sessionId/deliver

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sessionId | path string | Yes |  | session-uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| signature_url | string |  |  |  |
| photo_urls | array<string> |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "signature_url": "https://example.com/signature.png",
  "photo_urls": [
    "https://example.com/delivery.jpg"
  ],
  "notes": "Shared route delivery proof"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route session delivered successfully",
  "data": {
    "id": "session-uuid",
    "status": "completed",
    "completed_at": "2026-04-14T10:00:00Z",
    "proof_requirements": {
      "pickup": {
        "photo_required": true,
        "signature_required": false
      },
      "delivery": {
        "photo_required": true,
        "signature_required": true
      }
    },
    "proof_of_pickup": {
      "signature": null,
      "photos": [
        "https://example.com/pickup.jpg"
      ]
    },
    "proof_of_delivery": {
      "signature": "https://example.com/signature.png",
      "photos": [
        "https://example.com/delivery.jpg"
      ],
      "notes": "Shared route delivery proof"
    }
  }
}
```

### Response 409 Required delivery signature missing

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Signature URL is required",
  "data": null
}
```

### Response 409 Session deliveries changed

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "session_deliveries_changed",
  "message": "Route session deliveries changed; refresh and try again",
  "data": null
}
```

## GET /delivery/routes/:routeId/eligible-deliveries — List Eligible Deliveries for Route

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#get-list-eligible-deliveries-for-route)

Returns paginated deliveries eligible for route assignment.

### Eligibility criteria
- status is `pending` and `carrier_type` is `internal`.
- `trip_id`, `waybill_id`, `route_id`, and `route_session_id` are all null.
- P&D parent orders are excluded; only operational child legs can appear.

### Search behavior
- `search` performs a case-insensitive partial match against `delivery_code` and `external_reference_id`.
- PostgREST search control characters are escaped, so `%`, `_`, backslashes, quotes, commas, and parentheses are treated as literal text.

### Frontend integration contract

use this endpoint for the route picker, but treat assignment-time HTTP 409 results as authoritative because eligibility is revalidated atomically. No navigation action is returned.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/routes/:routeId/eligible-deliveries?page=1&limit=20&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| routeId | path string | Yes |  | route-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number | 1 |
| limit | query string |  | Items per page (max 100) | 20 |
| search | query string |  | Search by delivery_code or external_reference_id |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Eligible deliveries retrieved successfully",
  "data": {
    "deliveries": [
      {
        "id": "del-uuid",
        "delivery_code": 1865,
        "barcode": "BAR1865",
        "external_reference_id": "ORD-001",
        "status": "pending",
        "unit_size": 1,
        "pickup_location": {
          "address": "Erbil, 60m Street"
        },
        "delivery_location": {
          "address": "Sulaymaniyah, Salim St"
        },
        "customer": {
          "id": "cust-uuid",
          "full_name": "Ahmad Karimi",
          "phone": "+9647501111111"
        }
      }
    ],
    "total": 24,
    "page": 1,
    "limit": 20
  }
}
```

### Response 500 Eligible delivery query failed

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Failed to retrieve eligible deliveries",
  "data": null
}
```

## POST /delivery/route-sessions/:sessionId/cancel — Cancel Session (Dispatcher)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/routes#post-cancel-session-dispatcher)

Dispatcher action. Requires a non-empty `reason` of at most 500 characters. The cancelling actor is always resolved from the authenticated request and cannot be supplied by the client. Cancels an open or started session; picked_up, in_transit, completed, and already-cancelled sessions are rejected.

In one atomic transaction, the backend snapshots the cancellation actor, reason, timestamp, and session order grid fields; marks active assignments unassigned with the `route_session_released` audit context; releases non-terminal orders to pending with route/session links cleared; releases driver capacity; and marks the session cancelled. The marked release is permitted by the route ownership guard, while generic assignment unassign and driver-switch operations remain blocked. The historical snapshot survives later reassignment.

Blocked with `delivery_has_open_exception` when any non-terminal delivery in the session has an open exception.

### Frontend integration contract
- Require `reason` in the cancellation dialog and submit exactly `{ reason }`.
- Use `orders_count` and `released_orders_count` from session lists.
- Continue opening the existing session deliveries endpoint; render `session_membership_status=released` rows as history and disable selection/removal actions.
- Display `session.cancellation.cancelled_at`, `.reason`, and `.cancelled_by`. No new navigation action is returned.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/route-sessions/:sessionId/cancel

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sessionId | path string | Yes |  | session-uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "reason": "Driver unavailable"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route session cancelled successfully",
  "data": {
    "id": "session-uuid",
    "status": "cancelled",
    "cancelled_at": "2026-08-03T10:00:00Z",
    "orders_count": 4,
    "released_orders_count": 4,
    "cancellation": {
      "cancelled_at": "2026-08-03T10:00:00Z",
      "reason": "Driver unavailable",
      "cancelled_by": {
        "id": "user-uuid",
        "full_name": "Admin Name"
      }
    }
  }
}
```

### Response 400 Reason required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "reason_required",
  "message": "Reason is required",
  "data": null
}
```

### Response 409 Already picked up

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot cancel a session that has been picked up — driver already has the orders",
  "data": null
}
```

## POST /delivery/bulk/validate — Validate Bulk Action

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#post-validate-bulk-action)

Accepts 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. A `staged_at_warehouse` parent 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 be `staged_at_warehouse`.
- `mark_pending` — params: reason (optional). Allowed only for `assigned` or `en_route_to_pickup`; atomically unassigns the active driver and returns the active delivery to `pending`. Parent codes resolve to the active child leg. When that leg is inbound, its staged parent delivery also returns to `pending`; driver ownership remains child-only.
- `assign_driver` — params: driver_id (required). `assign_driver` respects `require_accept_or_reject_for_assignments`: when enabled, each assignment and resolved active delivery/leg remain `pending` until the driver accepts; when disabled, both become `assigned`. Each delivery is also checked against the driver's allowed zones at execute time; deliveries outside the driver's zones fail with `driver_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 by `sequence_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 either `pending` acceptance or `assigned`; 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 accepts `pending` acceptance, `assigned`, or `en_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) and `photo_urls` (optional); common `reason` and `notes` remain optional. Direct orders must be `picked_up` or `in_transit` with 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 shared `bin_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 is `exception`, its exception is open with custody `awaiting_warehouse_receipt`, and its linked `failed_delivery_return` receiving order is `pending` or `in_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 with `duplicate_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` — common `reason` and `notes` are optional
- `mark_returned` — params: none
- `reassign_driver` — params: driver_id (required); common `reason` and `notes` are optional. Same zone check as `assign_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-`pending` behavior as `mark_pending`; `pending`, `picked_up`, `in_transit`, and later states are rejected.
- `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. 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 `open` delivery exception fails validation with `delivery_has_open_exception` and cannot be moved by bulk actions until the exception is resolved, except that `mark_staged_at_warehouse` may perform the exact linked physical receipt while custody is `awaiting_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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk/validate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_codes | array<string> |  |  |  |
| action_id | string |  |  |  |
| params | object |  |  |  |



### Request body (raw)

```json
{
  "order_codes": [
    "1865",
    "1853"
  ],
  "action_id": "execute_draft",
  "params": {}
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Validation completed",
  "data": {
    "results": [
      {
        "order_code": "1865",
        "valid": true
      },
      {
        "order_code": "1853",
        "valid": false,
        "error_message": "Order status does not allow this action"
      }
    ],
    "total_valid": 1,
    "total_failed": 1
  }
}
```

## POST /delivery/bulk/execute — Execute Bulk Action

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#post-execute-bulk-action)

### Uses 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_assignment` until the delivery is reassigned. `mark_en_route_to_pickup` and `mark_picked_up` may 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_up` uses 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 the `pick` movement, clears stock/reservation, sets custody `in_transit`, completes the handoff, marks picking `handed_off`, and advances delivery/assignment together. It never creates missing preparation. Waybill and ordinary rows keep their existing bulk behavior.
- `mark_delivered` executes 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_delivered` validates the active child assignment but executes the submitted parent code. The backend completes every incomplete non-terminal leg and sets the parent to `delivered`; an already delivered inbound leg remains delivered. A draft/missing active leg returns `no_active_child_leg`, an active leg outside `picked_up` and `in_transit` returns `bulk_invalid_status_for_action`, and a genuinely unassigned active leg returns `bulk_no_active_assignment`.

**Driver-assignment actions** (`assign_driver`, `reassign_driver`, and `mark_staged_at_warehouse` when creating new assignments):
- For `assign_driver` and `reassign_driver`, a staged/P&D parent order code is accepted as dispatcher input but is resolved to the first active non-terminal child leg by `sequence_plan` before validation or writes. The parent order code remains in the response result, but assignment rows are inserted for the resolved child delivery only. Bulk `assign_driver` uses the same tenant acceptance setting as single assignment: acceptance required keeps the child assignment/delivery and parent projection `pending`; otherwise the child and parent projection become `assigned`.
- If the active child leg is still `draft`, missing, or all child legs are terminal, the parent order result fails with `no_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_driver` also 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_id` so the per-row realtime trigger is suppressed; the API emits one consolidated realtime row and one `bulk_delivery_assigned` notification.

### Unassign and mark-pending actions
- `pending`, `assigned`, and `en_route_to_pickup` are 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 to `pending`; sibling legs remain unchanged.
- An outbound child unassign does not reset a parent already at warehouse staging.

### Warehouse staging action
- `mark_staged_at_warehouse` processes 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 active `default_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_warehouse` with actor audit context.
- When `params.driver_id` is omitted and an inbound child is unassigned, a parent created from a Transfer Manifest (both source IDs present) uses `assignment_change_settings.default_driver_id` only when `transfer_manifest.allow_warehouse_staging_without_assigned_driver=true`. Normal/non-G2G rows and toggle-Off rows keep `driver_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 `draft` unless the existing tenant setting `auto_execute_delivery_order_on_receive` activates 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 to `staged_at_warehouse`, releases the old assignment/capacity, stages its parent when present, and leaves the delivery and exception in `exception`/`open` for the next dispatcher decision. Failures are row-isolated, so eligible parcels still complete. Important row-level errors include `not_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`, and `staging_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_route` accepts exactly one mode: `params.session_id` with an optional matching `params.route_id` for an existing session, or `params.route_id` and `params.driver_id` together 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 returns `data.assignment_outcome`, `data.requested_session`, `data.session`, `data.fallback_reason`, and `data.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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk/execute

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_codes | array<string> |  |  |  |
| action_id | string |  |  |  |
| params | object |  |  |  |



### Request body (raw)

```json
{
  "order_codes": [
    "1865",
    "1853"
  ],
  "action_id": "execute_draft",
  "params": {}
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk operation completed",
  "data": {
    "results": [
      {
        "order_code": "1865",
        "success": true
      },
      {
        "order_code": "1853",
        "success": false,
        "error_message": "Order status does not allow this action"
      }
    ],
    "total_success": 1,
    "total_failed": 1
  }
}
```

### Response 200 Success - P&D Inbound Package Returned Before Staging

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk operation completed",
  "data": {
    "results": [
      {
        "order_code": "3349",
        "success": true
      }
    ],
    "total_success": 1,
    "total_failed": 0
  }
}
```

### Response 200 Success - P&D Draft Prepared

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk operation completed",
  "data": {
    "results": [
      {
        "order_code": "1865",
        "success": true
      }
    ],
    "total_success": 1,
    "total_failed": 0
  }
}
```

### Response 200 Assign to existing route session

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk operation completed",
  "data": {
    "results": [
      {
        "order_code": "3267",
        "success": true
      }
    ],
    "total_success": 1,
    "total_failed": 0,
    "assignment_outcome": "existing_session",
    "requested_session": {
      "id": "session-uuid",
      "session_number": 1,
      "status": "open"
    },
    "session": {
      "id": "session-uuid",
      "session_number": 1,
      "status": "open",
      "route_id": "route-uuid",
      "driver_id": "driver-uuid"
    },
    "fallback_reason": null,
    "assigned_count": 1
  }
}
```

### Response 200 Partial — open exception blocks order

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk operation completed",
  "data": {
    "results": [
      {
        "order_code": "1865",
        "success": true
      },
      {
        "order_code": "1853",
        "success": false,
        "error_message": "delivery_has_open_exception"
      }
    ],
    "total_success": 1,
    "total_failed": 1
  }
}
```

### Response 200 Partial — driver outside zone

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk operation completed",
  "data": {
    "results": [
      {
        "order_code": "1865",
        "success": true
      },
      {
        "order_code": "1853",
        "success": false,
        "error_message": "Driver is not allowed to operate in the pickup zone"
      }
    ],
    "total_success": 1,
    "total_failed": 1
  }
}
```

### Response 200 Wallet Error Contract - Bulk Draft Execution

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Bulk operation completed",
  "data": {
    "results": [
      {
        "order_code": "1853",
        "success": false,
        "code": "insufficient_available_balance",
        "error_message": "Customer has reached the Minimum Wallet Balance limit."
      }
    ],
    "total_success": 0,
    "total_failed": 1
  }
}
```

## POST /delivery/bulk/validate — Validate Bulk Exception Resolution

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#post-validate-bulk-exception-resolution)

Builds 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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk/validate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_codes | array<string> |  |  |  |
| action_id | string |  |  |  |
| params | object |  |  |  |



### Request body (raw)

```json
{
  "order_codes": [
    "1865",
    "DEL1853"
  ],
  "action_id": "resolve_exception",
  "params": {}
}
```

### Response 200 Success - Common Retry Outcome

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Validation completed",
  "data": {
    "results": [
      {
        "order_code": "1865",
        "valid": true,
        "exception_type": "refused",
        "actions": [
          {
            "key": "retry",
            "label": "Retry delivery",
            "description": "Try to deliver the parcel again",
            "override_reason_required": false
          }
        ]
      },
      {
        "order_code": "1853",
        "valid": true,
        "exception_type": "not_home",
        "actions": [
          {
            "key": "retry",
            "label": "Retry delivery",
            "description": "Try to deliver the parcel again",
            "override_reason_required": true
          }
        ],
        "attempt": {
          "number": 2,
          "allowed": 2,
          "override_reason_required": true
        }
      }
    ],
    "available_actions": [
      {
        "key": "retry",
        "label": "Retry delivery",
        "description": "Try to deliver the parcel again",
        "override_reason_required": true
      }
    ],
    "total_valid": 2,
    "total_failed": 0
  }
}
```

## POST /delivery/bulk/execute — Execute Bulk Exception Resolution

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#post-execute-bulk-exception-resolution)

Applies 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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk/execute

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| 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 |  |  |  |



### Request body (raw)

```json
{
  "order_codes": [
    "1865",
    "DEL1853"
  ],
  "action_id": "resolve_exception",
  "params": {
    "action_key": "retry",
    "notes": "Customers confirmed availability",
    "override_reason": "Dispatcher approved another attempt",
    "resolution_details": {
      "decision_note": "Resolved from bulk operations"
    }
  }
}
```

### Response 200 Partial Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk operation completed",
  "data": {
    "action_key": "retry",
    "results": [
      {
        "order_code": "1865",
        "success": true,
        "action_key": "retry"
      },
      {
        "order_code": "1853",
        "success": false,
        "code": "delivery_has_no_open_exception",
        "error_message": "This delivery has no open exception"
      }
    ],
    "total_success": 1,
    "total_failed": 1
  }
}
```

## POST /delivery/bulk/execute — Bulk Cancel Orders

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#post-bulk-cancel-orders)

Cancels 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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk/execute

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_codes | array<string> |  |  |  |
| action_id | string |  |  |  |
| params | object |  |  |  |
| params.reason | string |  |  |  |



### Request body (raw)

```json
{
  "order_codes": [
    "1865",
    "1853"
  ],
  "action_id": "cancel_orders",
  "params": {
    "reason": "Cancelled by dispatcher"
  }
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk operation completed",
  "data": {
    "results": [
      {
        "order_code": "1865",
        "success": true
      },
      {
        "order_code": "1853",
        "success": true
      }
    ],
    "total_success": 2,
    "total_failed": 0
  }
}
```

## GET /delivery/orders/search — Search Orders

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#get-search-orders)

Compact 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 + 1` rows to calculate `has_more`; it does not run an exact count.
- `next_page` is the next page number when more rows exist, otherwise `null`.

### Money contract
- `amount_to_collect` is the outstanding physical amount for the logical order. It equals positive `cod_amount` plus a positive delivery fee only when the delivery-fee `payment_method` is `cash`/`cod` and `payment_status` is not `paid`.
- COD is independent from the delivery-fee payment method. `payment_status=refunded` means 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 returns `null`.

### 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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/orders/search?q=1865&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| q | query string | No | 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. | 1865 |
| page | query string |  | Page number. Defaults to 1. | 1 |
| limit | query string |  | Rows per page. Defaults to 20 and is capped at 100. | 20 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Orders retrieved successfully",
  "data": {
    "results": [
      {
        "id": "parent-or-standalone-uuid",
        "delivery_code": "1865",
        "parent_delivery_id": null,
        "is_parent": true,
        "external_reference_id": "EXT-1001",
        "barcode": "DEL1865",
        "customer": {
          "id": "customer-uuid",
          "full_name": "John Doe",
          "phone": "+9647000000000"
        },
        "customer_name": "John Doe",
        "pickup_location": {
          "full_address": "Warehouse A"
        },
        "delivery_location": {
          "full_address": "123 Main St"
        },
        "address": "123 Main St",
        "delivery_services": {
          "id": "service-uuid",
          "name": "Same Day",
          "delivery_type": {
            "id": "type-uuid",
            "name": "Parcel"
          }
        },
        "status": "pending",
        "unit_size": 1,
        "cod_amount": 10000,
        "payment_method": "cash",
        "payment_status": "pending",
        "amount_to_collect": 12000,
        "delivery_type": "parcel",
        "direction": null,
        "requires_warehouse_staging": true,
        "driver": {
          "id": "driver-uuid",
          "full_name": "Driver Name"
        },
        "priority": 1,
        "has_open_exception": false,
        "created_at": "2026-08-03T07:30:00.000Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "has_more": true,
      "next_page": 2
    }
  }
}
```

## GET /delivery/bulk/print — Bulk Print — Shipping Labels

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#get-bulk-print-shipping-labels)

Returns shipment_label_url from each delivery record. If not generated yet, url is null with a message. Frontend handles opening/printing.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/bulk/print?order_codes[]=1865&order_codes[]=1853&type=shipping_label

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_codes[] | query string |  | Order code | 1865 |
| order_codes[] | query string |  | Order code | 1853 |
| type | query string |  | shipping_label \| receipt | shipping_label |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Print data retrieved successfully",
  "data": {
    "type": "shipping_label",
    "results": [
      {
        "order_code": "1865",
        "url": "https://label-url...",
        "status": "generated"
      },
      {
        "order_code": "1853",
        "url": null,
        "status": "pending",
        "message": "label_not_generated"
      }
    ]
  }
}
```

## POST /delivery/bulk/shipment-labels — Generate Bulk Shipment Labels

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#post-generate-bulk-shipment-labels)

Generates 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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk/shipment-labels

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_codes | array<string> |  |  |  |
| print_source | string |  |  |  |



### Request body (raw)

```json
{
  "delivery_codes": [
    "2501",
    "DEL2502",
    "DEL2501"
  ],
  "print_source": "bulk"
}
```

### Response 200 Success — combined PDF

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Shipment labels generated successfully",
  "data": {
    "url": "https://.../storage/v1/object/sign/shipment-label-pdfs/bulk-shipment-labels-v2/tenant-uuid/cache-key.pdf?token=...",
    "delivery_count": 2,
    "cached": false,
    "expires_in": 900
  }
}
```

### Response 200 Success - cache hit

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Shipment labels generated successfully",
  "data": {
    "url": "https://.../storage/v1/object/sign/shipment-label-pdfs/bulk-shipment-labels-v2/tenant-uuid/cache-key.pdf?token=...",
    "delivery_count": 2,
    "cached": true,
    "expires_in": 900
  }
}
```

### Response 200 Success — 200-label combined PDF

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Shipment labels generated successfully",
  "data": {
    "url": "https://.../storage/v1/object/sign/shipment-label-pdfs/bulk-shipment-labels-v2/tenant-uuid/cache-key.pdf?token=...",
    "delivery_count": 200,
    "cached": false,
    "expires_in": 900
  }
}
```

### Response 400 Batch limit exceeded

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Batch limit exceeded. Maximum 200 deliveries per request.",
  "data": {
    "batch_limit": 200
  }
}
```

### Response 404 Delivery not found or outside tenant

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Deliveries were not found for these codes: DEL9999",
  "data": {
    "code": "delivery_not_found",
    "delivery_codes": [
      "DEL9999"
    ]
  }
}
```

### Response 413 Source byte limit exceeded

Payload Too Large

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Shipment label generation failed",
  "data": {
    "code": "source_label_too_large",
    "delivery_id": "11111111-1111-4111-8111-111111111111",
    "max_bytes": 2097152
  }
}
```

### Response 502 Canonical label unavailable

Bad Gateway

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Shipment label generation failed",
  "data": {
    "code": "label_generation_failed",
    "delivery_id": "11111111-1111-4111-8111-111111111111"
  }
}
```

### Response 503 Label generation temporarily unavailable

Service Unavailable

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Retry-After | header string |  |  | 45 |



```json
{
  "success": false,
  "message": "Shipment label generation is temporarily unavailable. Please try again shortly.",
  "data": {
    "code": "label_generation_rate_limited",
    "retry_after_ms": 44968,
    "retry_after_seconds": 45
  }
}
```

### Response 200 Validate mixed codes (validate_only=true)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Validation completed",
  "data": {
    "results": [
      {
        "input_code": "GB100020000036",
        "valid": true,
        "delivery_id": "delivery-uuid",
        "delivery_code": "2635"
      },
      {
        "input_code": "2635",
        "valid": true,
        "delivery_id": "delivery-uuid",
        "delivery_code": "2635"
      },
      {
        "input_code": "GB100020000037",
        "valid": false,
        "code": "g2g_barcode_has_no_order",
        "message": "This G2G item does not have a delivery order yet."
      }
    ],
    "delivery_count": 1
  }
}
```

## POST /delivery/bulk/return-shipment-labels — Print Return Shipment Labels

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#post-print-return-shipment-labels)

Resolves 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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk/return-shipment-labels

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| original_delivery_codes | array<string> |  |  |  |



### Request body (raw)

```json
{
  "original_delivery_codes": [
    "2501",
    "2502"
  ]
}
```

### Response 200 Success - eligible returns printed with skipped feedback

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Return shipment labels generated successfully",
  "data": {
    "url": "https://.../combined-return-labels.pdf",
    "delivery_count": 1,
    "cached": false,
    "expires_in": 900,
    "results": [
      {
        "original_delivery_code": "2501",
        "return_delivery_id": "return-delivery-uuid",
        "return_delivery_code": "2601",
        "return_barcode": "DEL2601",
        "success": true,
        "status": "printed"
      },
      {
        "original_delivery_code": "2502",
        "return_delivery_id": null,
        "return_delivery_code": null,
        "return_barcode": null,
        "success": false,
        "status": "skipped",
        "code": "return_order_not_created",
        "message": "No linked return order has been created for this order"
      }
    ],
    "total_printed": 1,
    "total_skipped": 1
  }
}
```

### Response 422 No eligible linked return orders

Unprocessable Entity

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "no_eligible_return_orders",
  "message": "None of the selected orders has an eligible linked return label",
  "data": {
    "results": [
      {
        "original_delivery_code": "2502",
        "return_delivery_id": null,
        "return_delivery_code": null,
        "return_barcode": null,
        "success": false,
        "status": "skipped",
        "code": "return_order_not_created",
        "message": "No linked return order has been created for this order"
      }
    ],
    "total_printed": 0,
    "total_skipped": 1
  }
}
```

### Response 502 Eligible return label batch blocked

Bad Gateway

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "label_generation_failed",
  "message": "Failed to generate shipment labels for the requested deliveries.",
  "data": {
    "delivery_id": "return-delivery-uuid",
    "results": [
      {
        "original_delivery_code": "2501",
        "return_delivery_id": "return-delivery-uuid",
        "return_delivery_code": "2601",
        "return_barcode": "DEL2601",
        "success": false,
        "status": "blocked",
        "code": "label_generation_failed",
        "message": "No return labels were printed because the label batch could not be completed"
      }
    ],
    "total_printed": 0,
    "total_blocked": 1,
    "total_skipped": 0
  }
}
```

## GET /delivery/bulk/print — Bulk Print — Receipts

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-operations#get-bulk-print-receipts)

Creates 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.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/bulk/print?order_codes[]=1865&order_codes[]=DEL1853&type=receipt

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_codes[] | query string |  | Order code | 1865 |
| order_codes[] | query string |  | Bare delivery code or scanned DEL-prefixed barcode | DEL1853 |
| type | query string |  | shipping_label \| receipt | receipt |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Print data retrieved successfully",
  "data": {
    "type": "receipt",
    "url": "https://example.supabase.co/storage/v1/object/sign/shipment-label-pdfs/bulk-payment-receipts/tenant-uuid/cache-key.pdf?token=...",
    "delivery_count": 2,
    "cached": false,
    "expires_in": 3600,
    "results": [
      {
        "order_code": "1865",
        "url": "https://example.supabase.co/storage/v1/object/sign/documents/delivery-receipts/payment-1.pdf?token=...",
        "receipt_generated_at": "2026-09-06T10:00:00Z"
      },
      {
        "order_code": "1853",
        "url": "https://example.supabase.co/storage/v1/object/sign/documents/delivery-receipts/payment-2.pdf?token=...",
        "receipt_generated_at": "2026-09-06T10:01:00Z"
      }
    ]
  }
}
```

### Response 404 Unknown delivery code

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_not_found",
  "message": "Deliveries were not found for these codes: DEL9999",
  "data": {
    "code": "delivery_not_found",
    "delivery_codes": [
      "DEL9999"
    ]
  }
}
```

### Response 409 Receipt not ready

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "receipt_not_ready",
  "message": "Payment receipt is not ready yet",
  "data": {
    "code": "receipt_not_ready",
    "delivery_codes": [
      "1865"
    ]
  }
}
```

## POST /delivery/driver/bulk-pick/validate — Validate Driver Bulk Pick

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/driver-bulk-pick#post-validate-driver-bulk-pick)

Driver-only, read-only validation for 1–200 shipment labels. Driver identity is resolved from the authenticated user; `driver_id` is never accepted. Labels are normalized using the existing `DEL<delivery_code>` convention while input order and duplicate occurrences are preserved. A staged parent label resolves to its first active non-terminal child. When an original or P&D label has no active forward leg and its persisted `return_delivery_id` identifies an active linked return, the scanned label resolves directly to that return. The failed forward delivery is not returned as a candidate. All normal assignment, service, team, vehicle, zone, exception, Waybill, handoff, proof and capacity validation then runs against the return. If it is ineligible, the row returns that return order's actual rejection reason.

### Self-assign behavior
- When tenant `driver_config.driver_can_self_assign` is true, an unassigned Pending internal delivery may be claimed after manual-assignment policy, team, primary vehicle, service-vehicle, zone and cumulative-capacity checks.
- When false, only an active assignment owned by the authenticated driver is eligible.
- Another driver's assignment always returns `assigned_to_another_driver`. Pending acceptance returns `assignment_pending_acceptance`.

### Pickup behavior
- Eligible existing assignment statuses are `assigned`, `accepted`, and `en_route_to_pickup`. An order already at `picked_up` is rejected with `already_picked_up` and cannot be scanned for pickup again.
- Open exceptions, non-internal carriers, active Waybills (`drafted`, `sealed`, `assigned`, `picked_up`, `in_transit`, `awaiting_receipt`), missing required handoff, and non-pickable states are rejected per row.
- A pending relational P&D handoff is valid preparation: the execute call completes it atomically with stock exit and custody transfer. It must not be completed through the warehouse handoff API first.
- V1 does not collect pickup proof. When the service policy requires pickup photo or signature, the row returns `pickup_proof_required` and must use the existing single-order flow.
- Capacity is allocated deterministically in scan order only to new self-assignments; already assigned orders consume no new units.
- If the original and direct return labels are both submitted, both resolve to the same delivery ID. The first occurrence is considered and the later occurrence returns `duplicate_delivery`, so the return is never counted or picked twice.

Important row reasons include `unknown_barcode`, `duplicate_scan`, `duplicate_delivery`, `already_picked_up`, `delivery_not_ready_for_pickup`, `delivery_has_open_exception`, `delivery_in_active_waybill`, `assigned_to_another_driver`, `assignment_pending_acceptance`, `driver_self_assign_disabled`, `insufficient_vehicle_capacity`, `pickup_proof_required`, and `handoff_required_from_warehouse`. Every `message` is localized; branch on `reason`, not message text.

### Frontend integration contract

keep sending the physically scanned labels in the full accumulated `barcodes` array after each new local scan. For a linked-return resolution, keep `results[].barcode` as the scanned original label and use `results[].resolved_delivery_id` plus `results[].delivery` as authoritative; `delivery.id`, `delivery.delivery_code`, `delivery.barcode`, and `delivery.delivery_flow` describe the return. Merge rows by normalized scanned `barcode` and array position so duplicates remain visible. Use `counts.scanned`, `.valid`, `.rejected` and `capacity`. Enable review only when at least one row has `valid=true`. HTTP 200 may contain zero valid rows; 400 is malformed input, 403 is role/driver-state failure, and 500 is a global failure.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/driver/bulk-pick/validate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcodes | array<string> |  |  |  |



### Request body (raw)

```json
{
  "barcodes": [
    "DEL1865",
    "DEL1866"
  ]
}
```

### Response 200 Mixed validation

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk pickup labels validated",
  "results": [
    {
      "barcode": "DEL2863",
      "valid": true,
      "reason": "bulk_pick_valid_scan",
      "message": "Return order 2865 is ready for bulk pickup",
      "resolved_delivery_id": "RETURN-2865-ID",
      "delivery": {
        "id": "RETURN-2865-ID",
        "delivery_code": "2865",
        "barcode": "DEL2865",
        "status": "pending",
        "delivery_flow": "operational_return",
        "unit_size": 1,
        "pickup_summary": "Jaipur Warehouse",
        "dropoff_summary": "Return destination"
      }
    },
    {
      "barcode": "DEL1866",
      "valid": false,
      "reason": "assigned_to_another_driver",
      "message": "This order is assigned to another driver"
    }
  ],
  "counts": {
    "scanned": 2,
    "valid": 1,
    "rejected": 1
  },
  "capacity": {
    "requested_new_units": 1,
    "accepted_new_units": 1,
    "available_units": 20,
    "exceeded": false
  }
}
```

### Response 400 Too many labels

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "bulk_pick_barcode_limit_exceeded",
  "message": "A bulk pick can contain at most 200 labels"
}
```

## POST /delivery/driver/bulk-pick/execute — Execute Driver Bulk Pick

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/driver-bulk-pick#post-execute-driver-bulk-pick)

Revalidates every physical barcode, including linked-return resolution, then sends eligible resolved deliveries through one service-role-only atomic database operation. An original label with no active forward leg is resolved again to its active linked return at execution time; only that return delivery ID is sent to the atomic pickup owner. If both original and return labels were submitted, the duplicate resolved ID is never executed twice. The RPC locks assignments, deliveries and the driver in stable order; rechecks ownership, status, exceptions, active Waybills, handoff and capacity; creates allowed self-assignments with one `bulk_assignment_id`; records the normal assigned/start/picked-up events and timestamps; and returns ordered partial results. A business conflict affects only that row.

For lifecycle-managed P&D and operational-return parcels, Pickup is the warehouse custody boundary. The same transaction requires the completed picking order, current assignment and current pending handoff; rechecks delivery and warehouse-item exceptions; writes one `pick` inventory movement; clears warehouse quantity/reservation; marks item custody `in_transit`; changes the picking order to `handed_off`; completes the handoff; and marks the delivery/assignment `picked_up`. Waybill and ordinary delivery pickup retain their existing behavior.

For successful database rows, the Edge Function uses the existing idempotent sender-cash pickup accounting and delivery-status notification services. Payment work runs with bounded concurrency. If an order reaches `picked_up` before or during execution, that row fails with `already_picked_up`; pickup transitions and side effects are not repeated.

### Response rules
- HTTP 200 represents completion of the batch request and may contain mixed success/failure rows.
- `counts.picked` contains newly completed pickups; `counts.failed` contains rejected validation, execution conflicts, already-picked orders, or side-effect failures.
- `bulk_assignment_id` is present only when at least one new self-assignment was created.
- A state race returns row reason `bulk_pick_state_changed`; missing warehouse preparation returns `delivery_not_ready_for_pickup`; a missing current handoff returns `handoff_required_from_warehouse`; and other rows continue.

### Frontend integration contract

submit only the currently valid physical barcodes once and disable Confirm while this request is pending. Do not replace an original scanned barcode with the return barcode before Execute; the backend resolves it again. Display each returned row using localized `message` and branch on stable `reason`. Keep failed rows available for review. Treat `already_picked_up` as a terminal rejection and do not offer it for pickup retry. After the response, navigate through the existing Orders flow; no later delivery API or status behavior changes.

### Event actor contract

assignment and delivery status events created by this driver action use the validated `drivers.id` as `actor_id`. The backend retains the authenticated user ID for authorization and non-event ownership fields. Clients send barcodes only, not an actor ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/driver/bulk-pick/execute

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcodes | array<string> |  |  |  |



### Request body (raw)

```json
{
  "barcodes": [
    "DEL1865",
    "DEL1866"
  ]
}
```

### Response 200 Partial success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk pickup completed",
  "results": [
    {
      "barcode": "DEL1865",
      "success": true,
      "reason": "bulk_pick_completed",
      "message": "Order picked up successfully",
      "delivery": {
        "id": "{{delivery_id}}",
        "delivery_code": "1865",
        "status": "picked_up",
        "assignment_id": "{{assignment_id}}"
      }
    },
    {
      "barcode": "DEL1866",
      "success": false,
      "reason": "already_picked_up",
      "message": "This order has already been picked up"
    }
  ],
  "counts": {
    "picked": 1,
    "failed": 1
  },
  "bulk_assignment_id": "{{bulk_assignment_id}}"
}
```

## POST /delivery/bulk-order-creations/upload-url — Request Signed Upload URL

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#post-request-signed-upload-url)

Step 1 of the bulk import flow. Returns a one-time signed PUT URL the client uploads the file to. The returned `file_key` must be passed back to Create Bulk Order Creation Job.

### Validation
- Unsupported extension → 400 `unsupported_file_extension`
- File too large → 413 `file_too_large` (limit returned in response)

### Permissions

`manage:operations:tenant` or system admin.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk-order-creations/upload-url?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| file_name | string | Yes | original file name; extension must be `csv`, `xls`, or `xlsx`. |  |
| file_size_bytes | number | No | if provided, must be ≤ 15 MB (15728640 bytes). |  |



### Request body (raw)

```json
{
  "file_name": "orders_batch_a.csv",
  "file_size_bytes": 524288
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "file_key": "imports/<tenant_id>/<uuid>/original.csv",
  "file_kind": "csv",
  "upload_url": "https://<project>.supabase.co/storage/v1/upload/sign/bulk-delivery-imports/...",
  "token": "eyJhbGciOi...",
  "expires_at": "2026-04-30T13:30:00.000Z"
}
```

### Response 400 Error - Unsupported Extension

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "unsupported_file_extension",
  "allowed": [
    "csv",
    "xls",
    "xlsx"
  ]
}
```

### Response 413 Error - File Too Large

Payload Too Large

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "file_too_large",
  "limit": 15728640
}
```

## PUT {{upload_url}} — Upload File To Signed URL (Client Side)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#put-upload-file-to-signed-url-client-side)

Step 2 of the bulk import flow. Client PUTs the file bytes directly to the signed URL returned by Request Signed Upload URL. No tenant headers — the URL is pre-authorized.

Use the matching `Content-Type`:
- `text/csv` for `.csv`
- `application/vnd.ms-excel` for `.xls`
- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` for `.xlsx`

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: PUT {{upload_url}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | text/csv |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| upload_url | path string | Yes | Variable used inside the request path. |  |



### Request body (file)

```text
/path/to/file
```

## POST /delivery/bulk-order-creations — Create Bulk Order Creation Job

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#post-create-bulk-order-creation-job)

Step 3 of the bulk import flow. Creates the job row, verifies the file at `file_key` exists in storage, then kicks off the background worker. Returns 202 with the job row.

### Logical fields (used as `mappings` values)
- Required: `pickup_full_address`, `pickup_latitude`, `pickup_longitude`, `pickup_city`, `pickup_country`, `delivery_full_address`, `delivery_latitude`, `delivery_longitude`, `delivery_city`, `delivery_country`, `delivery_contact_name`, `delivery_contact_phone`
- Optional: `external_reference_id`, `cod_amount`, `package_special_instructions`, `package_items` (CSV cell holds a JSON array string)

### Row behavior
- `unit_size` is calculated from usable `package_items` measurements. When measurements are absent, `general.unit_size.settings.missing_unit_size_fallback` is applied once per row using the job's preloaded effective setting.
- If `is_draft=true`, each created row has delivery `status=draft`; use Bulk Operations action `execute_draft` later to move selected draft orders to Pending.
- When tenant setting `delivery_external_reference.unique_per_customer=true` (default), an `external_reference_id` already used by the same customer root order is marked failed with `external_reference_id_already_present`; when false, duplicate values are accepted.

### Permissions

`manage:operations:tenant` or system admin.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk-order-creations?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string | No | human-friendly label for the import |  |
| source_view | string | Yes | `dispatcher` \| `admin` |  |
| customer_id | string | Yes | (required, UUID): tenant customer the deliveries are billed to / created for |  |
| delivery_service_id | string | Yes | (required, UUID): determines `delivery_type_id` server-side; do NOT send `delivery_type_id` |  |
| payer_type | string | Yes | `sender` \| `receiver` |  |
| payment_method | string | Yes | `wallet` \| `cod` \| `card` \| etc. |  |
| is_draft | boolean | No | (optional, default false): when true, all successfully imported deliveries are created as Draft. Payment processing follows normal delivery creation; driver release waits for Execute. |  |
| mappings | object | Yes | (required, object): maps your CSV header names → worker logical fields |  |
| mappings.Pickup Address | string |  |  |  |
| mappings.Pickup Lat | string |  |  |  |
| mappings.Pickup Lng | string |  |  |  |
| mappings.Pickup City | string |  |  |  |
| mappings.Pickup Country | string |  |  |  |
| mappings.Drop Address | string |  |  |  |
| mappings.Drop Lat | string |  |  |  |
| mappings.Drop Lng | string |  |  |  |
| mappings.Drop City | string |  |  |  |
| mappings.Drop Country | string |  |  |  |
| mappings.Recipient Name | string |  |  |  |
| mappings.Recipient Phone | string |  |  |  |
| mappings.Reference | string |  |  |  |
| mappings.COD | string |  |  |  |
| mappings.Notes | string |  |  |  |
| mappings.Items | string |  |  |  |
| total_records | number | No | (optional, int): client-side row count for UI; the worker reads the actual count from the file |  |
| file_key | string | Yes | exact value returned by `upload-url` — the server uses the full storage key as the idempotency token (same `file_key` returns the existing job) |  |
| file_kind | string | Yes | `csv` \| `xls` \| `xlsx` |  |
| file_size_bytes | number | No | (optional) |  |



### Request body (raw)

```json
{
  "name": "Q2 storefront orders – batch A",
  "source_view": "dispatcher",
  "customer_id": "uuid",
  "delivery_service_id": "uuid",
  "payer_type": "sender",
  "payment_method": "wallet",
  "is_draft": false,
  "mappings": {
    "Pickup Address": "pickup_full_address",
    "Pickup Lat": "pickup_latitude",
    "Pickup Lng": "pickup_longitude",
    "Pickup City": "pickup_city",
    "Pickup Country": "pickup_country",
    "Drop Address": "delivery_full_address",
    "Drop Lat": "delivery_latitude",
    "Drop Lng": "delivery_longitude",
    "Drop City": "delivery_city",
    "Drop Country": "delivery_country",
    "Recipient Name": "delivery_contact_name",
    "Recipient Phone": "delivery_contact_phone",
    "Reference": "external_reference_id",
    "COD": "cod_amount",
    "Notes": "package_special_instructions",
    "Items": "package_items"
  },
  "total_records": 50,
  "file_key": "imports/<tenant_id>/<uuid>/original.csv",
  "file_kind": "csv",
  "file_size_bytes": 524288
}
```

### Response 202 Success Response

Accepted

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "tenant_id": "uuid",
    "created_by": "uuid",
    "name": "Q2 storefront orders – batch A",
    "source_view": "dispatcher",
    "customer_id": "uuid",
    "delivery_service_id": "uuid",
    "payer_type": "sender",
    "payment_method": "wallet",
    "idempotency_key": "client-generated-uuid",
    "original_file_url": "imports/<tenant_id>/<uuid>/original.csv",
    "file_kind": "csv",
    "file_size_bytes": 524288,
    "status": "pending",
    "total_records": 50,
    "success_records": 0,
    "failed_records": 0,
    "progress": 0,
    "event_timeline": [
      {
        "event": "file_accepted",
        "at": "2026-04-30T13:00:00.000Z"
      },
      {
        "event": "mapping_received",
        "at": "2026-04-30T13:00:00.000Z"
      }
    ],
    "created_at": "2026-04-30T13:00:00.000Z",
    "is_draft": false
  }
}
```

### Response 200 Idempotent Replay

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "idempotent": true,
  "data": {
    "id": "uuid",
    "status": "processing"
  }
}
```

### Response 400 Error - Uploaded File Not Found

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "uploaded_file_not_found"
}
```

### Response 202 Row Failure - Duplicate External Reference

Accepted

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "status": "completed",
    "failed_order_logs": [
      {
        "row_number": 3,
        "error_field": "external_reference_id",
        "error_code": "external_reference_id_already_present"
      }
    ]
  }
}
```

### Response 202 Row Failure - Insufficient Wallet Balance

Accepted

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "status": "completed",
    "failed_order_logs": [
      {
        "row_number": 4,
        "error_field": null,
        "error_code": "insufficient_balance"
      }
    ]
  }
}
```

### Response 400 Error - File Key Tenant Mismatch

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "file_key_tenant_mismatch"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "insufficient_permissions"
}
```

## GET /delivery/bulk-order-creations — List Bulk Order Creations

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#get-list-bulk-order-creations)

Paginated list of bulk import jobs for the current tenant, newest first.

### Permissions

`manage:operations:tenant` or system admin.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/bulk-order-creations?page=1&limit=20&status=&search=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Items per page (max 200) | 20 |
| status | query string |  | Filter by status (pending \| processing \| completed \| failed) |  |
| search | query string |  | ilike match on name OR idempotency_key |  |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "name": "Q2 storefront orders – batch A",
      "status": "completed",
      "total_records": 50,
      "success_records": 48,
      "failed_records": 2,
      "progress": 100,
      "created_at": "2026-04-30T13:00:00.000Z",
      "completed_at": "2026-04-30T13:02:14.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "total_pages": 1
  }
}
```

## GET /delivery/bulk-order-creations/:jobId — Get Bulk Order Creation

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#get-get-bulk-order-creation)

Returns the full job row plus signed download URLs (`original_signed_url`, `success_signed_url`, `failed_signed_url`), the `event_timeline` array (milestones: `file_accepted`, `mapping_received`, `import_started`, `import_completed` / `import_failed`), and `failed_order_logs` (per-row diagnostics).

### Permissions

`manage:operations:tenant` or system admin.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/bulk-order-creations/:jobId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| jobId | path string | Yes | Bulk order creation job ID | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "tenant_id": "uuid",
    "name": "Q2 storefront orders – batch A",
    "status": "completed",
    "total_records": 50,
    "success_records": 48,
    "failed_records": 2,
    "progress": 100,
    "original_file_url": "imports/<tenant_id>/<job_id>/original.csv",
    "success_file_url": "imports/<tenant_id>/<job_id>/success.csv",
    "failed_file_url": "imports/<tenant_id>/<job_id>/failed.csv",
    "event_timeline": [
      {
        "event": "file_accepted",
        "at": "2026-04-30T13:00:00.000Z"
      },
      {
        "event": "mapping_received",
        "at": "2026-04-30T13:00:00.000Z"
      },
      {
        "event": "import_started",
        "at": "2026-04-30T13:00:05.000Z"
      },
      {
        "event": "import_completed",
        "success_records": 48,
        "failed_records": 2,
        "at": "2026-04-30T13:02:14.000Z"
      }
    ],
    "failed_order_logs": [
      {
        "row_number": 7,
        "error_field": "delivery_contact_phone",
        "error_code": "delivery_contact_phone_required"
      },
      {
        "row_number": 23,
        "error_field": "pickup_latitude",
        "error_code": "pickup_latitude_invalid"
      }
    ],
    "original_signed_url": "https://...signed...",
    "success_signed_url": "https://...signed...",
    "failed_signed_url": "https://...signed..."
  }
}
```

### Response 404 Error - Job Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "job_not_found"
}
```

## GET /delivery/bulk-order-creations/:jobId/success-csv — Download Success CSV

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#get-download-success-csv)

302 redirect to a signed URL for `success.csv`. Each CSV row carries the original CSV columns plus a `delivery_id` column. Returns 404 `csv_not_ready` if the worker has not yet generated the file.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/bulk-order-creations/:jobId/success-csv?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| jobId | path string | Yes | Bulk order creation job ID | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 302 Redirect To Signed URL

Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Location | header string |  |  | https://...signed-success-csv-url... |



## GET /delivery/bulk-order-creations/:jobId/failed-csv — Download Failed CSV

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#get-download-failed-csv)

302 redirect to a signed URL for `failed.csv`. Each CSV row carries the original CSV columns plus `row_number`, `error_field`, `error_code` columns. Returns 404 `csv_not_ready` if the worker has not yet generated the file.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/bulk-order-creations/:jobId/failed-csv?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| jobId | path string | Yes | Bulk order creation job ID | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 302 Redirect To Signed URL

Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Location | header string |  |  | https://...signed-failed-csv-url... |



## POST /delivery/bulk-order-creations/:jobId/_resume — Resume Job (Internal Cron)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#post-resume-job-internal-cron)

Internal cron-only hook invoked by the watchdog when a `processing` job's lease has expired. Re-triggers the worker, which re-runs all rows from the start; rows that already produced a delivery for the same bulk job are counted as success on recovery, while external-reference collisions with other deliveries remain row failures.

Internal backend/cron only. Send `apikey: {{supabase_secret_key}}` with the default managed Supabase secret key (`SUPABASE_SECRET_KEYS.default`). Never put this value in a frontend or mobile application. Missing, invalid, publishable, legacy anon, and legacy service-role bearer credentials return HTTP 401 before work is dispatched. `Authorization: Bearer ...` alone is not accepted. Frontend handoff: none; application clients must not call this internal endpoint. The unauthorized message is localized using the request language.

Authentication: Internal backend or cron only. Send the managed Supabase secret in apikey. Application clients must not call this endpoint.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk-order-creations/:jobId/_resume

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| apikey | header string |  | Default managed Supabase secret key; backend only. | {{supabase_secret_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| jobId | path string | Yes | Bulk order creation job ID | uuid |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "resumed": "uuid"
}
```

### Response 401 401 Missing or invalid internal secret

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## POST /delivery/bulk-order-creations/:jobId/abort — Abort Job

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#post-abort-job)

Marks a `pending` or `processing` job as `cancelled` and stops the worker from processing remaining rows. The job's `worker_lease_id` is cleared — any in-flight worker invocation will fail its next `refreshLease` call and exit cleanly.

Already-created deliveries are **kept** (not deleted automatically). Use `DELETE /:id/deliveries` if you want to remove them.

### Errors
- `job_not_found` 404 — unknown ID or wrong tenant
- `job_already_terminal` 409 — job status is already `completed`, `failed`, or `cancelled`
- `insufficient_permissions` 403

### Permissions

`manage:operations:tenant` or system admin.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/bulk-order-creations/:jobId/abort?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| jobId | path string | Yes | Bulk order creation job ID | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  |  | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "job_aborted",
  "data": {
    "id": "uuid",
    "status": "cancelled",
    "completed_at": "2026-05-19T12:00:00.000Z"
  }
}
```

### Response 409 Error - Already Terminal

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "job_already_terminal",
  "status": "completed"
}
```

### Response 404 Error - Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "job_not_found"
}
```

## DELETE /delivery/bulk-order-creations/:jobId/deliveries — Delete Job Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/bulk-delivery-order-creations#delete-delete-job-deliveries)

Soft-deletes all delivery records (`deleted_at` set to now) that were created by this bulk import job. Only allowed when the job is in a terminal state (`cancelled`, `completed`, or `failed`). If the job is still `pending` or `processing`, abort it first.

### Returns

`deleted_count` — number of deliveries that were soft-deleted.

### Errors
- `job_not_found` 404
- `job_still_running` 409 — job is still active; abort first
- `insufficient_permissions` 403

### Permissions

`manage:operations:tenant` or system admin.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/bulk-order-creations/:jobId/deliveries?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| jobId | path string | Yes | Bulk order creation job ID | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  |  | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "deliveries_deleted",
  "deleted_count": 13
}
```

### Response 409 Error - Job Still Running

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "job_still_running",
  "hint": "Abort the job first before deleting its deliveries."
}
```

### Response 404 Error - Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "job_not_found"
}
```

## GET /delivery/teams — List Teams

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#get-list-teams)

List all teams for the tenant with member counts.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/teams?page=1&limit=20&active=true&search=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number | 1 |
| limit | query string |  | Items per page (max 100) | 20 |
| active | query string |  | Filter by active status (true/false) | true |
| search | query string |  | Search by name or code |  |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": [
    {
      "id": "uuid",
      "tenant_id": "uuid",
      "name": "Erbil",
      "code": "erbil",
      "description": "Erbil city operations",
      "active": true,
      "created_at": "2026-05-09T10:00:00Z",
      "updated_at": "2026-05-09T10:00:00Z",
      "counts": {
        "services": 3,
        "drivers": 12,
        "users": 4,
        "customers": 0
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 3,
    "total_pages": 1
  }
}
```

## POST /delivery/teams — Create Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#post-create-team)

Create a new team. Code is auto-generated (T001, T002, ...). Name/description accept a plain string or a multilingual object { en, ar, ku, ... }.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/teams?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| description.ar | string |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Erbil",
    "ar": "أربيل",
    "ku": "ھەولێر"
  },
  "description": {
    "en": "Erbil city operations team",
    "ar": "فريق عمليات مدينة أربيل"
  }
}
```

### Response 201 Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "team_created",
  "data": {
    "id": "uuid",
    "tenant_id": "uuid",
    "name": "Erbil",
    "code": "T001",
    "description": "Erbil city operations team",
    "active": true,
    "created_at": "2026-05-09T10:00:00Z",
    "updated_at": "2026-05-09T10:00:00Z"
  }
}
```

### Response 409 Duplicate Code

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "team_code_already_exists"
}
```

## GET /delivery/teams/:teamId — Get Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#get-get-team)

Get team detail with assigned services, drivers, users, and customers.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "uuid",
    "tenant_id": "uuid",
    "name": "Erbil",
    "code": "erbil",
    "description": "Erbil city operations",
    "active": true,
    "created_at": "2026-05-09T10:00:00Z",
    "updated_at": "2026-05-09T10:00:00Z",
    "services": [
      {
        "id": "uuid",
        "name": "Erbil Same Day",
        "code": "erbil-sd",
        "active": true
      }
    ],
    "drivers": [
      {
        "id": "uuid",
        "user_id": "uuid",
        "status": "active",
        "full_name": "Ahmad",
        "avatar_url": null
      }
    ],
    "users": [
      {
        "id": "uuid",
        "full_name": "Dispatcher Ali",
        "avatar_url": null,
        "email": "ali@example.com"
      }
    ],
    "customers": []
  }
}
```

### Response 404 Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "team_not_found"
}
```

## PUT /delivery/teams/:teamId — Update Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#put-update-team)

Update team properties. All fields optional. Code is auto-generated and cannot be changed. Name/description accept a plain string or multilingual object.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| description | string |  |  |  |
| active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Erbil Updated",
    "ar": "أربيل محدث"
  },
  "description": "Updated description",
  "active": true
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "team_updated",
  "data": {
    "id": "uuid",
    "name": "Erbil Updated",
    "code": "erbil",
    "active": true
  }
}
```

## DELETE /delivery/teams/:teamId — Delete Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#delete-delete-team)

Delete a team. Clears all entity mappings (entities become 'All Teams' if no other assignments).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "team_deleted"
}
```

## PUT /delivery/teams/:teamId/services — Assign Services to Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#put-assign-services-to-team)

Replace all services assigned to this team. Send empty array to clear.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId/services?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| service_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "service_ids": [
    "service-uuid-1",
    "service-uuid-2"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "team_services_updated"
}
```

## PUT /delivery/teams/:teamId/drivers — Assign Drivers to Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#put-assign-drivers-to-team)

Replace all drivers assigned to this team.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId/drivers?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "driver_ids": [
    "driver-uuid-1",
    "driver-uuid-2"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "team_drivers_updated"
}
```

## PUT /delivery/teams/:teamId/users — Assign Users to Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#put-assign-users-to-team)

Replace all users (admins/dispatchers) assigned to this team.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId/users?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "user_ids": [
    "user-uuid-1",
    "user-uuid-2"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "team_users_updated"
}
```

## PUT /delivery/teams/:teamId/customers — Assign Customers to Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#put-assign-customers-to-team)

Replace all customers assigned to this team.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId/customers?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| customer_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "customer_ids": [
    "customer-uuid-1"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "team_customers_updated"
}
```

## DELETE /delivery/teams/:teamId/services/:serviceId — Remove Service from Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#delete-remove-service-from-team)

Remove a single service from a team without affecting other assignments.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId/services/:serviceId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |
| serviceId | path string | Yes | Delivery Service UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "service_removed_from_team"
}
```

### Response 404 Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "assignment_not_found"
}
```

## DELETE /delivery/teams/:teamId/drivers/:driverId — Remove Driver from Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#delete-remove-driver-from-team)

Remove a single driver from a team. Also removes the driver's auto-synced user_teams entry.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId/drivers/:driverId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |
| driverId | path string | Yes | Driver UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "driver_removed_from_team"
}
```

### Response 404 Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "assignment_not_found"
}
```

## DELETE /delivery/teams/:teamId/users/:userId — Remove User from Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#delete-remove-user-from-team)

Remove a single user (admin/dispatcher) from a team.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId/users/:userId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |
| userId | path string | Yes | User UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "user_removed_from_team"
}
```

### Response 404 Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "assignment_not_found"
}
```

## DELETE /delivery/teams/:teamId/customers/:customerId — Remove Customer from Team

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#delete-remove-customer-from-team)

Remove a single customer from a team.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/teams/:teamId/customers/:customerId?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| teamId | path string | Yes | Team UUID |  |
| customerId | path string | Yes | Customer UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "customer_removed_from_team"
}
```

### Response 404 Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "assignment_not_found"
}
```

## PUT /delivery/services/:serviceId/teams — Set Teams for Service

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#put-set-teams-for-service)

Set teams for a delivery service. Use all_teams:true to make unrestricted.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/services/:serviceId/teams?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| serviceId | path string | Yes | Delivery Service UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| all_teams | boolean |  |  |  |
| team_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "all_teams": false,
  "team_ids": [
    "team-uuid-1",
    "team-uuid-2"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "service_teams_updated"
}
```

## PUT /delivery/drivers/:driverId/teams — Set Teams for Driver

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#put-set-teams-for-driver)

Set teams for a driver. Use all_teams:true to make unrestricted.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/drivers/:driverId/teams?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes | Driver UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| all_teams | boolean |  |  |  |
| team_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "all_teams": false,
  "team_ids": [
    "team-uuid-1"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "driver_teams_updated"
}
```

## PUT /delivery/users/:userId/teams — Set Teams for User

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#put-set-teams-for-user)

Set teams for a user (admin/dispatcher). Use all_teams:true to make unrestricted.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/users/:userId/teams?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | User UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| all_teams | boolean |  |  |  |
| team_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "all_teams": false,
  "team_ids": [
    "team-uuid-1",
    "team-uuid-2"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "user_teams_updated"
}
```

## PUT /delivery/customers/:customerId/teams — Set Teams for Customer

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/teams#put-set-teams-for-customer)

Set teams for a customer. Use all_teams:true to make unrestricted.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/customers/:customerId/teams?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| customerId | path string | Yes | Customer UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant ID |  |
| language | query string |  | Preferred language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| all_teams | boolean |  |  |  |
| team_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "all_teams": false,
  "team_ids": [
    "team-uuid-1"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "customer_teams_updated"
}
```

## POST /delivery/drivers/:driverId/manual-transaction — Create Manual Driver Transaction

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/driver-finance#post-create-manual-driver-transaction)

Create a manual credit or debit transaction on a driver's wallet.

### Permissions Required

`manage:wallets` OR `manage:operations:tenant` OR `is_system_admin`

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `amount` | number | Yes | Transaction amount (must be > 0) |
| `type` | string | Yes | `credit` or `debit` |
| `reason` | string | Yes | Reason for the transaction (max 500 chars) |
| `reference` | string | No | External reference (max 200 chars) |
| `notes` | string | No | Additional notes (max 500 chars) |

### Business Logic
- Credit: Adds funds to driver wallet (e.g., bonuses, corrections)
- Debit: Removes funds from driver wallet (e.g., penalties, adjustments)
- For debit: fails if insufficient wallet balance
- Transaction metadata records actor ID, the authenticated actor-name snapshot, reason, and reference for the audit trail. The driver wallet history exposes the saved name through `recorded_by.name`; older records without a name snapshot resolve the current name using their saved actor ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/drivers/:driverId/manual-transaction?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driverId | path string | Yes | The driver's ID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| amount | number |  |  |  |
| type | string |  |  |  |
| reason | string |  |  |  |
| reference | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "amount": 50,
  "type": "credit",
  "reason": "Bonus for excellent performance",
  "reference": "BON-2024-001",
  "notes": "Q1 performance bonus"
}
```

### Response 201 Success - Credit

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Credit was added to the driver wallet successfully",
  "transaction": {
    "id": "txn-uuid",
    "wallet_id": "wallet-uuid",
    "amount": 50,
    "type": "credit",
    "description": "Bonus for excellent performance",
    "reference_type": "manual_credit",
    "previous_balance": 200,
    "new_balance": 250,
    "created_at": "2024-01-15T10:00:00Z"
  }
}
```

### Response 201 Success - Debit

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Debit was added to the driver wallet successfully",
  "transaction": {
    "id": "txn-uuid",
    "wallet_id": "wallet-uuid",
    "amount": 25,
    "type": "debit",
    "description": "Penalty - late delivery",
    "reference_type": "manual_debit",
    "previous_balance": 250,
    "new_balance": 225,
    "created_at": "2024-01-15T10:00:00Z"
  }
}
```

### Response 400 Error - Insufficient Balance

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient wallet balance"
}
```

### Response 400 Error - Validation

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_request",
  "errors": [
    {
      "field": "amount",
      "message": "must_be_positive_number"
    }
  ]
}
```

### Response 403 Error - Forbidden

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "forbidden"
}
```

## GET /delivery/tenant-default-policy — Get Tenant Default Policy

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/service-fulfillment-policies#get-get-tenant-default-policy)

Returns `data.effective` with all inherited policy values and `data.tenant_overrides` with the explicitly stored tenant setting. Effective payment defaults are `refund_delivery_fee_on_cancel=true` and `refund_delivery_fee_on_return=false`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/tenant-default-policy?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 Success

OK

```json
{
  "success": true,
  "data": {
    "effective": {
      "payment_policies": {
        "allow_cod_on_replacement": true,
        "refund_delivery_fee_on_cancel": true,
        "refund_delivery_fee_on_return": false
      }
    },
    "tenant_overrides": null
  }
}
```

## PUT /delivery/tenant-default-policy — Update Tenant Default Policy

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/service-fulfillment-policies#put-update-tenant-default-policy)

Stores tenant-level defaults. Both refund fields are booleans. Service policy profiles and custom service overrides may override them per service. Invalid types return HTTP 400 with localized `validation_error` and a field-specific detail.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/tenant-default-policy?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| payment_policies | object |  |  |  |
| payment_policies.allow_cod_on_replacement | boolean |  |  |  |
| payment_policies.refund_delivery_fee_on_cancel | boolean |  |  |  |
| payment_policies.refund_delivery_fee_on_return | boolean |  |  |  |



### Request body (raw)

```json
{
  "payment_policies": {
    "allow_cod_on_replacement": true,
    "refund_delivery_fee_on_cancel": true,
    "refund_delivery_fee_on_return": false
  }
}
```

### Response 200 Success

OK

```json
{
  "success": true,
  "message": "Tenant default policy updated successfully",
  "data": {
    "payment_policies": {
      "allow_cod_on_replacement": true,
      "refund_delivery_fee_on_cancel": true,
      "refund_delivery_fee_on_return": false
    }
  }
}
```

### Response 400 Validation Error

Bad Request

```json
{
  "success": false,
  "message": "Validation error",
  "details": [
    {
      "field": "payment_policies.refund_delivery_fee_on_return",
      "message": "Must be true or false"
    }
  ]
}
```

## GET /delivery/service-policy-profiles — List Fulfillment Policies

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/service-fulfillment-policies#get-list-fulfillment-policies)

Browse the tenant’s reusable fulfillment policies, including assignment, proof and payment settings.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/service-policy-profiles?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 Success

OK

```json
{
  "success": true,
  "policies": [
    {
      "id": "uuid",
      "name": "Express Policy",
      "assignment_mode": "automatic",
      "proof_settings": {
        "driver_pickup": {
          "photo_required": false,
          "signature_required": false
        },
        "client_pickup": {
          "photo_required": false,
          "signature_required": false
        },
        "driver_delivery": {
          "photo_required": true,
          "signature_required": true
        },
        "warehouse_pickup": {
          "photo_required": false,
          "signature_required": false
        },
        "warehouse_delivery": {
          "photo_required": false,
          "signature_required": false
        }
      },
      "active": true,
      "created_at": "2025-01-01T00:00:00Z",
      "updated_at": "2025-01-01T00:00:00Z"
    }
  ],
  "total": 1,
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "total_pages": 1
  }
}
```

## GET /delivery/service-policy-profiles/:id — Get Fulfillment Policy

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/service-fulfillment-policies#get-get-fulfillment-policy)

Read one tenant fulfillment policy and its linked delivery services before changing the policy’s rules.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/service-policy-profiles/:id?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Fulfillment policy profile UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 Success

OK

```json
{
  "success": true,
  "policy": {
    "id": "uuid",
    "tenant_id": "uuid",
    "name": "Express Policy",
    "assignment_mode": "automatic",
    "proof_settings": {
      "driver_pickup": {
        "photo_required": false,
        "signature_required": false
      },
      "client_pickup": {
        "photo_required": false,
        "signature_required": false
      },
      "driver_delivery": {
        "photo_required": true,
        "signature_required": true
      },
      "warehouse_pickup": {
        "photo_required": false,
        "signature_required": false
      },
      "warehouse_delivery": {
        "photo_required": false,
        "signature_required": false
      }
    },
    "active": true,
    "created_at": "2025-01-01T00:00:00Z",
    "updated_at": "2025-01-01T00:00:00Z"
  },
  "linked_services": []
}
```

### Response 404 Not Found

Not Found

```json
{
  "success": false,
  "message": "not_found"
}
```

## POST /delivery/service-policy-profiles — Create Fulfillment Policy

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/service-fulfillment-policies#post-create-fulfillment-policy)

Save a reusable set of assignment, proof-of-delivery and payment rules for delivery services.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/service-policy-profiles?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| assignment_mode | string |  |  |  |
| proof_settings | object |  |  |  |
| proof_settings.driver_pickup | object |  |  |  |
| proof_settings.driver_pickup.photo_required | boolean |  |  |  |
| proof_settings.driver_pickup.signature_required | boolean |  |  |  |
| proof_settings.client_pickup | object |  |  |  |
| proof_settings.client_pickup.photo_required | boolean |  |  |  |
| proof_settings.client_pickup.signature_required | boolean |  |  |  |
| proof_settings.driver_delivery | object |  |  |  |
| proof_settings.driver_delivery.photo_required | boolean |  |  |  |
| proof_settings.driver_delivery.signature_required | boolean |  |  |  |
| proof_settings.warehouse_pickup | object |  |  |  |
| proof_settings.warehouse_pickup.photo_required | boolean |  |  |  |
| proof_settings.warehouse_pickup.signature_required | boolean |  |  |  |
| proof_settings.warehouse_delivery | object |  |  |  |
| proof_settings.warehouse_delivery.photo_required | boolean |  |  |  |
| proof_settings.warehouse_delivery.signature_required | boolean |  |  |  |
| payment_policies | object |  |  |  |
| payment_policies.allow_cod_on_replacement | boolean |  |  |  |
| payment_policies.refund_delivery_fee_on_cancel | boolean |  |  |  |
| payment_policies.refund_delivery_fee_on_return | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": "Express Policy",
  "assignment_mode": "automatic",
  "proof_settings": {
    "driver_pickup": {
      "photo_required": false,
      "signature_required": false
    },
    "client_pickup": {
      "photo_required": false,
      "signature_required": false
    },
    "driver_delivery": {
      "photo_required": true,
      "signature_required": true
    },
    "warehouse_pickup": {
      "photo_required": false,
      "signature_required": false
    },
    "warehouse_delivery": {
      "photo_required": false,
      "signature_required": false
    }
  },
  "payment_policies": {
    "allow_cod_on_replacement": true,
    "refund_delivery_fee_on_cancel": true,
    "refund_delivery_fee_on_return": false
  }
}
```

### Response 201 Created

Created

```json
{
  "success": true,
  "policy": {
    "id": "uuid",
    "tenant_id": "uuid",
    "name": "Express Policy",
    "assignment_mode": "automatic",
    "proof_settings": {
      "driver_pickup": {
        "photo_required": false,
        "signature_required": false
      },
      "client_pickup": {
        "photo_required": false,
        "signature_required": false
      },
      "driver_delivery": {
        "photo_required": true,
        "signature_required": true
      },
      "warehouse_pickup": {
        "photo_required": false,
        "signature_required": false
      },
      "warehouse_delivery": {
        "photo_required": false,
        "signature_required": false
      }
    },
    "active": true,
    "created_at": "2025-01-01T00:00:00Z",
    "updated_at": "2025-01-01T00:00:00Z"
  }
}
```

### Response 400 Validation Error

Bad Request

```json
{
  "success": false,
  "message": "name is required"
}
```

## PUT /delivery/service-policy-profiles/:id — Update Fulfillment Policy

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/service-fulfillment-policies#put-update-fulfillment-policy)

Change the name or fulfillment rules of an existing policy.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/delivery/service-policy-profiles/:id?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Fulfillment policy profile UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| proof_settings | object |  |  |  |
| proof_settings.driver_pickup | object |  |  |  |
| proof_settings.driver_pickup.photo_required | boolean |  |  |  |
| proof_settings.driver_pickup.signature_required | boolean |  |  |  |
| proof_settings.warehouse_delivery | object |  |  |  |
| proof_settings.warehouse_delivery.photo_required | boolean |  |  |  |
| proof_settings.warehouse_delivery.signature_required | boolean |  |  |  |
| payment_policies | object |  |  |  |
| payment_policies.allow_cod_on_replacement | boolean |  |  |  |
| payment_policies.refund_delivery_fee_on_cancel | boolean |  |  |  |
| payment_policies.refund_delivery_fee_on_return | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": "Updated Express Policy",
  "proof_settings": {
    "driver_pickup": {
      "photo_required": true,
      "signature_required": true
    },
    "warehouse_delivery": {
      "photo_required": false,
      "signature_required": false
    }
  },
  "payment_policies": {
    "allow_cod_on_replacement": true,
    "refund_delivery_fee_on_cancel": true,
    "refund_delivery_fee_on_return": true
  }
}
```

### Response 200 Success

OK

```json
{
  "success": true,
  "policy": {
    "id": "uuid",
    "tenant_id": "uuid",
    "name": "Express Policy",
    "assignment_mode": "automatic",
    "proof_settings": {
      "driver_pickup": {
        "photo_required": false,
        "signature_required": false
      },
      "client_pickup": {
        "photo_required": false,
        "signature_required": false
      },
      "driver_delivery": {
        "photo_required": true,
        "signature_required": true
      },
      "warehouse_pickup": {
        "photo_required": false,
        "signature_required": false
      },
      "warehouse_delivery": {
        "photo_required": false,
        "signature_required": false
      }
    },
    "active": true,
    "created_at": "2025-01-01T00:00:00Z",
    "updated_at": "2025-01-01T00:00:00Z"
  }
}
```

## DELETE /delivery/service-policy-profiles/:id — Delete (Deactivate) Fulfillment Policy

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/service-fulfillment-policies#delete-delete-deactivate-fulfillment-policy)

Deactivate a fulfillment policy. A policy still used by a service cannot be deactivated.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/service-policy-profiles/:id?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Fulfillment policy profile UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 Success

OK

```json
{
  "success": true,
  "message": "deactivated"
}
```

### Response 409 In Use

Conflict

```json
{
  "success": false,
  "message": "policy_in_use",
  "service_count": 3
}
```

## POST /delivery/transfer-processing-points — Processing Points - Create

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-processing-points-create)

Creates a lightweight processing point. A processing point is not a warehouse; it is a receiving/sorting/data-entry location. `code` is unique per tenant.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-processing-points

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| code | string |  |  |  |
| address_id | string |  |  |  |
| metadata | object |  |  |  |
| metadata.source | string |  |  |  |



### Request body (raw)

```json
{
  "name": "G2G Processing Point",
  "code": "G2G-PP-001",
  "address_id": "{{source_address_id}}",
  "metadata": {
    "source": "postman"
  }
}
```

### Response 201 Success

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "created_successfully",
  "data": {
    "id": "uuid",
    "name": "G2G Processing Point",
    "code": "G2G-PP-001",
    "is_active": true
  }
}
```

## GET /delivery/transfer-processing-points — Processing Points - List

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-processing-points-list)

Lists processing points for the authenticated tenant. Optional filters: `is_active`, `search`, and `branch_id`. When `branch_id` is provided, the branch's default processing point has `is_default: true` and is prioritized on page 1. Non-default items omit the `is_default` key.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/transfer-processing-points?page=1&limit=20&is_active=true&branch_id={{source_branch_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| is_active | query string | No | Optional: true \| false | true |
| branch_id | query string | No | Optional branch ID used to identify and prioritize its default processing point | {{source_branch_id}} |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "data_retrieved_successfully",
  "data": [
    {
      "id": "{{processing_point_id}}",
      "name": "G2G Processing Point",
      "code": "G2G-PP-001",
      "is_active": true,
      "is_default": true
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "total_pages": 1
  }
}
```

## GET /delivery/transfer-processing-points/{{processing_point_id}} — Processing Points - Get Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-processing-points-get-detail)

Gets a processing point by id.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/transfer-processing-points/{{processing_point_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| processing_point_id | path string | Yes | Variable used inside the request path. |  |



## PATCH /delivery/transfer-processing-points/{{processing_point_id}} — Processing Points - Update

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#patch-processing-points-update)

Updates processing point fields. `code` must remain unique per tenant if changed.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/delivery/transfer-processing-points/{{processing_point_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| processing_point_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| address_id | string |  |  |  |
| metadata | object |  |  |  |
| metadata.updated_from | string |  |  |  |



### Request body (raw)

```json
{
  "name": "G2G Processing Point Updated",
  "address_id": "{{source_address_id}}",
  "metadata": {
    "updated_from": "postman"
  }
}
```

## POST /delivery/transfer-processing-points/{{processing_point_id}}/deactivate — Processing Points - Deactivate

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-processing-points-deactivate)

Soft-deactivates a processing point. Fails if non-terminal manifests reference it.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-processing-points/{{processing_point_id}}/deactivate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| processing_point_id | path string | Yes | Variable used inside the request path. |  |



### Request body (raw)

```json
{}
```

## GET /delivery/institutions — Institutions - List

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-institutions-list)

Lists tenant users whose tenant_user_type is `institution`. Use a returned `id` as `institution_user_id`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/institutions?page=1&limit=20&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| search | query string | No | Optional search by name, email, or phone |  |



## GET /delivery/institutions/{{institution_user_id}} — Institutions - Get Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-institutions-get-detail)

Gets an institution tenant_user by id.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/institutions/{{institution_user_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | path string | Yes | Variable used inside the request path. |  |



## POST /delivery/institutions/{{institution_user_id}}/branches — Institution Branches - Create

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-institution-branches-create)

Creates a branch under an institution. `institution_user_id` comes from the path, not the body. Branch code is unique per tenant/institution.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/institutions/{{institution_user_id}}/branches

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| code | string |  |  |  |
| address_id | string |  |  |  |
| default_processing_point_id | string |  |  |  |
| contact_info | object |  |  |  |
| contact_info.phone | string |  |  |  |
| metadata | object |  |  |  |
| metadata.source | string |  |  |  |



### Request body (raw)

```json
{
  "name": "G2G Branch",
  "code": "G2G-BR-001",
  "address_id": "{{source_address_id}}",
  "default_processing_point_id": "{{processing_point_id}}",
  "contact_info": {
    "phone": "+10000000000"
  },
  "metadata": {
    "source": "postman"
  }
}
```

### Response 201 Success

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "created_successfully",
  "data": {
    "id": "uuid",
    "institution_user_id": "uuid",
    "name": "G2G Branch",
    "code": "G2G-BR-001"
  }
}
```

## GET /delivery/institutions/{{institution_user_id}}/branches — Institution Branches - List

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-institution-branches-list)

Lists branches for an institution. Optional filters: `is_active`, `search`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/institutions/{{institution_user_id}}/branches?page=1&limit=20&is_active=true

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| is_active | query string |  |  | true |



## GET /delivery/institution-branches — Institution Branches - Tenant-wide List

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-institution-branches-tenant-wide-list)

Lists active branch/location records across all institutions in the tenant. Optional filters: `institution_user_id`, `is_active`, `search`, `page`, `limit`. Use this for the destination modal's Institution locations combobox. Response includes branch address and institution user basic profile.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/institution-branches?page=1&limit=20&is_active=true

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| is_active | query string |  |  | true |
| institution_user_id | query string | No |  | {{institution_user_id}} |
| search | query string | No |  | main |



## GET /delivery/institution-branches/by-code/{{branch_code}} — Institution Branches - Lookup by Code

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-institution-branches-lookup-by-code)

Exact lookup for driver branch QR scanning. The QR should contain the branch code, URL-encoded if needed. By default only active branches are returned. Response includes branch detail, address, default processing point, and institution_user. If the same code exists for multiple institutions in the tenant, the API returns 409 `branch_code_ambiguous` with matching branch/institution summaries.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/institution-branches/by-code/{{branch_code}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| branch_code | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| include_inactive | query string | No |  | true |



## GET /delivery/institutions/{{institution_user_id}}/branches/{{branch_id}} — Institution Branches - Get Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-institution-branches-get-detail)

Gets a branch owned by the institution.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/institutions/{{institution_user_id}}/branches/{{branch_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | path string | Yes | Variable used inside the request path. |  |
| branch_id | path string | Yes | Variable used inside the request path. |  |



## PATCH /delivery/institutions/{{institution_user_id}}/branches/{{branch_id}} — Institution Branches - Update

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#patch-institution-branches-update)

Updates branch fields.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/delivery/institutions/{{institution_user_id}}/branches/{{branch_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | path string | Yes | Variable used inside the request path. |  |
| branch_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| default_processing_point_id | string |  |  |  |
| metadata | object |  |  |  |
| metadata.updated_from | string |  |  |  |



### Request body (raw)

```json
{
  "name": "G2G Branch Updated",
  "default_processing_point_id": "{{processing_point_id}}",
  "metadata": {
    "updated_from": "postman"
  }
}
```

## POST /delivery/institutions/{{institution_user_id}}/branches/{{branch_id}}/deactivate — Institution Branches - Deactivate

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-institution-branches-deactivate)

Soft-deactivates a branch. Fails when non-terminal manifests reference it.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/institutions/{{institution_user_id}}/branches/{{branch_id}}/deactivate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | path string | Yes | Variable used inside the request path. |  |
| branch_id | path string | Yes | Variable used inside the request path. |  |



### Request body (raw)

```json
{}
```

## POST /delivery/institutions/{{institution_user_id}}/branches/{{branch_id}}/barcode-batches — Barcodes - Generate Batch

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-barcodes-generate-batch)

Generates postal-grade G2G barcode identities for an institution branch. `prefix` is optional; when omitted or blank, the backend uses the branch `code` as the prefix. The effective prefix must be exactly `G` + governorate letter + 5-digit Pickup Location ID (PLI), for example `GE00001`. The backend atomically reserves a 6-digit serial per governorate/PLI and appends a MOD 11 check digit calculated from PLI + serial. The first three values for `GE00001` are `GE000010000017`, `GE000010000025`, and `GE000010000033`. A governorate/PLI cannot be assigned to another branch. Max quantity is 5000. Does not create driver tasks. Frontend: send `quantity`; send `prefix` only when overriding the branch code. Error `code` remains machine-readable (`invalid_g2g_prefix`, `invalid_g2g_batch_quantity`, `g2g_pickup_location_already_assigned`, `g2g_serial_exhausted`, or `g2g_branch_not_available`), while `message` is localized using `language` or `Accept-Language`. Branch on `code` and display the returned plain-language `message`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/institutions/{{institution_user_id}}/branches/{{branch_id}}/barcode-batches

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | path string | Yes | Variable used inside the request path. |  |
| branch_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| quantity | number |  |  |  |



### Request body (raw)

```json
{
  "quantity": 3
}
```

### Response 201 Success

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Created successfully",
  "data": {
    "batch_id": "uuid",
    "prefix": "GE00001",
    "start_sequence": 1,
    "end_sequence": 3,
    "quantity": 3,
    "status": "generated",
    "print_file_url": null
  }
}
```

## GET /delivery/institutions/{{institution_user_id}}/barcode-batches — Barcodes - List Batches

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-barcodes-list-batches)

Lists barcode batches for an institution.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/institutions/{{institution_user_id}}/barcode-batches?page=1&limit=20&status=generated

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| status | query string | No | Optional: draft \| generated \| printed \| voided | generated |



## GET /delivery/institutions/{{institution_user_id}}/branches/{{branch_id}}/barcodes — Barcodes - List Branch Barcodes

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-barcodes-list-branch-barcodes)

Lists individual barcodes for a branch. Pick an `available` barcode_value for manifest scanning.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/institutions/{{institution_user_id}}/branches/{{branch_id}}/barcodes?page=1&limit=20&status=available

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | path string | Yes | Variable used inside the request path. |  |
| branch_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| status | query string | No | Optional barcode status filter | available |



## GET /delivery/institution-barcodes/{{barcode_value}} — Barcodes - Lookup Barcode

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-barcodes-lookup-barcode)

Looks up a scanned G2G barcode by barcode_value. The value must be a valid 14-character postal barcode with a matching MOD 11 check digit; malformed or checksum-invalid input returns HTTP `400` with stable `code=invalid_barcode` and a localized plain-language `message` before lookup. Frontend must branch on `code` and display `message`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/institution-barcodes/{{barcode_value}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcode_value | path string | Yes | Variable used inside the request path. |  |



## GET /delivery/barcode-batches/{{batch_id}} — Barcodes - Get Batch Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-barcodes-get-batch-detail)

Gets batch detail plus individual barcodes.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/barcode-batches/{{batch_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| batch_id | path string | Yes | Variable used inside the request path. |  |



## GET /delivery/barcode-batches/{{batch_id}}/barcodes — Barcodes - List Batch Barcodes

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-barcodes-list-batch-barcodes)

Tenant-scoped batch barcode list. page is 1-based; limit defaults to 50 and is capped at 100. Optional status and search filters apply before pagination. Returns {success, message, data: [{id, barcode_value, status, ...}], pagination: {page, limit, total, total_pages}} ordered by barcode_value ascending. Full-batch CSV: fetch all pages without search/status filters; verify unique IDs/values and count against pagination.total and batch.quantity before downloading. Selected CSV: use the same paginated list and include only selected IDs; fail if any selected record is missing. CSV is generated in the frontend and does not update printed_at, status, or print_file_url. Errors: 400 invalid filters/missing tenant, 404 missing batch, 500 unexpected error.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/barcode-batches/{{batch_id}}/barcodes?page=1&limit=100

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| batch_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 100 |



## POST /delivery/barcode-batches/{{batch_id}}/print — Barcodes - Print Batch

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-barcodes-print-batch)

Generates a printable PDF for a barcode batch. Send an empty body or `{ "print_mode": "all" }` to print all barcodes; this stores the returned file URL on `institution_barcode_batches.print_file_url` and marks the batch `printed`. Send `print_mode=selective` with `barcode_ids`, `barcode_values`, or `barcodes` to print only selected barcodes; this returns the PDF URL and updates `institution_item_barcodes.printed_at` only for selected rows without changing the batch `print_file_url`. Barcode row `status` remains the lifecycle status (`available`, `in_manifest`, etc.) so printed but unused barcodes can still be scanned. Full exports fetch every page and verify the stored count against the batch quantity before rendering. Larger batches are rendered in bounded chunks and merged into one PDF before upload. Every export has a unique file URL; selective exports never overwrite the saved full-batch file. The response data contains batch_id, print_mode, status, print_file_url, printed_at, printed_count (actual count in both modes), and barcode_ids (selected IDs or null for all). Frontend: open print_file_url for either mode, but update the cached batch download URL/status only for print_mode=all. A 500 response with code=barcode_export_incomplete means the count or uniqueness checks failed; do not report success or download a partial file. Existing errors: 400 for missing tenant/selection, 404 for missing batch or requested barcode, 409 for a voided batch, and localized 500 for rendering, upload, or bookkeeping failure.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/barcode-batches/{{batch_id}}/print

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| batch_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| print_mode | string |  |  |  |
| barcode_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "print_mode": "selective",
  "barcode_ids": [
    "{{barcode_id}}"
  ]
}
```

## POST /delivery/institution-barcodes/{{barcode_id}}/void — Barcodes - Void Single Barcode

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-barcodes-void-single-barcode)

Voids a single barcode only when status is `available`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/institution-barcodes/{{barcode_id}}/void

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcode_id | path string | Yes | Variable used inside the request path. |  |



### Request body (raw)

```json
{}
```

## POST /delivery/barcode-batches/{{batch_id}}/void — Barcodes - Void Batch

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-barcodes-void-batch)

Voids an entire batch only if all barcodes are unused (`available` or already `void`).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/barcode-batches/{{batch_id}}/void

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| batch_id | path string | Yes | Variable used inside the request path. |  |



### Request body (raw)

```json
{}
```

## POST /delivery/transfer-manifests — Driver - Create Draft Manifest

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-driver-create-draft-manifest)

Driver creates a draft transfer manifest for an institution branch. Normally driver_id resolves from the driver token; body driver_id is included here for explicit testing.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| institution_user_id | string |  |  |  |
| source_branch_id | string |  |  |  |
| processing_point_id | string |  |  |  |
| driver_id | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "institution_user_id": "{{institution_user_id}}",
  "source_branch_id": "{{branch_id}}",
  "processing_point_id": "{{processing_point_id}}",
  "driver_id": "{{driver_id}}",
  "notes": "Postman transfer manifest test"
}
```

### Response 201 Success

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "created_successfully",
  "data": {
    "id": "uuid",
    "manifest_number": "TM-...",
    "status": "draft"
  }
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/items/scan — Driver - Scan Item Barcode

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-driver-scan-item-barcode)

Scans a barcode into a draft or picked_up manifest. The claim, manifest-item insert, barcode custody update, and audit event are atomic. `barcode_value` must be a valid 14-character G2G postal barcode with a matching MOD 11 check digit. Optional `entry_method` accepts only `manual` or `camera` and defaults to `camera` when omitted. The normalized value is persisted as `payload.entry_method` on the atomic `item_scanned` event; item status and the success response remain unchanged. Invalid values return HTTP 400 with `code=invalid_entry_method` and a localized `message`. Every barcode failure returns `{ success: false, code, message }`; mobile must classify by `code`, not translated `message`. Codes/statuses: `invalid_barcode` (400), `barcode_not_found` (404), `barcode_institution_mismatch` (400), `barcode_branch_mismatch` (400), `barcode_voided` (409), `barcode_already_scanned` (409 when the barcode already belongs to this manifest), `barcode_already_used` (409 when it belongs to another manifest/workflow), and `internal_server_error` (500). Concurrent duplicate scans return the same stable conflict codes. Frontend integration: send `entry_method=manual` only when the operator typed the barcode; otherwise send `camera` or omit the field.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items/scan

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcode_value | string |  |  |  |
| entry_method | string |  |  |  |



### Request body (raw)

```json
{
  "barcode_value": "{{barcode_value}}",
  "entry_method": "camera"
}
```

### Response 201 Success

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "item_added_successfully",
  "data": {
    "id": "uuid",
    "barcode_value": "GE000010000017",
    "status": "scanned"
  }
}
```

### Response 409 Already Scanned In Current Manifest

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "barcode_already_scanned",
  "message": "Barcode already scanned"
}
```

### Response 409 Already Used Elsewhere

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "barcode_already_used",
  "message": "Barcode already used"
}
```

## DELETE /delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}} — Driver - Remove Item Before Submit

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#delete-driver-remove-item-before-submit)

Removes a scanned item only while the manifest is still draft. The operation is atomic: it returns the barcode to available, deletes the manifest item, reconciles expected_item_count to the actual remaining row count, and records item_removed in manifest history. A success response guarantees that the database item row was deleted.

### Frontend integration

call DELETE with the manifest ID and item ID from the current manifest item. On HTTP 200, remove that item from the local list and use data.remaining_item_count as the authoritative count. Handle 404 as a stale/missing manifest or item, 409 as a manifest that is no longer editable, and 500 as a failed removal; do not remove the item locally for non-200 responses.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |
| item_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Item Removed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "item_removed_successfully",
  "data": {
    "id": "{{item_id}}",
    "manifest_id": "{{manifest_id}}",
    "barcode_value": "SPB00100000008",
    "remaining_item_count": 1
  }
}
```

### Response 409 Manifest No Longer Editable

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "manifest_not_editable"
}
```

### Response 404 Manifest or Item Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "item_not_found"
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/submit-pickup — Driver - Submit Pickup

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-driver-submit-pickup)

Locks the scan list and moves manifest draft -> picked_up. Items move scanned -> picked_up. Requires at least one item. Accepts optional `photo_urls` (string[]) and `signature_url` (string) for proof of pickup. Required fields depend on tenant proof_settings (transfer_manifest > proof_settings.pickup).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/submit-pickup

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| photo_urls | array<string> |  |  |  |
| signature_url | string |  |  |  |



### Request body (raw)

```json
{
  "photo_urls": [
    "https://cdn.example.com/photos/pickup_1.jpg"
  ],
  "signature_url": "https://cdn.example.com/signatures/pickup_sig.png"
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/driver-action — Driver - Driver Action

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-driver-driver-action)

Generic driver action endpoint for transfer manifests. Supported action values: `submit_pickup`, `arrive_processing`. Accepts optional `photo_urls` (string[]) and `signature_url` (string) for proof capture at the relevant lifecycle stage. Required fields depend on tenant proof_settings (transfer_manifest > proof_settings.pickup/delivery).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/driver-action

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| action | string |  |  |  |
| photo_urls | array<string> |  |  |  |
| signature_url | string |  |  |  |



### Request body (raw)

```json
{
  "action": "submit_pickup",
  "photo_urls": [
    "https://cdn.example.com/photos/pickup_1.jpg"
  ],
  "signature_url": "https://cdn.example.com/signatures/pickup_sig.png"
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/arrive-processing — Driver - Arrive Processing

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-driver-arrive-processing)

Marks the manifest as awaiting receipt at the processing point. Accepts optional `photo_urls` (string[]) and `signature_url` (string) for proof of delivery. Required fields depend on tenant proof_settings (transfer_manifest > proof_settings.delivery).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/arrive-processing

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| photo_urls | array<string> |  |  |  |
| signature_url | string |  |  |  |



### Request body (raw)

```json
{
  "photo_urls": [
    "https://cdn.example.com/photos/delivery_1.jpg"
  ],
  "signature_url": "https://cdn.example.com/signatures/delivery_sig.png"
}
```

## PATCH /delivery/transfer-manifests/{{manifest_id}}/status — App - Change Manifest Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#patch-app-change-manifest-status)

Moves a Transfer Manifest through its forward transport stages or cancels it. Receiving, destination assignment/order creation, and completion remain dedicated actions after arrival.

- Legacy flat `photo_urls` and `signature_url` remain accepted when the request crosses only one proof stage.

### Forward state machine
`draft` -> `picked_up` -> `in_transit` -> `awaiting_receipt`.
A request may target any later transport state. The backend runs every missing intermediate transition and records the same submitted `reason` and `notes` on each transition event.

### Cancellation
- `cancelled` is allowed from `draft`, `picked_up`, `in_transit`, or `awaiting_receipt`.
- Cancellation uses the existing atomic cancellation operation: non-order-created items become `void` and their linked barcodes return to `available`.
- Cancellation is rejected after receiving starts or when an order has already been created.
- The dedicated `POST /delivery/transfer-manifests/{id}/cancel` endpoint remains supported for compatibility.

After `awaiting_receipt`, use the existing dedicated actions:
- Start receiving: `POST /delivery/transfer-manifests/{id}/receive/start`.
- Receive scanned items: `POST /delivery/transfer-manifests/{id}/receive/items`.
- Complete receiving: `POST /delivery/transfer-manifests/{id}/receive/complete`.
- Assign destinations/create orders: the existing item destination or bulk destination endpoints.
- Complete manifest: `POST /delivery/transfer-manifests/{id}/complete`.

Success response data contains `id`, `previous_status`, `requested_status`, authoritative `status`, submitted `reason`, optional `notes`, `reached_target`, ordered `executed_transitions`, and `allowed_next_statuses`. Transport responses include every later transport target plus `cancelled` while cancellation remains available. A cancellation response also includes `voided_count`.

### Important errors
- HTTP 400 `invalid_manifest_status`: missing or unknown status; `data.allowed_statuses` contains `picked_up`, `in_transit`, `awaiting_receipt`, and `cancelled`.
- HTTP 400 `manifest_status_reason_required`: `reason` is missing or blank.
- HTTP 400 for oversized reason/notes or required pickup/delivery proof; display the localized `message`.
- HTTP 404 `manifest_not_found`.
- HTTP 409 `invalid_manifest_status_transition`: backward, same-state, or post-arrival target.
- HTTP 409 `manifest_cannot_be_cancelled` or `manifest_cannot_be_cancelled_orders_created`: cancellation business rule failed.

### Frontend integration contract
- Always collect and send non-blank `reason`; send `notes` only when provided.
- Render status actions from `data.allowed_next_statuses`, including the literal `cancelled` action.
- After HTTP 200, use `data.status` as authoritative and do not update optimistically.
- At `awaiting_receipt`, use the existing receiving/processing buttons for later work; `cancelled` remains available only if returned by the backend.
- Display localized `message` and branch on stable `code`. No navigation action is returned.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string | Yes | `picked_up`, `in_transit`, `awaiting_receipt`, or `cancelled`. |  |
| reason | string | Yes | (required, max 2000 characters): a non-blank operator reason recorded on every executed status event. |  |
| notes | string | No | (optional, max 2000 characters): additional audit context recorded with the reason. |  |
| proofs | object |  |  |  |
| proofs.pickup | object | No | optional `photo_urls` and `signature_url`; required values depend on tenant pickup-proof settings. |  |
| proofs.pickup.photo_urls | array |  |  |  |
| proofs.pickup.signature_url | null |  |  |  |
| proofs.delivery | object | No | optional `photo_urls` and `signature_url`; required values depend on tenant delivery-proof settings. |  |
| proofs.delivery.photo_urls | array |  |  |  |
| proofs.delivery.signature_url | null |  |  |  |



### Request body (raw)

```json
{
  "status": "awaiting_receipt",
  "reason": "Manifest reached the destination processing point",
  "notes": "Optional dispatcher note",
  "proofs": {
    "pickup": {
      "photo_urls": [],
      "signature_url": null
    },
    "delivery": {
      "photo_urls": [],
      "signature_url": null
    }
  }
}
```

### Response 200 Success - Forward State Machine

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Transfer manifest status updated successfully",
  "data": {
    "id": "manifest-uuid",
    "previous_status": "draft",
    "requested_status": "awaiting_receipt",
    "status": "awaiting_receipt",
    "reason": "Manifest reached the destination processing point",
    "notes": "Optional dispatcher note",
    "reached_target": true,
    "executed_transitions": [
      {
        "from_status": "draft",
        "to_status": "picked_up"
      },
      {
        "from_status": "picked_up",
        "to_status": "in_transit"
      },
      {
        "from_status": "in_transit",
        "to_status": "awaiting_receipt"
      }
    ],
    "allowed_next_statuses": [
      "cancelled"
    ]
  }
}
```

### Response 200 Success - Cancelled

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Transfer manifest status updated successfully",
  "data": {
    "id": "manifest-uuid",
    "previous_status": "in_transit",
    "requested_status": "cancelled",
    "status": "cancelled",
    "reason": "Transport plan cancelled",
    "notes": null,
    "voided_count": 12,
    "reached_target": true,
    "executed_transitions": [
      {
        "from_status": "in_transit",
        "to_status": "cancelled"
      }
    ],
    "allowed_next_statuses": []
  }
}
```

### Response 409 Invalid Transition

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_manifest_status_transition",
  "message": "Invalid manifest status transition",
  "data": {
    "id": "manifest-uuid",
    "current_status": "completed",
    "requested_status": "processing",
    "allowed_next_statuses": []
  }
}
```

## GET /delivery/driver/transfer-manifests — Driver - List My Transfer Manifests

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-driver-list-my-transfer-manifests)

Driver-scoped manifest list. Returns only manifests assigned to the authenticated driver. Optional `search` matches the manifest number OR a contained item barcode (case-insensitive partial matching), while preserving tenant and driver scope, counts, and pagination. Returns each matching manifest once; no match returns an empty list.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver/transfer-manifests?page=1&limit=20&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| search | query string |  | Case-insensitive partial search by manifest number OR any item barcode in the manifest. Accepts a full barcode or a suffix such as the last 6 digits. Returns each matching manifest once. |  |



## GET /delivery/driver/transfer-manifests/{{manifest_id}} — Driver - Get My Transfer Manifest

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-driver-get-my-transfer-manifest)

Driver-scoped manifest detail. Returns 404 if the manifest belongs to another driver.

- Requires `Authorization: Bearer {{access_token}}` for the assigned driver.

### Successful response
- `data` contains the transfer manifest header, five maintained counts (`expected_item_count`, `received_item_count`, `exception_item_count`, `order_created_count`, `voided_item_count`), institution user, processing point, `source_branch`, `branch`, and `proofRequirements`.
- `data.cancel_reason` is `null` for active manifests and contains `payload.reason` from the latest `manifest_cancelled` event when `data.status` is `cancelled`.
- `proofRequirements` (object): `{ pickup: { photo_required, signature_required }, delivery: { photo_required, signature_required } }` — tells the app what proof must be captured at each stage based on tenant settings.
- `data.branch` is the source branch information object and mirrors `data.source_branch` for frontend convenience.
- Branch fields include `id`, `tenant_id`, `institution_user_id`, `address_id`, `name`, `code`, `contact_info`, `default_processing_point_id`, `is_active`, `metadata`, `address`, and `default_processing_point`.
- When proof has been captured, `data.proof_of_pickup` and/or `data.proof_of_delivery` are present with `{ photos, signature_url, captured_at, captured_by }`.

### Frontend integration contract
- Use the five count fields directly; do not derive them from a paginated item list.
- Use `data.cancel_reason` to display cancellation reason on cancelled manifests.
- Use `data.branch` or `data.source_branch` to render source branch name/code and branch address.
- Continue using `data.source_branch_id` as the stable entity ID for follow-up API calls.
- Read `proofRequirements` to determine which capture fields to show/enforce at pickup and delivery stages.
- On HTTP 404, navigate away from the manifest detail screen or show a stale/unauthorized manifest state.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/driver/transfer-manifests/{{manifest_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



## GET /delivery/transfer-manifests — Dispatcher - List Transfer Manifests

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-dispatcher-list-transfer-manifests)

Dispatcher list with tenant-scoped server-side filters, exact filtered pagination totals, and deterministic sorting. Existing `page`, `limit`, `search`, `status`, `institution_user_id`, `source_branch_id`, `processing_point_id`, `date_from`, `date_to`, and `barcode_value` behavior remains supported. Every populated `column_filters` value is AND-combined with the standalone filters and search before count and pagination. Empty values are ignored; populated unsupported keys and invalid UUID, number, range, or YYYY-MM-DD date values return HTTP 400. Date `to` values include the complete UTC day. `search` remains top-level and is not read from `column_filters`. Tenant cannot be overridden through `column_filters`.

### Supported column filters
- `manifest_number`: case-insensitive contains.
- `status`: exact string.
- `institution_user_id`, `processing_point_id`: exact UUID.
- `expected_item_count`, `received_item_count`, `exception_item_count`, `order_created_count`: inclusive `{ min?, max? }` non-negative numeric range.
- `picked_up_at`, `received_at`, `arrived_at`, `completed_at`, `created_at`: inclusive `{ from?, to? }` YYYY-MM-DD range.

### Examples
- Text: `column_filters={"manifest_number":"TM"}`
- Exact status: `column_filters={"status":"receiving"}`
- Autocomplete UUID: `column_filters={"institution_user_id":"11111111-1111-4111-8111-111111111111"}`
- Numeric range: `column_filters={"expected_item_count":{"min":1,"max":100}}`
- Date range: `column_filters={"created_at":{"from":"2026-08-01","to":"2026-08-31"}}`
- Sorted response: `sort_by=processing_point_id&sort_order=asc` sorts by processing-point name, with nulls last and `id DESC` as the deterministic secondary order.

Allowed `sort_by`: `manifest_number`, `status`, `institution_user_id`, `processing_point_id`, the four count fields, the five timestamp fields, and `created_at`. Missing or invalid `sort_by` uses `created_at DESC`; invalid `sort_order` returns HTTP 400 `invalid_sort_order`.

### The response structure remains unchanged

`data`, `status_matrix`, and `pagination`. `pagination.total` is the filtered count before limit/offset. Each row includes the five maintained item counts, `received_by`, `received_by_user` (`id`, `full_name`, `email`), `institution_user` (`id`, `full_name`, `email`, `phone`, `avatar_url`), and `processing_point` (`id`, `name`, `code`, `address_id`) relationship objects. `received_item_count` is the durable number of items with `received_at`; it remains populated after those items become orders.

### Frontend integration contract

JSON-encode `column_filters`; keep global search in `search`; use IDs from autocomplete selections; omit empty filters; send only documented `sort_by` values and `asc`/`desc`; retain the existing response mapper; display localized HTTP 400 `message` and use `code` for branching. Render Received By from `received_by_user` and Received Time from `received_at`; no extra user lookup is required.

### Global search

send only `search` for manifest-number or item-barcode lookup. Both are OR-combined within the authenticated tenant before filtering, exact counts, status_matrix, sorting, and pagination. Matching multiple items does not duplicate a manifest; a barcode in multiple manifests returns every matching manifest. Full and partial barcodes (including the last 6 digits) are supported. No match returns `data: []` and zero pagination totals. Default sorting remains newest first. The separate `barcode_value` filter remains supported and is AND-combined when explicitly supplied. No new response fields or permissions are required. Frontend: keep sending `search` and label the input "Search manifest number or item barcode...".

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests?page=1&limit=20&status=&institution_user_id=&source_branch_id=&processing_point_id=&date_from=&date_to=&search=&barcode_value=&column_filters=%7B%22status%22%3A%22receiving%22%2C%22expected_item_count%22%3A%7B%22min%22%3A1%2C%22max%22%3A100%7D%2C%22created_at%22%3A%7B%22from%22%3A%222026-08-01%22%2C%22to%22%3A%222026-08-31%22%7D%7D&sort_by=created_at&sort_order=desc

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| status | query string | No | Optional manifest status |  |
| institution_user_id | query string | No | Optional institution tenant_user id |  |
| source_branch_id | query string | No | Optional source branch id |  |
| processing_point_id | query string | No | Optional processing point id |  |
| date_from | query string | No | Optional created_at from |  |
| date_to | query string | No | Optional created_at to |  |
| search | query string |  | Case-insensitive partial search by manifest number OR any item barcode in the manifest. Accepts a full barcode or a suffix such as the last 6 digits. Returns each matching manifest once. |  |
| barcode_value | query string | No | Optional case-insensitive partial G2G barcode filter. Returns manifests containing a matching item barcode. |  |
| column_filters | query string | No | Optional JSON-encoded column filters. Supported keys: manifest_number, status, institution_user_id, processing_point_id, expected_item_count, received_item_count, exception_item_count, order_created_count, picked_up_at, received_at, arrived_at, completed_at, created_at. | {"status":"receiving","expected_item_count":{"min":1,"max":100},"created_at":{"from":"2026-08-01","to":"2026-08-31"}} |
| sort_by | query string | No | Optional whitelisted sort column. institution_user_id sorts by institution user full_name; processing_point_id sorts by processing point name. Invalid values fall back to created_at DESC. | created_at |
| sort_order | query string | No | Optional asc or desc. Defaults to desc when a valid sort_by is supplied. | desc |



### Response 400 Malformed column_filters JSON



```json
{
  "success": false,
  "code": "invalid_column_filters_json",
  "message": "Invalid column_filters JSON",
  "details": [
    {
      "field": "column_filters",
      "message": "Invalid column_filters JSON"
    }
  ]
}
```

### Response 400 Invalid sort_order



```json
{
  "success": false,
  "code": "invalid_sort_order",
  "message": "sort_order must be asc or desc",
  "details": [
    {
      "field": "sort_order",
      "message": "sort_order must be asc or desc"
    }
  ]
}
```

## GET /delivery/transfer-manifests/{{manifest_id}} — Dispatcher - Get Manifest Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-dispatcher-get-manifest-detail)

Manifest detail with source branch, processing point, source address, driver, creator, and receiver embeds.

- Requires `Authorization: Bearer {{access_token}}`.

### Successful response
- `data` contains the transfer manifest header and five database-maintained counts: `expected_item_count`, `received_item_count`, `exception_item_count`, `order_created_count`, and `voided_item_count`. Counts are recalculated after every item write, including empty manifests. `received_item_count` is historical custody evidence based on item `received_at`, so it does not fall back to zero after order creation.
- Every received manifest includes `received_at`, `received_by`, and `received_by_user`. Legacy manifests that previously skipped receipt remain readable; the backend does not invent missing custody data.
- `data.cancel_reason` is `null` for active manifests and contains `payload.reason` from the latest `manifest_cancelled` event when `data.status` is `cancelled`.
- `data.branch` is the source branch information object and mirrors `data.source_branch` for frontend convenience.
- Branch fields include `id`, `tenant_id`, `institution_user_id`, `address_id`, `name`, `code`, `contact_info`, `default_processing_point_id`, `is_active`, `metadata`, `address`, and `default_processing_point`.
- `data.source_address` remains the manifest's stored source address snapshot/reference.

### Frontend integration contract
- Use the five count fields directly; do not derive them from the paginated item list.
- Render Received By from `data.received_by_user` and Received Time from `data.received_at`.
- Use `data.cancel_reason` to display cancellation reason on cancelled manifests.
- Use `data.branch` or `data.source_branch` to render source branch name/code and branch address without an extra branch lookup.
- Continue using `data.source_branch_id` as the stable branch entity ID for filters and follow-up API calls.
- On HTTP 404, show a missing/stale manifest state; on HTTP 400, tenant context is missing.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



## GET /delivery/transfer-manifests/{{manifest_id}}/items — Dispatcher - List Manifest Items

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-dispatcher-list-manifest-items)

Returns paginated items for one transfer manifest. Search matches either the G2G barcode or the denormalized delivery code written when the item becomes an order. Barcode matching uses the submitted search text unchanged. Delivery-code matching removes a leading `DEL` prefix when present, so `DEL1911`, `DEL19`, `1911`, and `19` can match delivery_code `1911`. Existing linked items are backfilled from deliveries. PostgREST control characters are escaped by the backend.

requires Authorization: Bearer {{access_token}}.

### Filters
- `status` filters the primary manifest-item status. Use `status=order_created` to list items promoted to delivery orders.
- `delivery_status` optionally filters the linked delivery's exact status. It is tenant- and manifest-scoped and applied in the database before the exact count and page range.
- `destination_added` filters whether the item has a stored destination-address snapshot.

### Success

HTTP 200 with data and exact pagination fields `page`, applied `limit`, `requested_limit`, `limit_capped`, `max_limit`, `total`, and `total_pages`. Promoted items include `delivery_code`, `delivery_id`, `delivery`, and `destination_address`. `destination_address` is the JSON snapshot copied from the current linked delivery and is returned directly without reading the addresses table. `destination_address_id` remains the optional address-book reference and can be null for custom map or Places locations. Each item also returns `can_change_destination` and `destination_change_block_code`. The action is allowed for users with `manage:operations:tenant`, `manage:module:delivery`, or system-admin access while the linked order is not delivered, returned, or cancelled. Manifest completion does not disable the action because manifest status represents order-creation processing, not final delivery. When a requested limit exceeds 200, the backend returns up to 200 rows, sets `pagination.limit=200`, `pagination.requested_limit` to the submitted number, `pagination.limit_capped=true`, `pagination.max_limit=200`, top-level `code=manifest_items_page_limit_applied`, and a localized explanatory `message`. Normal requests set `limit_capped=false` and omit the top-level code.

### Errors

HTTP 400 when tenant context is missing, HTTP 404 when the manifest is missing, and HTTP 500 for database failures.

### Frontend integration contract

continue rendering `data[].destination_address.full_address`. Show the Change destination row action only when `data[].can_change_destination=true`; do not derive availability from the manifest status. `destination_change_block_code` is null when allowed and otherwise explains the permission or order-state refusal. Use `pagination.limit` as the applied page size. When `pagination.limit_capped=true` or `code=manifest_items_page_limit_applied`, show the localized `message` and reconcile the grid page size to `pagination.limit`. For a manifest Delivery Orders view, send `status=order_created`, use `search` for order-code or item-barcode search, and send `delivery_status` for the Order Status filter. Use `data[].delivery.id` to open the existing order detail surface. No navigation action is returned by this endpoint.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items?page=1&limit=50&search=&status=&delivery_status=&destination_added=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number. Default: 1. | 1 |
| limit | query string |  | Items per page. Default: 50; maximum: 200. | 50 |
| search | query string |  | Case-insensitive partial search against barcode_value using the submitted text, or the linked order's stored delivery_code after removing a leading DEL prefix when present. Example: DEL1911, DEL19, 1911, and 19 can match delivery_code 1911. |  |
| status | query string | No | Optional exact manifest-item status. |  |
| delivery_status | query string | No | Optional exact status of the linked delivery. Applied in the database before counting and pagination. |  |
| destination_added | query string | No | Optional boolean filter based on whether the manifest item has a stored destination_address JSON snapshot. |  |



## POST /delivery/transfer-manifests/{{manifest_id}}/receive/start — Dispatcher - Start Receiving

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-start-receiving)

Begins receipt at the processing point. The manifest must be in `awaiting_receipt`, which confirms that arrival has been completed. Any other current status returns HTTP 409 with `{ "success": false, "message": <localized invalid manifest status transition message> }`, and receiving does not start.

### Success

HTTP 200 returns `{ "success": true, "message": <localized success message>, "data": { "id": <manifest_id>, "status": "receiving" } }`.

### Frontend integration contract

enable Start Receiving only when the manifest status is exactly `awaiting_receipt`. On HTTP 409, keep the current manifest state and display the returned localized `message`. No request fields or navigation behavior change.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/receive/start

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body (raw)

```json
{}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/receive/items — Dispatcher - Receive Items

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-receive-items)

Marks submitted items received in scan_each_item mode and supports partial batch success. Optional top-level `entry_method` accepts only `manual` or `camera`, applies to every barcode in this request, and defaults to `camera` when omitted.

### Business rules
- The manifest must be in `receiving` status.
- Only manifest items in `scanned`, `picked_up`, or `in_transit` status can be newly received.
- Item state, barcode state, `received_at`, one item-level `item_received` event per item, and the aggregate `items_received` event are written atomically in one database call. User-driven events persist the normalized value as `payload.entry_method`; automated count-only and bulk receipts do not fabricate an entry method.
- Invalid `entry_method` returns HTTP 400 with `code=invalid_entry_method` and a localized `message`; no submitted item is changed.
- `data.received` contains only barcodes newly received by this request. `data.received_count` always equals `data.received.length`; already-received barcodes never appear here, preventing count inflation.
- `data.already_processed` contains submitted barcodes already in `received` or another non-receivable item status. These are completed items, not invalid barcodes, so they do not appear in `data.invalid_barcodes`.
- `data.not_in_manifest` contains every submitted barcode absent from this transfer manifest.
- `data.not_found` is the subset of `not_in_manifest` that is also absent from the tenant barcode registry.
- `data.invalid_barcodes` gives the exact per-barcode reason, including `invalid_barcode`, `unknown_barcode`, `not_part_of_this_manifest`, `wrong_branch`, `wrong_institution`, `already_used_in_another_manifest`. Classification lists intentionally overlap where useful: for example, an unknown barcode appears in both `not_in_manifest` and `not_found`.
- A malformed barcode is classified without stopping the rest of the batch, and its response message uses the localized `invalid_barcode` translation.
- Valid receivable barcodes are committed even when other submitted barcodes fail. A mixed result returns HTTP `200`, `success=true`, localized message `Partially completed`, the newly received barcodes in `received`, and all failures in the other lists.
- When no barcode is newly received and failures remain, the endpoint returns HTTP `409`, `success=false`. An already-processed-only request uses the localized already-processed message; a malformed-only request also returns `code=invalid_barcode`.

### Frontend integration contract
- Send `entry_method=manual` only when the operator typed the submitted barcode values; otherwise send `camera` or omit the field.
- Treat each response list as the source of truth; do not infer the whole batch result from only the HTTP status.
- Add only `data.received` to the newly received UI state and increment by exactly `data.received_count`.
- Do not add `data.already_processed` again or show it under Invalid Barcodes; show it under Already Processed with the localized message `This item has already been processed` in English.
- Show `data.not_in_manifest`, `data.not_found`, and `data.invalid_barcodes[]` as per-barcode failures. `data.invalid_barcodes[]` contains only actual validation failures.
- On HTTP `200` with `message=Partially completed`, retain the failed barcodes for correction while applying the successful `received` entries.
- On HTTP `409`, no submitted barcode was newly received. No navigation change is required.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/receive/items

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcode_values | array<string> |  |  |  |
| entry_method | string |  |  |  |



### Request body (raw)

```json
{
  "barcode_values": [
    "{{barcode_value}}"
  ],
  "entry_method": "camera"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Items received successfully",
  "data": {
    "received_count": 1,
    "received": [
      "GE000010000017"
    ],
    "invalid_barcodes": [],
    "not_in_manifest": [],
    "not_found": [],
    "already_processed": []
  }
}
```

### Response 200 Partial Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Partially completed",
  "data": {
    "received_count": 1,
    "received": [
      "GE000010000017"
    ],
    "invalid_barcodes": [
      {
        "barcode_value": "GE100020000012",
        "reason": "invalid_barcode"
      },
      {
        "barcode_value": "GE100029999990",
        "reason": "unknown_barcode"
      }
    ],
    "not_in_manifest": [
      "GE100020000012",
      "GE100029999990"
    ],
    "not_found": [
      "GE100020000012",
      "GE100029999990"
    ],
    "already_processed": []
  }
}
```

### Response 409 Already Received

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This item has already been processed",
  "data": {
    "received_count": 0,
    "received": [],
    "invalid_barcodes": [],
    "not_in_manifest": [],
    "not_found": [],
    "already_processed": [
      "GE000010000017"
    ]
  }
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/receive/complete — Dispatcher - Complete Receiving

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-complete-receiving)

Completes receipt. In scan_each_item mode, remaining unreceived items become missing_at_receipt exceptions. In count_only mode, open items are atomically marked received with one item-level `item_received` event per item before the manifest receipt completes.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/receive/complete

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "notes": "Receipt completed from Postman"
}
```

## PATCH /delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}}/destination — Dispatcher - Assign Item Destination

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#patch-dispatcher-assign-item-destination)

Assigns destination address and delivery service to one received item, then immediately creates the delivery order using institution wallet payment with payer_type=sender. destination_branch_id is optional; remove it or leave blank if not applicable. delivery_service_id is optional when G2G settings has default_delivery_service_id configured; otherwise it is required. Destination branches must be active and have an address. Fails if item is not received or is terminal.

The created order always uses the manifest processing point as its pickup location. `transfer_manifest.delivery_fee_origin` controls only the fee calculation: `processing_point` prices from that pickup location; `source_location` prices from the manifest's stored source address. When `source_location` is selected but the source address is unavailable, the endpoint returns HTTP 400 with `source_address_unresolved`.

Delivery creation honors the effective policy of the selected delivery service, including pricing fallback, payment policy, and warehouse staging policy. For a staged Pickup and Delivery or Delivery order with no package items, the backend creates one fallback warehouse inventory item named from the delivery code and customer name. Non-delivery service types can still fail with `warehouse_staging_only_supported_for_pickup_and_delivery`. A staged P&D promotion commits its parent, A/B legs, inventory item, receiving order, and sequence plan atomically; an internal creation failure leaves no promotable partial order, and a retry will not accept a historical staged parent without a complete A/B sequence plan. Automatically allocated inventory item codes skip codes already claimed manually.

### Event

an `item_destination_set` event is recorded with `payload.barcode_value` set to the item's barcode.

### Manifest origin and label behavior
- The operational pickup remains the manifest processing point; receiver/dropoff behavior is unchanged.
- At order creation, the backend stores `source_transfer_manifest_meta.snapshot_version=manifest_origin_v1` with the manifest number, item barcode, source institution ID/name, item source branch ID/name/code, cleaned branch address, branch phone when available, processing-point ID, and existing pickup/pricing audit fields. The source comes from each manifest item, not from the processing point.
- A warehouse-staged parent stores the manifest ID, manifest item ID, and snapshot. Its inbound/outbound A/B legs store the manifest ID and the same snapshot, but not the item ID, preserving the one-order-per-manifest-item constraint.
- The canonical direct/parent order uses the dedicated `manifest_shipment_label` assignment. A/B operational legs keep their existing operational labels. Label generation reads the stored snapshot without joining manifest, branch, institution, or address tables; normal deliveries keep the existing `shipment_label` path.

### Frontend integration contract

no request fields, response fields, actions, or navigation behavior change. Continue using the returned delivery exactly as before; the generated label URL now points to the manifest-specific label for newly promoted canonical manifest orders.

For each created G2G delivery order, an omitted `unit_size` with no usable measurements is resolved by `general.unit_size.settings.missing_unit_size_fallback`. The configured value may select the service minimum, half of the service maximum, the service maximum, or reject creation.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}}/destination

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |
| item_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| destination_address_id | string |  |  |  |
| delivery_service_id | string |  |  |  |
| destination_branch_id | string |  |  |  |



### Request body (raw)

```json
{
  "destination_address_id": "{{destination_address_id}}",
  "delivery_service_id": "{{delivery_service_id}}",
  "destination_branch_id": "{{destination_branch_id}}"
}
```

### Response 400 400 - Branch destination required by tenant policy

Bad Request

```json
{
  "success": false,
  "code": "manifest_destination_branch_required",
  "message": "A destination branch is required for manifest orders",
  "details": [
    {
      "field": "destination_branch_id",
      "code": "required",
      "message": "A destination branch is required for manifest orders"
    }
  ]
}
```

### Response 400 400 - Delivery service unavailable for G2G

Bad Request

```json
{
  "success": false,
  "code": "delivery_service_not_available_for_g2g",
  "message": "This delivery service is not available for G2G",
  "details": [
    {
      "field": "delivery_service_id",
      "message": "This delivery service is not available for G2G"
    }
  ]
}
```

### Response 400 Wallet Error Contract - Transfer Manifest Promotion

Bad Request

```json
{
  "success": false,
  "code": "insufficient_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/items/bulk-assign-destination — Dispatcher - Bulk Assign Destination

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-bulk-assign-destination)

Assigns the same destination and service to multiple received items, then creates delivery orders using institution wallet payment with payer_type=sender. Delivery creation uses bounded parallel processing. Successful promotions are finalized by one database operation that links deliveries, items, and barcodes and updates the manifest state and bulk event once.

### Request selectors (at least one is required)
- `item_ids`: non-empty array of transfer manifest item UUIDs.
- `barcode_values`: non-empty array of scanned or manually entered barcode strings. Values are trimmed, normalized to uppercase, and deduplicated.
- Both arrays may be sent; matching eligible items are combined and deduplicated before order creation.
- HTTP 400 validation error `item_ids_or_barcode_values_required` is returned when neither array contains a usable value.

`delivery_service_id` is optional when G2G settings has `default_delivery_service_id` configured; otherwise it is required. Send either `destination_branch_id` or `destination_address_id`. Destination branches must be active and have an address. Only items under the path manifest with status `received` and no linked delivery are eligible.

### Barcode validation follows the receiving contract
- `data.invalid_barcodes` contains one object per rejected barcode with its exact machine-readable reason: `invalid_barcode`, `unknown_barcode`, `not_part_of_this_manifest`, `wrong_branch`, `wrong_institution`, `already_used_in_another_manifest`, or `already_processed`.
- An `unknown_barcode` entry also includes localized `message`. In English it is `This barcode is not in this manifest`; the frontend must display `message` and keep `reason` only for branching.
- `data.not_in_manifest` contains every submitted barcode absent from the path transfer manifest.
- `data.not_found` is the subset of `not_in_manifest` also absent from the tenant barcode registry. Classification lists intentionally overlap; an unknown barcode appears in both lists and has an `invalid_barcodes` entry.
- `data.already_processed` contains same-manifest barcodes that are no longer eligible or already have a linked delivery. The matching `invalid_barcodes` object includes `item_status` and `delivery_id`; these barcodes are never executed again.
- `data.validation_failed_count` equals `data.invalid_barcodes.length`. `failed_count` continues to count only eligible items that reached order promotion and failed there.
- Every `data.created[]` and promotion `data.failed[]` entry includes `barcode_value`, allowing the caller to show the exact barcode as added or failed.
- Valid received barcodes are still executed when other submitted barcodes fail validation. A mixed result returns HTTP 200, `success=true`, localized `partial_success`, successful orders in `created`, and all rejected barcodes in the validation lists.
- When no selector is eligible and barcode validation failures exist, the endpoint returns HTTP 409, `success=false`, and the same complete validation lists. A malformed-only request also returns `code=invalid_barcode` with the localized `invalid_barcode` message; an already-processed-only request uses the localized already-processed message; and a not-in-manifest-only request uses localized `This barcode is not in this manifest` in English.
- Item-ID selectors retain their existing behavior. If there are no eligible item IDs and no barcode classification errors, the endpoint returns HTTP 400 `no_ready_items`.

The created orders always use the manifest processing point as their pickup location. `transfer_manifest.delivery_fee_origin` controls only the fee calculation: `processing_point` prices from that pickup location; `source_location` prices from the manifest's stored source address. If the source address is unavailable, affected items fail with `source_address_unresolved`.

Delivery creation honors the effective policy of the selected delivery service, including pricing fallback, payment policy, and warehouse staging policy. For a staged Pickup and Delivery or Delivery order with no package items, the backend creates one fallback warehouse inventory item named from the delivery code and customer name. Non-delivery service types can still fail with `warehouse_staging_only_supported_for_pickup_and_delivery`. Each staged P&D promotion commits its parent, A/B legs, inventory item, receiving order, and sequence plan atomically; one failed item does not leave a promotable partial order, and retries will not accept a historical staged parent without a complete A/B sequence plan. Automatically allocated inventory item codes skip codes already claimed manually.

### Event

a bulk `items_destination_bulk_set` event is recorded only for successfully promoted items, with each item's `barcode_value` in the payload.

### Frontend integration contract
- For scan or manual-code entry, submit selected codes in `barcode_values`; `item_ids` remains supported for existing selection flows.
- Add only `data.created` to the successfully added/executed UI state and use each entry's `barcode_value` to show that it was added. Never add `data.already_processed` again; show those barcodes as already processed.
- Show `data.invalid_barcodes[].message` when present; never render `data.invalid_barcodes[].reason` as user-facing text. Continue using `reason` as the stable machine code.
- Show `data.not_in_manifest`, `data.not_found`, and `data.invalid_barcodes[]` as per-barcode failures.
- On HTTP 200 with the localized partial-success message, apply `data.created` and retain rejected barcodes for correction. On HTTP 409, no submitted selector was executed.
- Keep the existing destination and delivery-service fields. No navigation change is required.

### Manifest origin and label behavior
- The operational pickup remains the manifest processing point; receiver/dropoff behavior is unchanged.
- At order creation, the backend stores `source_transfer_manifest_meta.snapshot_version=manifest_origin_v1` with the manifest number, item barcode, source institution ID/name, item source branch ID/name/code, cleaned branch address, branch phone when available, processing-point ID, and existing pickup/pricing audit fields. The source comes from each manifest item, not from the processing point.
- A warehouse-staged parent stores the manifest ID, manifest item ID, and snapshot. Its inbound/outbound A/B legs store the manifest ID and the same snapshot, but not the item ID, preserving the one-order-per-manifest-item constraint.
- The canonical direct/parent order uses the dedicated `manifest_shipment_label` assignment. A/B operational legs keep their existing operational labels. Label generation reads the stored snapshot without joining manifest, branch, institution, or address tables; normal deliveries keep the existing `shipment_label` path.

### Frontend integration contract

no request fields, response fields, actions, or navigation behavior change. Continue using the returned delivery exactly as before; the generated label URL now points to the manifest-specific label for newly promoted canonical manifest orders.

For each created G2G delivery order, an omitted `unit_size` with no usable measurements is resolved by `general.unit_size.settings.missing_unit_size_fallback`. The configured value may select the service minimum, half of the service maximum, the service maximum, or reject creation.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items/bulk-assign-destination

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcode_values | array<string> |  |  |  |
| destination_address_id | string |  |  |  |
| delivery_service_id | string |  |  |  |
| destination_branch_id | string |  |  |  |



### Request body (raw)

```json
{
  "barcode_values": [
    "{{barcode_value}}"
  ],
  "destination_address_id": "{{destination_address_id}}",
  "delivery_service_id": "{{delivery_service_id}}",
  "destination_branch_id": "{{destination_branch_id}}"
}
```

### Response 400 400 - Branch destination required by tenant policy

Bad Request

```json
{
  "success": false,
  "code": "manifest_destination_branch_required",
  "message": "A destination branch is required for manifest orders",
  "details": [
    {
      "field": "destination_branch_id",
      "code": "required",
      "message": "A destination branch is required for manifest orders"
    }
  ]
}
```

### Response 400 400 - Delivery service unavailable for G2G

Bad Request

```json
{
  "success": false,
  "code": "delivery_service_not_available_for_g2g",
  "message": "This delivery service is not available for G2G",
  "details": [
    {
      "field": "delivery_service_id",
      "message": "This delivery service is not available for G2G"
    }
  ]
}
```

### Response 200 Wallet Error Contract - Transfer Manifest Bulk Promotion

OK

```json
{
  "success": false,
  "message": "Bulk manifest item execution completed",
  "data": {
    "created_count": 0,
    "failed_count": 1,
    "failed": [
      {
        "item_id": "item-uuid",
        "error": "insufficient_balance",
        "code": "insufficient_balance",
        "error_message": "Customer has reached the Minimum Wallet Balance limit.",
        "barcode_value": "G2G-ITEM-001"
      }
    ]
  }
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}}/change-destination — Dispatcher - Change Manifest Item Destination

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-change-manifest-item-destination)

Changes the destination branch of one existing delivery order linked to an `order_created` manifest item. Requires `manage:operations:tenant`, `manage:module:delivery`, or system-admin access.

The existing delivery is updated in place through the canonical delivery update engine; no second order is created. Pending, assigned, picked-up, and in-transit orders may be changed. Delivered orders return `delivery_already_delivered`; returned or cancelled orders return `manifest_destination_change_not_allowed`. The database repeats the status/link/branch checks under deterministic row locks, so a concurrent completion or stale request cannot change a completed order.

A real change recalculates the destination zone and fee, reconciles wallet payment state, updates the staged outbound leg when present, synchronizes the exact branch/address/contact snapshot to the manifest item, and marks an existing shipment label `needs_reprint`. It appends the normal `order_edited` event plus an atomic manifest `item_destination_changed` event containing actor, barcode, old/new branch snapshots, old/new address IDs, and delivery status. Repeating an already-applied identical change returns success without another event.

### Success

HTTP 200 with `data.item_id`, `barcode_value`, `delivery_id`, `delivery_code`, `delivery_status`, `destination_branch_id`, `destination_address_id`, and `shipment_label_status`.

### Frontend integration contract

use the existing institution/branch picker, send the branch ID, and keep the current delivery service selected. Display the localized `message`; branch on stable `code`. After success, use the returned IDs/status and refetch the Manifest Items page so its destination snapshot and action flag match the committed order.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}}/change-destination

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |
| item_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| destination_branch_id | string | Yes | required active tenant branch UUID. The branch must have a registered address. Its address and merged address/branch contact details become the delivery destination snapshot. |  |
| delivery_service_id | string | No | optional compatibility field for the reused picker. When supplied it must equal the order's current service; changing service or delivery structure from this action is rejected. |  |
| reason | string |  | optional trimmed edit reason, maximum 500 characters; required when the tenant delivery-order setting requires edit reasons. |  |



### Request body (raw)

```json
{
  "destination_branch_id": "{{destination_branch_id}}",
  "delivery_service_id": "{{delivery_service_id}}",
  "reason": "Corrected destination branch"
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/items/bulk-change-destination — Dispatcher - Bulk Change Manifest Item Destination

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-bulk-change-manifest-item-destination)

Changes one registered destination branch for up to 200 selected `order_created` manifest items. Authentication, permission, branch, service-compatibility, status, pricing, wallet, staged-order, label-reprint, exact-branch, audit, and retry rules are the same as the single endpoint. Duplicate `item_ids` are accepted once and results preserve first-seen request order.

### Performance

the handler reads the selected manifest items in one tenant/manifest-scoped query, reads their linked deliveries in one primary-key batch, resolves the destination branch/address once, caches effective service policies and identical pricing inputs, then performs at most four atomic item updates concurrently. Each item remains its own transaction so mixed eligibility returns truthful partial success without rolling back valid corrections.

### Success/partial success

HTTP 200 when at least one item succeeds. `data` contains `requested_count`, `succeeded_count`, `failed_count`, ordered `succeeded[]`, and ordered `failed[]`. Every failed entry contains `item_id`, stable `code`, and localized `message`. A mixed result uses the localized partial-success message. HTTP 409 is returned when no selected item can be changed.

### Frontend integration contract

show the bulk action when at least one selected row has `can_change_destination=true`. Send the selected manifest item IDs and one branch ID. Keep the current delivery service selected; a different service returns `manifest_destination_service_change_not_supported`. Display `failed[].message`, branch on `failed[].code`, and show the returned succeeded/failed counts. Refetch the Manifest Items page after any success.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items/bulk-change-destination

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| item_ids | array<string> |  |  |  |
| destination_branch_id | string |  |  |  |
| delivery_service_id | string |  |  |  |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "item_ids": [
    "{{item_id}}"
  ],
  "destination_branch_id": "{{destination_branch_id}}",
  "delivery_service_id": "{{delivery_service_id}}",
  "reason": "Corrected destination branch"
}
```

## GET /delivery/transfer-manifest-exception-reasons — Dispatcher - List Exception Reasons

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-dispatcher-list-exception-reasons)

Returns the tenant-configured Transfer Manifest exception reasons for frontend dropdowns. Values come from `tenant_settings.setting_key = transfer_manifest`, field `exception_reasons`, filtered to backend-supported canonical values.

- Requires `Authorization: Bearer {{access_token}}`.

### Successful response
- `data.reasons` contains the tenant-enabled `{ value, label, description }` options. Use `reason.value` as `exception_reason`.

### Frontend integration contract
- Populate Mark Exception dropdown options from `data.reasons`.
- Submit only `reason.value` as `exception_reason` to the single or bulk exception endpoint.
- On HTTP 400, show the backend validation message; do not display it as unauthorized.

### Important errors
- HTTP 400 when tenant context is missing.
- HTTP 401 when authentication is missing or invalid.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-exception-reasons

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## POST /delivery/transfer-manifests/{{manifest_id}}/items/bulk-exception — Dispatcher - Bulk Flag Item Exceptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-bulk-flag-item-exceptions)

Moves one or more eligible manifest items to exception by barcode value. Authentication is required.

### Backend-supported reasons

missing_at_receipt, duplicate_barcode, barcode_unreadable, wrong_branch_or_institution, damaged, opened_envelope, restricted_destination, destination_unknown, manual_hold, other. The tenant-enabled subset comes from `transfer_manifest.exception_reasons`.

### Eligibility and duplicate protection
- Only items currently in `scanned`, `picked_up`, `in_transit`, `received`, or `processing` can be newly flagged.
- Items already in `exception`, `order_created`, `void`, or another ineligible status are never updated or audited again.
- The conditional database update repeats the eligible-status rule. A concurrent duplicate request is returned as already processed rather than being added twice.
- Only rows actually changed appear in `data.flagged`, update their institution barcode, and appear in the `item_exception` event.

### Per-barcode validation
- `data.invalid_barcodes` contains each rejected barcode with its exact reason: `invalid_barcode`, `unknown_barcode`, `not_part_of_this_manifest`, `wrong_branch`, `wrong_institution`, `already_used_in_another_manifest`, or `already_processed`.
- `data.not_in_manifest` contains every submitted barcode absent from the path transfer manifest.
- `data.not_found` is the subset of `not_in_manifest` also absent from the tenant barcode registry. Lists intentionally overlap where useful.
- `data.already_processed` contains same-manifest barcodes that cannot be added again. `data.skipped` remains the backward-compatible duplicate list and contains the same rejected duplicate/concurrent values.
- Malformed-barcode failures use localized `invalid_barcode`; an already-processed-only request uses localized `item_already_processed`; and a not-in-manifest-only request uses the localized not-in-manifest message.

### Partial and error behavior
- Valid eligible barcodes are committed even when other submitted barcodes fail validation.
- A mixed result returns HTTP `200`, `success=true`, localized `partial_success`, newly added barcodes in `data.flagged`, and rejected barcodes in the validation lists.
- When no barcode is newly flagged and validation failures remain, the endpoint returns HTTP `409`, `success=false`. A malformed-only request also returns `code=invalid_barcode`.
- A fully successful request returns HTTP `200`, `success=true`, localized `item_added_successfully`.
- `data.flagged_count` always equals `data.flagged.length`; `data.validation_failed_count` equals `data.invalid_barcodes.length`.

### Event

one bulk `item_exception` event is recorded only when at least one item is newly flagged. Its barcode list and count contain only newly changed items.

### Frontend integration contract
- Use `GET /delivery/transfer-manifest-exception-reasons` for the reason dropdown.
- Submit barcode strings in `barcode_values` and the selected reason `value` in `exception_reason`.
- Add only `data.flagged` to exception state and show those barcodes as added.
- Never add `data.already_processed` or `data.skipped` again.
- Show `data.invalid_barcodes`, `data.not_in_manifest`, and `data.not_found` as per-barcode failures.
- On partial HTTP 200, apply `data.flagged` and retain rejected barcodes. On HTTP 409, no submitted barcode was added. No navigation change is required.

### Important errors
- HTTP 400 for an invalid body, a reason not enabled for the tenant, or missing tenant context.
- HTTP 404 when the manifest is not found.
- HTTP 409 when every submitted barcode fails validation or was already processed.
- HTTP 401 when authentication is missing or invalid.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items/bulk-exception

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcode_values | array<string> | Yes | required non-empty array of barcode strings. Values are trimmed, normalized to uppercase, and deduplicated. |  |
| exception_reason | string | Yes | required tenant-enabled canonical reason value. |  |
| exception_notes | string | No | optional note, max 2000 characters. |  |



### Request body (raw)

```json
{
  "barcode_values": [
    "{{barcode_value}}"
  ],
  "exception_reason": "missing_at_receipt",
  "exception_notes": "Item was not in the physical handoff"
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}}/exception — Dispatcher - Flag Item Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-flag-item-exception)

Moves one open manifest item to exception.

### Backend-supported values

missing_at_receipt, duplicate_barcode, barcode_unreadable, wrong_branch_or_institution, damaged, opened_envelope, restricted_destination, destination_unknown, manual_hold, other. The tenant-enabled subset comes from `transfer_manifest.exception_reasons`.

### Duplicate-processing rules
- Only items currently in `scanned`, `picked_up`, `in_transit`, `received`, or `processing` can be flagged.
- An item already in `exception` or `void` returns HTTP `409` with the localized `item_already_processed` message. An item in `order_created` returns HTTP `409` with `order_already_created`.
- The database update repeats the eligible-status condition. If another request processes the item first, this request returns HTTP `409` and does not update the barcode or record a duplicate event.

### Successful response
- HTTP `200` with `{ success: true, message: <localized item_added_successfully> }`; English is `Item added successfully`.
- The stored `transfer_manifest_items.exception_reason` is the submitted canonical value.

### Event

one `item_exception` event is recorded only after the item status and institution barcode are successfully updated.

### Frontend integration contract
- Use `GET /delivery/transfer-manifest-exception-reasons` to populate the reason dropdown.
- Submit selected reason `value` as `exception_reason`.
- Show the returned localized `Item added successfully` message after HTTP `200`.
- On HTTP `409`, do not add or execute the item again; show the returned already-processed/order-created message. No navigation change is required.

### Important errors
- HTTP 400 for invalid body, reason not enabled for the tenant, or missing tenant context.
- HTTP 404 when the manifest or item is not found.
- HTTP 409 when the item has already been processed.
- HTTP 401 when authentication is missing or invalid.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}}/exception

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |
| item_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| exception_reason | string | Yes | required tenant-enabled canonical reason value. |  |
| exception_notes | string | No | optional note, max 2000 characters. |  |



### Request body (raw)

```json
{
  "exception_reason": "manual_hold",
  "exception_notes": "Held for manual review"
}
```

## GET /delivery/transfer-manifest-exceptions — Exceptions - List Unresolved Transfer Manifest Exceptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-exceptions-list-unresolved-transfer-manifest-exceptions)

Lists manifest items currently in exception status.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-exceptions?page=1&limit=20&manifest_id={{manifest_id}}&exception_reason=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| manifest_id | query string | No | Optional manifest id | {{manifest_id}} |
| exception_reason | query string | No | Optional reason filter |  |



## POST /delivery/transfer-manifest-exceptions/{{item_id}}/resolve — Exceptions - Resolve Exception As Reprocess

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-exceptions-resolve-exception-as-reprocess)

Resolves an exception by returning the item to `received` so destination/order creation can continue. When the resolved item is the last remaining exception, a manifest currently in `received_with_exceptions` automatically returns to `received`. If every manifest item is then `order_created` or `void`, the manifest automatically becomes `completed`.

### Events

an `exception_resolved` event is recorded with `payload.barcode_value`, `payload.resolution`, and `payload.notes`; the manifest status correction records `manifest_status_changed` with `payload.reason=all_exceptions_resolved`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-exceptions/{{item_id}}/resolve

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| item_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| resolution | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "resolution": "reprocess",
  "notes": "Corrected and ready for processing"
}
```

## POST /delivery/transfer-manifest-exceptions/{{item_id}}/resolve — Exceptions - Resolve Exception As Void

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-exceptions-resolve-exception-as-void)

Resolves an exception by voiding the item/barcode. If the voided item is the last remaining exception, a manifest currently in `received_with_exceptions` automatically returns to `received` when other non-terminal items remain. If every manifest item is `order_created` or `void`, the manifest automatically becomes `completed`.

### Events

an `exception_resolved` event is recorded with `payload.barcode_value`, `payload.resolution`, and `payload.notes`; manifest status changes are recorded as `manifest_status_changed`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-exceptions/{{item_id}}/resolve

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| item_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| resolution | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "resolution": "void",
  "notes": "Voided after review"
}
```

## GET /delivery/transfer-manifests/{{manifest_id}}/events — Events - Manifest Timeline

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#get-events-manifest-timeline)

Returns append-only event history for a manifest. Expected happy-path events include manifest_created, item_scanned, manifest_picked_up, manifest_arrived, receiving_started, item_received, items_received, receiving_completed, order_created, manifest_status_changed, and manifest_completed. Each `item_received` and `order_created` event includes `manifest_item_id`, `from_status`, and `to_status`, preserving the item sequence before order creation. Other item-level events (item_destination_set, item_exception, exception_resolved, item_scanned, item_removed) include `barcode_value` in the payload. New `item_scanned` events include `payload.entry_method` as `manual` or `camera`; historical events without this field must be treated as `camera`. User-driven `item_received` and aggregate `items_received` events also include the submitted entry method, while automated count-only or bulk receipt events can omit it. Frontend integration: render `Manual Entry` only when an `item_scanned` event has `payload.entry_method=manual`; otherwise render `Scanned`. Exception events include `reason` and `notes` in the payload. The automatic status correction after the last exception is resolved uses `manifest_status_changed` with `from_status=received_with_exceptions`, `to_status=received`, and `payload.reason=all_exceptions_resolved`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/events?page=1&limit=50

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 50 |



## POST /delivery/transfer-manifests/{{manifest_id}}/cancel — Dispatcher - Cancel Manifest

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-cancel-manifest)

Cancels a manifest before receiving/order creation. Allowed only while status is `draft`, `picked_up`, `in_transit`, or `awaiting_receipt`. Once receiving has started (`receiving`, `received`, or `received_with_exceptions`), the API returns HTTP 409 `manifest_cannot_be_cancelled`. Non-order-created items become void and linked barcodes return to available.

The stored manifest notes use `note` when supplied, otherwise `reason`. Successful response: `data` contains `{ id, status: "cancelled", reason, note, notes }`, where `notes` is the exact value stored on `transfer_manifests.notes`. Event: a `manifest_cancelled` event is recorded with `payload.reason` and `payload.note`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/cancel

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| reason | string | Yes | Reason for cancellation; trimmed and limited to 2000 characters. |  |
| note | string | No | Additional cancellation note, limited to 2000 characters. |  |



### Request body (raw)

```json
{
  "reason": "Duplicate pickup manifest",
  "note": "Cancelled before receiving started"
}
```

### Response 200 Success

OK

```json
{
  "success": true,
  "message": "updated_successfully",
  "data": {
    "id": "manifest-uuid",
    "status": "cancelled",
    "reason": "Duplicate pickup manifest",
    "note": "Cancelled before receiving started",
    "notes": "Cancelled before receiving started"
  }
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}}/void — Dispatcher - Void Manifest Item (Remove from Manifest)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-void-manifest-item-remove-from-manifest)

Voids (removes) a manifest item with a required reason. Works on non-draft manifests for items not yet order_created or void. The item's barcode is freed back to available. An individual `item_voided` event is recorded on the manifest timeline.

### Allowed item statuses

any except `order_created` and `void`.

### Errors
- `item_not_found` 404
- `order_already_created` 409
- `item_already_processed` 409

### Event

`item_voided` with `payload.reason` and `payload.barcode_value`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/items/{{item_id}}/void

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |
| item_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "reason": "Item damaged during processing"
}
```

## POST /delivery/transfer-manifests/{{manifest_id}}/complete — Dispatcher - Complete Manifest (Mark as Completed)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/institution-transfer-manifest-v1#post-dispatcher-complete-manifest-mark-as-completed)

Manually marks a manifest as completed. Automatic completion never occurs while any item remains in `exception`; resolve the exception by reprocessing or voiding it first. If exception items remain during this explicit manual action, each is voided individually with its own `item_voided` event entry (who, when, why) before the manifest transitions to `completed`. If all items are already terminal (`order_created` or `void`), the manifest completes directly.

### Allowed manifest statuses

`received_with_exceptions` and `processing`.

### Guard

Returns 409 if any non-terminal, non-exception items exist (e.g., `received` items that haven't been processed yet).

### Event

`manifest_completed` with `payload.voided_count` and `payload.reason`. Each voided exception item gets an individual `item_voided` event.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifests/{{manifest_id}}/complete

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "reason": "All items resolved or processed"
}
```

## GET /delivery/transfer-manifest-items/search — Search Manifest Items

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/g2g-transfer-manifest-items-bulk-scan#get-search-manifest-items)

Tenant-wide manifest item search for autocomplete / barcode scanner input.

Returns only unpromoted post-pickup items with status `picked_up`, `in_transit`, or `received` and no existing `delivery_id`. Excludes `scanned`, `processing`, `order_created`, and `void` items. Returns enriched rows including manifest context (manifest_number, manifest_status), institution user, and source branch.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-items/search?q=BC-001&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| q | query string |  | Search term. If a valid UUID, performs an exact id match. Otherwise, performs a case-insensitive partial match on barcode_value. Leave empty to list recent items. | BC-001 |
| limit | query string |  | Max results to return. Range 1–100. Default 20. | 20 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Data retrieved successfully",
  "data": [
    {
      "manifest_item_id": "item-uuid",
      "barcode_value": "BC-001",
      "status": "received",
      "manifest_id": "manifest-uuid",
      "manifest_number": "MF-2026-0042",
      "manifest_status": "received",
      "institution": {
        "id": "user-uuid",
        "full_name": "City Hospital",
        "phone": "+9647001234567"
      },
      "source_branch": {
        "id": "branch-uuid",
        "name": "Main Branch",
        "code": "MB-01"
      },
      "destination_branch_id": null,
      "destination_address_id": null,
      "delivery_id": null,
      "already_has_order": false
    }
  ]
}
```

## POST /delivery/transfer-manifest-items/bulk/validate — Bulk Validate — using object identifiers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/g2g-transfer-manifest-items-bulk-scan#post-bulk-validate-using-object-identifiers)

Same as the plain-string variant but uses the `{ type, value }` object shape. The `type` field is ignored server-side; routing is determined by the value format.

### A found item is valid when it can safely enter the one-call flow

`awaiting_receipt` or `receiving` with recorded driver arrival and required delivery proof, or receipt-safe retry status `received`, `received_with_exceptions`, or `processing` with complete receipt fields and an item still in `received`. Pre-arrival, terminal, inconsistent, or already-promoted rows return `valid: false` with a stable `error_key`, localized `error_message`, manifest number, manifest status, and current item status. The overall batch remains HTTP 200 so valid and invalid identifiers can be returned together.

### Frontend integration contract

use each result's `valid` boolean. Display `error_message`; use `error_key` only for branching. Keep one Execute action.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-items/bulk/validate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| identifiers | array<object> |  |  |  |
| identifiers.type | string |  |  |  |
| identifiers.value | string |  |  |  |



### Request body (raw)

```json
{
  "identifiers": [
    {
      "type": "barcode",
      "value": "GE000010000017"
    },
    {
      "type": "id",
      "value": "item-uuid-003"
    }
  ]
}
```

### Response 200 Mixed result — manifest arrival required

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Manifest item validation completed",
  "data": {
    "results": [
      {
        "input": "GE000090000020",
        "valid": false,
        "error_key": "manifest_bulk_order_creation_not_allowed",
        "error_message": "Manifest TM-MREX5VCN-ZQD2 is picked_up. Bulk order creation is not allowed in this status.",
        "manifest_item_id": "item-uuid",
        "barcode_value": "GE000090000020",
        "manifest_id": "manifest-uuid",
        "manifest_number": "TM-MREX5VCN-ZQD2",
        "manifest_status": "picked_up",
        "current_status": "picked_up",
        "allowed_manifest_statuses": [
          "awaiting_receipt",
          "receiving",
          "received",
          "received_with_exceptions",
          "processing"
        ]
      }
    ],
    "total_valid": 0,
    "total_failed": 1
  }
}
```

## POST /delivery/transfer-manifest-items/bulk/set-destination — Bulk Set Destination & Create Orders

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/g2g-transfer-manifest-items-bulk-scan#post-bulk-set-destination-and-create-orders)

Tenant-wide one-call receiving and order-creation endpoint. It accepts manifest item IDs from any manifest under the tenant and creates delivery orders with institution wallet payment and payer_type=sender. COD is not used.

- One of `destination_branch_id` or `destination_address_id`.

### State behavior
- `draft`, `picked_up`, `in_transit`, `completed`, and `cancelled` are rejected before any receipt/order write. The localized message names the manifest and current status.
- `awaiting_receipt`: verifies `arrived_at` and required delivery photo/signature, records `receiving_started`, and fills `received_by`.
- `receiving`: does not start receiving again; it requires the existing receiver record.
- Selected `picked_up`/`in_transit` items are atomically changed to `received`, receive `received_at`, update their barcode, and get item-level `item_received` events.
- If any manifest item remains unaccounted for, that manifest stays `receiving`; its selected items remain received but no order is created yet. The response uses `manifest_receiving_incomplete`.
- When every item is accounted for, receiving is completed first (`received_at`, `received_by`, durable received count, `receiving_completed`), then orders are created.
- `received`, `received_with_exceptions`, and `processing` allow retry only for items still in `received` with complete receipt fields.
- Every promotion is protected again in the shared service and database, so `picked_up → order_created` is impossible.

### Ordering contract

`receipt.received_item_ids`, `receipt.ready_item_ids`, per-manifest `item_ids`, `created`, and `failed` follow the first-seen `item_ids` order, independent of database row order or parallel completion timing. Before bounded parallel creation starts, the backend atomically reserves parent delivery codes in that same order. Invalid or ineligible items are skipped without changing the relative order of the remaining items. Passing the returned created delivery codes to Generate Shipment Labels in response order therefore produces page 1 for the first successfully scanned item, page 2 for the next, and so on. Retries reuse an existing delivery or its durable reservation.

### Existing promotion contract preserved

every created order stores the immutable source snapshot with `snapshot_version=manifest_origin_v1`. The parent delivery stores the manifest and item IDs; A/B legs store the manifest ID and the same snapshot, but not the item ID. Canonical manifest orders use the dedicated `manifest_shipment_label` assignment; normal deliveries keep the existing `shipment_label` path. When package measurements and an explicit unit size are both absent, the shared resolver uses the configured `missing_unit_size_fallback`.

### Processing and performance

receipt is one tenant-scoped database RPC with deterministic row locks and batch writes. Receipt-safe items/manifests are then loaded with focused batch queries. One short reservation RPC allocates the full contiguous code block before creation. Up to 20 ready items use the existing bounded creation path directly. A larger request is split into ordered parts of at most 20 and sent through at most two fresh Edge invocations at once; each part limits delivery creation concurrency to two, keeping total database concurrency equivalent to the previous path while resetting the 2-second CPU budget per part. Retryable 502/503/504/546 part failures are retried up to three times. If a child committed but its response was lost, the coordinator reconciles only items durably finalized as `order_created`. The public request and response shapes do not change. There is no per-item reservation or receipt/event query. Existing origin snapshot, label, pricing-origin, delivery-policy, warehouse-staging, and missing-unit fallback behavior remains unchanged.

### Response shape

`{ created_count, failed_count, created, failed, receipt }`. `receipt` contains `requested_count`, `received_count`, `received_item_ids`, `ready_item_ids`, and per-manifest status/results.

### Frontend integration contract

keep the existing request and one Execute action. Preserve the returned `created` order when passing delivery codes to Generate Shipment Labels. Do not call Start Receiving or Receive Items separately from this screen. Display localized `message`/`failed[].error_message`; branch on stable codes such as `invalid_uuid`, `manifest_bulk_order_creation_not_allowed`, `manifest_arrival_required`, `manifest_delivery_proof_required`, `manifest_bulk_receipt_incomplete`, and `manifest_receiving_incomplete`. On an incomplete manifest, keep the user in the workflow because selected items were received but orders were intentionally not created.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-items/bulk/set-destination

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| item_ids | array<string> | Yes | required array of manifest item UUID strings. At least 1. Duplicates are deduplicated; the first occurrence defines the item's execution position. |  |
| destination_address_id | string |  |  |  |
| delivery_service_id | string | No | optional when G2G settings has `default_delivery_service_id`; otherwise required. |  |
| destination_branch_id | string |  |  |  |



### Request body (raw)

```json
{
  "item_ids": [
    "{{item_id}}"
  ],
  "destination_address_id": "{{destination_address_id}}",
  "delivery_service_id": "{{delivery_service_id}}",
  "destination_branch_id": "{{destination_branch_id}}"
}
```

### Response 200 Success — all items promoted

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Orders created successfully",
  "data": {
    "created_count": 2,
    "failed_count": 0,
    "created": [
      {
        "item_id": "item-uuid-1",
        "delivery": {
          "id": "delivery-uuid",
          "delivery_code": "2501",
          "status": "pending"
        }
      },
      {
        "item_id": "item-uuid-2",
        "delivery": {
          "id": "delivery-uuid-2",
          "delivery_code": "2502",
          "status": "pending"
        }
      }
    ],
    "failed": [],
    "receipt": {
      "requested_count": 2,
      "received_count": 2,
      "received_item_ids": [
        "item-uuid-1",
        "item-uuid-2"
      ],
      "ready_item_ids": [
        "item-uuid-1",
        "item-uuid-2"
      ],
      "manifests": [
        {
          "manifest_id": "manifest-uuid",
          "manifest_number": "TM-001",
          "status": "received",
          "remaining_count": 0,
          "ready": true
        }
      ]
    }
  }
}
```

### Response 200 Partial — order creation failed for some updated items

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Bulk manifest item operation completed",
  "data": {
    "created_count": 1,
    "failed_count": 1,
    "created": [
      {
        "item_id": "item-uuid-1",
        "delivery": {
          "id": "delivery-uuid",
          "delivery_code": "2501",
          "status": "pending"
        }
      }
    ],
    "failed": [
      {
        "item_id": "item-uuid-2",
        "error": "order_creation_failed",
        "error_message": "Failed to create delivery order"
      }
    ],
    "receipt": {
      "requested_count": 2,
      "received_count": 2,
      "received_item_ids": [
        "item-uuid-1",
        "item-uuid-2"
      ],
      "ready_item_ids": [
        "item-uuid-1",
        "item-uuid-2"
      ]
    }
  }
}
```

### Response 409 409 — manifest has not reached receiving

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "manifest_bulk_order_creation_not_allowed",
  "message": "Manifest TM-MTLH6LP5-CLM3 is picked_up. Bulk order creation is not allowed in this status.",
  "details": {
    "manifest_id": "manifest-uuid",
    "manifest_number": "TM-MTLH6LP5-CLM3",
    "status": "picked_up"
  }
}
```

### Response 400 400 — malformed manifest item ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_uuid",
  "message": "Enter a valid ID",
  "details": [
    {
      "field": "item_ids[0]",
      "code": "invalid_uuid",
      "message": "Enter a valid ID"
    }
  ]
}
```

### Response 400 400 — branch destination required by tenant policy

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "manifest_destination_branch_required",
  "message": "A destination branch is required for manifest orders",
  "details": [
    {
      "field": "destination_branch_id",
      "code": "required",
      "message": "A destination branch is required for manifest orders"
    }
  ]
}
```

### Response 400 400 — delivery service unavailable for G2G

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "delivery_service_not_available_for_g2g",
  "message": "This delivery service is not available for G2G",
  "details": [
    {
      "field": "delivery_service_id",
      "message": "This delivery service is not available for G2G"
    }
  ]
}
```

### Response 400 400 — destination missing

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation error",
  "details": [
    {
      "field": "destination_address_id",
      "message": "destination_address_or_branch_required"
    }
  ]
}
```

### Response 400 400 — delivery service not found

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery service not found"
}
```

## POST /delivery/transfer-manifest-items/bulk/set-destination — Bulk Set Destination — with address ID directly

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/g2g-transfer-manifest-items-bulk-scan#post-bulk-set-destination-with-address-id-directly)

Same one-call receipt-safe endpoint, passing `destination_address_id` directly instead of `destination_branch_id`. `delivery_service_id` can be omitted when the G2G default is configured. Driver arrival remains mandatory; the backend starts/continues receiving, records item-level receipt, completes a fully accounted manifest, and only then creates orders. Receipt-safe retries from `received`, `received_with_exceptions`, or `processing` do not duplicate receiving events. First-seen `item_ids` order controls execution results and atomically reserved delivery codes even though order creation remains parallel; invalid or ineligible items are skipped without reordering the survivors. Existing institution-wallet, manifest-origin snapshot, label, pricing-origin, delivery-policy, warehouse-staging, and missing-unit fallback behavior remains unchanged. The immutable source snapshot uses `snapshot_version=manifest_origin_v1`; A/B legs store the manifest ID and the same snapshot, but not the item ID. Canonical manifest orders use the dedicated `manifest_shipment_label` assignment, while normal deliveries keep the existing `shipment_label` path. The shared resolver uses the configured `missing_unit_size_fallback` when package measurements and an explicit unit size are absent.

### Frontend integration contract

keep the same request and one Execute action, preserve the returned `created` order when requesting labels, and use the localized receipt/order errors described on the primary example.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-items/bulk/set-destination

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| item_ids | array<string> |  |  |  |
| destination_address_id | string |  |  |  |



### Request body (raw)

```json
{
  "item_ids": [
    "item-uuid-1",
    "item-uuid-2"
  ],
  "destination_address_id": "address-uuid"
}
```

## POST /delivery/transfer-manifest-items/bulk/resolve-exceptions — Bulk Resolve Exceptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/g2g-transfer-manifest-items-bulk-scan#post-bulk-resolve-exceptions)

Bulk resolution endpoint for dispatcher exception queues. The user can resolve selected exception item IDs from `GET /delivery/transfer-manifest-exceptions`, or resolve all exception items under one manifest.

### Selected items body
- `item_ids` — array of transfer manifest item UUIDs.
- `resolution` — optional, `reprocess` or `void`. Defaults to `reprocess`.
- `notes` — optional note stored on the resolved items.

### All exceptions under manifest body
- `manifest_id` — transfer manifest UUID.
- `all_exceptions` — must be `true` to explicitly resolve every current exception under the manifest.
- `resolution` — optional, `reprocess` or `void`. Defaults to `reprocess`.
- `notes` — optional note stored on the resolved items.

### Behavior
- Only items currently in `exception` status are resolved.
- `reprocess` moves item and barcode back to `received`, clears `exception_reason`, and makes the item eligible for bulk destination/order creation.
- `void` moves item and barcode to `void`, terminally excluding it from order creation.
- When the last exception under a manifest is resolved, a manifest in `received_with_exceptions` returns to `received` if non-terminal items remain. If every item is `order_created` or `void`, the manifest becomes `completed`.
- Non-exception selected items are returned in `skipped`; missing selected IDs are returned in `not_found`.

### Events

an `exception_resolved` event is recorded per resolved item with `payload.barcode_value`, `payload.resolution`, and `payload.notes`; automatic manifest correction is recorded as `manifest_status_changed`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-items/bulk/resolve-exceptions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| item_ids | array<string> |  |  |  |
| resolution | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "item_ids": [
    "{{exception_item_id}}"
  ],
  "resolution": "reprocess",
  "notes": "Resolved from dispatcher exception queue"
}
```

### Response 200 Success — reprocess selected exceptions

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Updated successfully",
  "data": {
    "resolved_count": 2,
    "resolved": [
      {
        "item_id": "item-uuid-1",
        "from_status": "exception",
        "to_status": "received"
      },
      {
        "item_id": "item-uuid-2",
        "from_status": "exception",
        "to_status": "received"
      }
    ],
    "skipped": [],
    "not_found": []
  }
}
```

## POST /delivery/transfer-manifest-items/bulk/resolve-exceptions — Bulk Resolve All Exceptions Under Manifest

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/g2g-transfer-manifest-items-bulk-scan#post-bulk-resolve-all-exceptions-under-manifest)

Resolves every current exception item under one transfer manifest. Requires `all_exceptions: true` so the client cannot accidentally resolve a whole manifest when it meant to send selected item IDs. When this resolves the last exception, a manifest in `received_with_exceptions` returns to `received` if non-terminal items remain, or becomes `completed` when every item is `order_created` or `void`.

### Events

an `exception_resolved` event is recorded per resolved item with `payload.barcode_value`, `payload.resolution`, and `payload.notes`; automatic manifest correction is recorded as `manifest_status_changed`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/transfer-manifest-items/bulk/resolve-exceptions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| manifest_id | string |  |  |  |
| all_exceptions | boolean |  |  |  |
| resolution | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "manifest_id": "{{manifest_id}}",
  "all_exceptions": true,
  "resolution": "void",
  "notes": "Void all unresolved exceptions under this manifest"
}
```

## GET /delivery/customer-portal/custom-domain — Get Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-portal-custom-domain#get-get-custom-domain)

Returns the current customer portal custom domain for the authenticated tenant, or `null` if none is connected.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/delivery/customer-portal/custom-domain?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional if in auth token) |  |
| language | query string |  |  | en |



### Response 200 Success - Active Domain

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Customer portal custom domain retrieved successfully",
  "data": {
    "custom_domain": {
      "id": "uuid",
      "tenant_id": "uuid",
      "custom_domain": "portal.tenant.com",
      "vercel_domain_id": "dom_xxx",
      "status": "active",
      "dns_records": [
        {
          "type": "CNAME",
          "name": "portal",
          "value": "cname.vercel-dns.com",
          "ttl": null,
          "purpose": "required"
        }
      ],
      "dns_verified_at": "2026-04-30T10:00:00Z",
      "connected_by": "uuid",
      "disconnected_by": null,
      "disconnected_at": null,
      "created_at": "2026-04-30T09:55:00Z",
      "updated_at": "2026-04-30T10:00:00Z"
    }
  }
}
```

### Response 200 Success - No Domain

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Customer portal custom domain retrieved successfully",
  "data": {
    "custom_domain": null
  }
}
```

### Response 403 Forbidden

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions",
  "data": null
}
```

## POST /delivery/customer-portal/custom-domain/connect — Connect Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-portal-custom-domain#post-connect-custom-domain)

Connects a new custom domain to the customer portal. Adds the domain to Vercel and stores the DNS records the tenant must publish.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/customer-portal/custom-domain/connect?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  |  |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| custom_domain | string |  |  |  |



### Request body (raw)

```json
{
  "custom_domain": "portal.tenant.com"
}
```

### Response 201 Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Customer portal custom domain connected successfully",
  "data": {
    "custom_domain": {
      "id": "uuid",
      "tenant_id": "uuid",
      "custom_domain": "portal.tenant.com",
      "vercel_domain_id": "dom_xxx",
      "status": "pending_dns",
      "dns_records": [
        {
          "type": "CNAME",
          "name": "portal",
          "value": "cname.vercel-dns.com",
          "ttl": null,
          "purpose": "required"
        }
      ],
      "dns_verified_at": null,
      "connected_by": "uuid",
      "disconnected_by": null,
      "disconnected_at": null,
      "created_at": "2026-04-30T09:55:00Z",
      "updated_at": "2026-04-30T09:55:00Z"
    }
  }
}
```

### Response 409 Already Exists

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This tenant already has a customer portal custom domain",
  "data": null
}
```

### Response 409 Domain Already In Use

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Custom domain already in use",
  "data": null
}
```

### Response 400 Invalid Domain

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid custom domain",
  "data": null
}
```

## POST /delivery/customer-portal/custom-domain/verify — Verify Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-portal-custom-domain#post-verify-custom-domain)

Asks Vercel to re-check DNS for the connected custom domain. If verified, the record transitions to `active`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/delivery/customer-portal/custom-domain/verify?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  |  |
| language | query string |  |  | en |



### Response 200 Verified

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Customer portal custom domain verified successfully",
  "data": {
    "verified": true,
    "custom_domain": {
      "id": "uuid",
      "tenant_id": "uuid",
      "custom_domain": "portal.tenant.com",
      "vercel_domain_id": "dom_xxx",
      "status": "active",
      "dns_verified_at": "2026-04-30T10:00:00Z"
    }
  }
}
```

### Response 200 Pending

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Customer portal custom domain DNS verification is still pending",
  "data": {
    "verified": false,
    "custom_domain": {
      "id": "uuid",
      "status": "pending_dns"
    }
  }
}
```

### Response 404 Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Customer portal custom domain not found",
  "data": null
}
```

## DELETE /delivery/customer-portal/custom-domain — Disconnect Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/customer-portal-custom-domain#delete-disconnect-custom-domain)

Removes the custom domain from Vercel and archives the local record. Tenant can connect a new domain afterwards.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/delivery/customer-portal/custom-domain?tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  |  |
| language | query string |  |  | en |



### Response 200 Disconnected

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Customer portal custom domain disconnected successfully",
  "data": {
    "custom_domain": {
      "id": "uuid",
      "status": "archived",
      "disconnected_by": "uuid",
      "disconnected_at": "2026-04-30T11:00:00Z"
    }
  }
}
```

### Response 404 Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Customer portal custom domain not found",
  "data": null
}
```

## POST /generate-pdf — Generate Shipment Label Batch (Internal)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/shipment-label-batch-internal#post-generate-shipment-label-batch-internal)

This contract applies only to the internal `record.delivery_ids` batch branch. Existing single-record PDF webhook branches are unchanged. `record.delivery_ids` must be an array of 1 to 50 distinct non-empty strings, each at most 100 characters. Optional record fields: `actor_id`, `source` (defaults to `system`), `correlation_id` (1-128 letters/digits/dot/underscore/colon/hyphen), `expected_label_versions` (delivery-ID to version map), and `deadline` (Unix milliseconds). HTTP 200 returns per-delivery outcomes and can have `success: false`; inspect each result. A reached deadline yields an in-progress per-item result without starting that item.

Internal backend/cron only. Send `apikey: {{supabase_secret_key}}` with the default managed Supabase secret key (`SUPABASE_SECRET_KEYS.default`). Never put this value in a frontend or mobile application. Missing, invalid, publishable, legacy anon, and legacy service-role bearer credentials return HTTP 401 before work is dispatched. `Authorization: Bearer ...` alone is not accepted. Frontend handoff: none; application clients must not call this internal endpoint.

Authentication: Internal backend or cron only. Send the managed Supabase secret in apikey. Application clients must not call this endpoint.

Request: POST https://staging-api.ondi.io/functions/v1/generate-pdf

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| apikey | header string |  | Default managed Supabase secret key; backend only. | {{supabase_secret_key}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| record | object |  |  |  |
| record.delivery_ids | array<string> |  |  |  |
| record.actor_id | string |  |  |  |
| record.source | string |  |  |  |
| record.correlation_id | string |  |  |  |



### Request body (raw)

```json
{
  "record": {
    "delivery_ids": [
      "{{delivery_id}}"
    ],
    "actor_id": "{{actor_id}}",
    "source": "bulk-labels",
    "correlation_id": "internal-label-batch"
  }
}
```

### Response 200 200 Batch completed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "delivery_count": 1,
  "success_count": 1,
  "failed_count": 0,
  "results": [
    {
      "delivery_id": "{{delivery_id}}",
      "success": true,
      "status": 200
    }
  ]
}
```

### Response 400 400 Non-array delivery IDs

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "delivery_ids_required"
}
```

### Response 400 400 Invalid batch

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_delivery_ids",
  "batch_limit": 50
}
```

### Response 400 400 Duplicate delivery IDs

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "duplicate_delivery_ids"
}
```

### Response 500 500 Invalid JSON or unexpected failure

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "internal_server_error"
}
```

### Response 401 401 Missing or invalid internal secret

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "unauthorized"
}
```

## GET /provider/providers — List providers (grouped)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#get-list-providers-grouped)

Read the system’s integration provider configuration, grouped by provider category.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/provider/providers

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



## GET /provider/providers — List providers by category (AI)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#get-list-providers-by-category-ai)

Read the configured AI providers when choosing or managing an AI integration.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/provider/providers?category=ai

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| category | query string |  |  | ai |



## GET /provider/providers — List providers by category (SMS)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#get-list-providers-by-category-sms)

Read the configured SMS providers when choosing or managing message delivery.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/provider/providers?category=sms

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| category | query string |  |  | sms |



## GET /provider/providers/gemini — Get provider by key

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#get-get-provider-by-key)

Read the configuration of one system integration provider by its provider key.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/provider/providers/gemini

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



## PATCH /provider/providers/gemini — Patch provider (update Gemini)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#patch-patch-provider-update-gemini)

### Requires a user JWT with manage

providers:system. API keys and callers without permission receive 403 before body parsing or database access. Accepts only category, display_name, icon_url, is_active, api_key, config, and description. Category must match the provider key. Config fields and HTTPS destination hosts are validated per provider. Success returns 200 {success:true,data}. Important errors: 400 invalid request/config/destination, 403 rbac_forbidden, 404 provider_not_found, 500 internal_server_error.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/provider/providers/gemini

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| display_name | string |  |  |  |
| is_active | boolean |  |  |  |
| api_key | string |  |  |  |
| config | object |  |  |  |
| config.model | string |  |  |  |



### Request body (raw)

```json
{
  "display_name": "Google Gemini",
  "is_active": true,
  "api_key": "REPLACE_ME",
  "config": {
    "model": "gemini-1.5-pro"
  }
}
```

### Response 200 Success

OK

```json
{
  "success": true,
  "data": {
    "key": "gemini",
    "category": "ai",
    "display_name": "Google Gemini",
    "is_active": true,
    "api_key": "REPLACE_ME",
    "config": {
      "model": "gemini-1.5-pro"
    }
  }
}
```

### Response 403 Forbidden

Forbidden

```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 400 Invalid destination

Bad Request

```json
{
  "success": false,
  "code": "provider_invalid_destination",
  "message": "Provider destination is not allowed."
}
```

## GET /provider/providers/twilio — Get SMS provider (Twilio)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#get-get-sms-provider-twilio)

Read the Twilio provider configuration used by the SMS integration.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/provider/providers/twilio

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



## PATCH /provider/providers/twilio — Update Twilio SMS provider

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#patch-update-twilio-sms-provider)

### Same PATCH authorization contract

user JWT plus manage:providers:system. Twilio baseUrl must use HTTPS on api.twilio.com. Unsupported fields, config values, and destinations are rejected before the single update query.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/provider/providers/twilio

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_active | boolean |  |  |  |
| api_key | string |  |  |  |
| config | object |  |  |  |
| config.baseUrl | string |  |  |  |
| config.accountSid | string |  |  |  |
| config.authToken | string |  |  |  |
| config.messagingServiceSid | string |  |  |  |



### Request body (raw)

```json
{
  "is_active": true,
  "api_key": "",
  "config": {
    "baseUrl": "https://api.twilio.com/2010-04-01",
    "accountSid": "AC...",
    "authToken": "...",
    "messagingServiceSid": "MG..."
  }
}
```

## PATCH /provider/providers/verifyway — Update Verifyway SMS provider

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#patch-update-verifyway-sms-provider)

### Same PATCH authorization contract

user JWT plus manage:providers:system. Verifyway baseUrl must use HTTPS on gateway.standingtech.com and apiPath must be a relative path beginning with /. Unsupported values are rejected before update.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/provider/providers/verifyway

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_active | boolean |  |  |  |
| api_key | string |  |  |  |
| config | object |  |  |  |
| config.baseUrl | string |  |  |  |
| config.apiPath | string |  |  |  |
| config.senderId | string |  |  |  |
| config.defaultLang | string |  |  |  |



### Request body (raw)

```json
{
  "is_active": true,
  "api_key": "your_verifyway_api_key",
  "config": {
    "baseUrl": "https://gateway.standingtech.com/api/v5",
    "apiPath": "/sms/send",
    "senderId": "YourBrand",
    "defaultLang": "en"
  }
}
```

## PATCH /provider/providers/otpiq — Update Otpiq SMS provider

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#patch-update-otpiq-sms-provider)

### Same PATCH authorization contract

user JWT plus manage:providers:system. OtpIQ baseUrl must use HTTPS on api.otpiq.com. Arbitrary webhook destinations are rejected; webhookUrl remains empty until an approved callback host is defined.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/provider/providers/otpiq

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_active | boolean |  |  |  |
| api_key | string |  |  |  |
| config | object |  |  |  |
| config.baseUrl | string |  |  |  |
| config.defaultProvider | string |  |  |  |
| config.senderId | string |  |  |  |
| config.webhookUrl | string |  |  |  |
| config.webhookType | string |  |  |  |
| config.webhookSecret | string |  |  |  |



### Request body (raw)

```json
{
  "is_active": true,
  "api_key": "your_otpiq_api_key",
  "config": {
    "baseUrl": "https://api.otpiq.com/api",
    "defaultProvider": "sms",
    "senderId": "YourBrand",
    "webhookUrl": "",
    "webhookType": "all",
    "webhookSecret": ""
  }
}
```

## GET /provider/ai/gemini/models — List Gemini models

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/providers#get-list-gemini-models)

Load Google’s model list using the active Gemini provider key stored in OnDi. The caller does not supply a Gemini API key.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/provider/ai/gemini/models

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



## POST /feedback/submit — Submit Feedback

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/feedback#post-submit-feedback)

Submit a feedback report with its type, severity, description and optional attachments or page context.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/feedback/submit?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| type | string |  |  |  |
| severity | string |  |  |  |
| description | string |  |  |  |
| attachments | array<string> |  |  |  |
| source_panel | string |  |  |  |
| path | string |  |  |  |
| locale | string |  |  |  |
| timezone | string |  |  |  |



### Request body (raw)

```json
{
  "type": "BugReport",
  "severity": "Normal",
  "description": "Steps to reproduce...",
  "attachments": [
    "https://cdn.example.com/feedback/img-1.png",
    "https://cdn.example.com/feedback/vid-1.mp4"
  ],
  "source_panel": "admin",
  "path": "/dashboard",
  "locale": "en",
  "timezone": "UTC"
}
```

## GET /feedback/feedbacks — List Feedbacks

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/feedback#get-list-feedbacks)

Browse submitted feedback by tenant, status, type, severity or date range.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/feedback/feedbacks?tenant_id={{tenant_id}}&status=&type=&severity=&from=&to=&page=1&limit=25

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| status | query string |  |  |  |
| type | query string |  |  |  |
| severity | query string |  |  |  |
| from | query string |  |  |  |
| to | query string |  |  |  |
| page | query string |  |  | 1 |
| limit | query string |  |  | 25 |



## GET /feedback/feedbacks/:id — Get Feedback

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/feedback#get-get-feedback)

Open the details of a submitted feedback report for review.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/feedback/feedbacks/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | uuid |



## PATCH /feedback/feedbacks/:id — Update Feedback Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/feedback#patch-update-feedback-status)

Change a feedback report’s review status as it moves through the support process.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/feedback/feedbacks/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "status": "Reviewed"
}
```

## GET /feedback/feedback-settings — Get Feedback Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/feedback#get-get-feedback-settings)

Read which feedback events send email notifications and which system administrators receive them.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/feedback/feedback-settings

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## PUT /feedback/feedback-settings — Update Feedback Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/feedback#put-update-feedback-settings)

Configure notification emails for received and resolved feedback, including system administrator recipients.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/feedback/feedback-settings

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| send_on_received | boolean |  |  |  |
| send_on_resolved | boolean |  |  |  |
| send_to_system_admin | boolean |  |  |  |
| system_admin_emails | array<string> |  |  |  |



### Request body (raw)

```json
{
  "send_on_received": true,
  "send_on_resolved": true,
  "send_to_system_admin": true,
  "system_admin_emails": [
    "ops@example.com"
  ]
}
```

## GET about:blank — Enums

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/feedback#get-enums)

### FeedbackType

FeatureRequest | BugReport | Feedback

### FeedbackSeverity

Urgent | High | Normal | Low

### FeedbackStatus (server-managed)

Received | Reviewed | Applied

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET about:blank

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| blank | path string | Yes | Dynamic path value from the request URL. |  |



## POST /external/deliveries/shipment-labels — Get Shipment Labels

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/external-and-carrier-integrations#post-get-shipment-labels)

Returns an `application/pdf` response containing canonical shipment labels for one or multiple deliveries. Use one `delivery_codes` value for a single shipment and multiple values for a combined PDF.

### Authentication and authorization

Requires X-API-Key authentication. Tenant scope comes from the authenticated context. Customer callers may request only their own deliveries; privileged tenant callers may request any delivery in that tenant.

### Request

`delivery_codes` is required and accepts 1-50 non-empty delivery-code strings. Duplicates are removed in first-seen order.

### Current-label behavior

Missing, stale, or unreadable labels use guarded generation; unstarted queued jobs can be claimed immediately, while running generation is awaited through the shared single-label lifecycle. Receiver data comes from the delivery's current `delivery_location`. Each successful generation has a new numeric file version and audit history; older PDF files remain available. A failed reprint preserves the previous URL.

### Response

Success is PDF binary with `Content-Disposition`; errors are localized JSON. Combined PDFs use the private content-addressed cache and do not reuse the previous cache key after a canonical label changes.

### Errors

400 invalid body/code or over 50 unique codes; 403 ownership/permission; 404 delivery/tenant not found; 413 byte limit; 422 invalid PDF; 500 lookup/cache/signing/upload errors; 502 label generation/download failure. No partial PDF is returned.

### 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. The external error envelope retains its stable `error` field; successful responses remain PDF binary.

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/external/deliveries/shipment-labels

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  | Authenticated external API key. | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_codes | array<string> |  |  |  |



### Request body (raw)

```json
{
  "delivery_codes": [
    "2501",
    "2502"
  ]
}
```

### Response 200 Success - PDF

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/pdf |
| Content-Disposition | header string |  |  | attachment; filename="shipment-labels-1710000000000.pdf" |



```json
<binary PDF response>
```

### Response 404 Error - Delivery not found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_not_found",
  "message": "Delivery not found",
  "data": {
    "code": "delivery_not_found",
    "delivery_codes": [
      "2502"
    ]
  }
}
```

### Response 403 Error - Customer does not own every delivery

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "unauthorized_delivery_access",
  "message": "Unauthorized delivery access",
  "data": {
    "code": "unauthorized_delivery_access",
    "delivery_codes": [
      "2502"
    ]
  }
}
```

### Response 400 Error - Invalid request

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "invalid_request_body",
  "message": "Invalid request body"
}
```

## POST /external/international-deliveries — Create International Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/external-international-delivery#post-create-international-delivery)

Create an international delivery using a warehouse pickup and a delivery address object.

- Requires X-API-Key authentication.

### Key behavior
- Pickup address is resolved from `warehouse_id`; do not send `pickup_address`.
- Creates inventory items from `products`.
- Creates a receiving order tied to the created inventory item(s).
- Creates the delivery with status `draft`.

### Notes
- `reference_number` for the receiving order is auto-generated as `RIO-<timestamp>`.
- `receiving_orders.status` is created as `pending`.
- `delivery.status` is created as `draft`.

### External delivery response enrichment
- Top-level response includes `currency_code`. Each delivery object also includes `currency_code`.
- `delivery.track_order` is the public tracking page URL built as `{track_url}/tracking?code={delivery_code}`; `track_url` resolves from tenant `track_orders.track_url`, then the `WEB_URL` secret, then system `external_links.delivery_customer_portal_url`, then `https://staging-app.ondi.io`.
- `delivery.customer_receipt_url` is a signed receipt URL when a successful delivery payment receipt exists; otherwise `null`.
- `delivery.status_updated_at` is derived from the latest `status_updates[].timestamp`, falling back to `updated_at`.
- `delivery.delivery_services` includes `description` and `image_url` when available.
- `delivery.proofs` is grouped as `{ pickup: [], delivery: [] }` using OnDi proof records.
- `delivery.proof_of_pickup` and `delivery.proof_of_delivery` are aggregate proof summaries when captured, shaped as `{ signature, photos }`.
- `delivery.failure_reason` is populated from `delivery_failure_reason` when present.
- Top-level `delivery_type` is not returned; use `delivery.delivery_services.delivery_type`.
- Response `payment_method` is documented as `wallet | cash`.

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/external/international-deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string | Yes | Warehouse UUID used as the pickup source. |  |
| delivery_address | object | Yes | Delivery address payload. |  |
| delivery_address.lat | number | Yes | Delivery latitude. |  |
| delivery_address.lng | number | Yes | Delivery longitude. |  |
| delivery_address.full_address | string | Yes | Full delivery address text. |  |
| delivery_address.city | string | No | City name. |  |
| delivery_address.state | string | No | State or province. |  |
| delivery_address.country | string | No | Country name or code. |  |
| delivery_address.postal_code | string | No | Postal or ZIP code. |  |
| delivery_address.contact_info | object | No | Receiver contact details. |  |
| delivery_address.contact_info.full_name | string | No | Receiver full name. |  |
| delivery_address.contact_info.phone | string | No | Receiver phone number. |  |
| delivery_address.contact_info.email | string | No | Receiver email address. |  |
| delivery_address.contact_info.tel_phone | string | No | Alternate receiver phone value used by some integrations. |  |
| customer_id | string |  | Required for non-customer callers. For customer/no-role callers it is auto-filled from the authenticated user. |  |
| delivery_service_id | string | Yes | Delivery service UUID. |  |
| payer_type | string | Yes | Supported values: `sender`, `receiver`. |  |
| payment_method | string | Yes | Accepted values: `wallet`, `cash`, `cod`. `cod` is normalized to `cash`. |  |
| external_reference_id | string | Yes | External reference. When `delivery_external_reference.unique_per_customer=true` (default), it must be unique among root orders for the same tenant customer; when false, duplicates are allowed. |  |
| order_number | string | Yes | Customer-facing order number used on shipment labels. |  |
| cod_amount | number | No | Cash-on-delivery amount. Use `0` when not applicable. |  |
| notes | string | No | General delivery / receiving notes. |  |
| scheduled_pickup_time | string | No | ISO 8601 datetime for planned pickup. |  |
| scheduled_delivery_time | string | No | ISO 8601 datetime for planned delivery. |  |
| unit_size | number | No | Positive unit size override. If omitted, the backend uses `products` totals and may derive size from `package_details.items`. |  |
| tracking_provider | string | No | Tracking provider identifier. |  |
| package_details | object | No | Parcel-level metadata. |  |
| package_details.special_instructions | string | No | Handling / packing notes. |  |
| package_details.items | array<object> | No | Parcel items used for unit-size calculation when needed. |  |
| package_details.items[].name | string | No | Item name. |  |
| package_details.items[].description | string | No | Item description. |  |
| package_details.items[].quantity | number | No | Quantity for unit-size calculation. Defaults to `1` when omitted. |  |
| package_details.items[].weight | number | No | Item weight. |  |
| package_details.items[].length | number | No | Item length. |  |
| package_details.items[].width | number | No | Item width. |  |
| package_details.items[].height | number | No | Item height. |  |
| products | array<object> | Yes | Minimum 1 product. Used to create inventory items. |  |
| products[].name | string | Yes | Product name. |  |
| products[].sku | string | Yes | SKU. |  |
| products[].barcode | string | Yes | Barcode. |  |
| products[].unit_size | number | Yes | Unit size for this product line. |  |
| products[].quantity | number | Yes | Quantity for this product line. |  |
| products[].description | string | No | Product description. |  |
| products[].brand | string | No | Brand name. |  |
| products[].model | string | No | Model name or number. |  |
| products[].category | string | No | Product category. |  |
| products[].subcategory | string | No | Product subcategory. |  |
| sender | object | Yes | Sender details used for labels / inventory linkage. |  |
| sender.name | string | Yes | Sender name. |  |
| sender.phone | string | Yes | Sender phone number. |  |
| sender.full_address | string | No | Sender full address. Recommended; omitted values fall back to an empty string in label data. |  |
| expected_arrival_date | string | No | Receiving order ETA date (`YYYY-MM-DD`). |  |
| receiving_details | object | No | Free-form receiving order metadata. Example keys: `carrier`, `incoterms`. |  |
| actual_arrival_date | string \| null | No | Present in the sample payload, but not currently consumed by the external handler. |  |
| received_by | string \| null | No | Present in the sample payload, but not currently consumed by the external handler. |  |
| supplier_id | string \| null | No | Supplier identifier passed into receiving-order creation. |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "delivery_address": {
    "lat": {{delivery_lat}},
    "lng": {{delivery_lng}},
    "full_address": "{{delivery_full_address}}",
    "city": "{{delivery_city}}",
    "state": "{{delivery_state}}",
    "country": "{{delivery_country}}",
    "postal_code": "{{delivery_postal_code}}",
    "contact_info": {
      "full_name": "{{receiver_name}}",
      "phone": "{{receiver_phone}}",
      "email": "{{receiver_email}}",
      "tel_phone": "{{receiver_tel}}"
    }
  },
  "customer_id": "{{customer_id}}",
  "delivery_service_id": "{{delivery_service_id}}",
  "payer_type": "sender",
  "payment_method": "wallet",
  "external_reference_id": "{{external_reference_id}}",
  "order_number": "{{order_number}}",
  "cod_amount": 0,
  "notes": "International shipment",
  "scheduled_pickup_time": "2024-01-15T10:00:00Z",
  "scheduled_delivery_time": "2024-01-20T18:00:00Z",
  "unit_size": 1,
  "tracking_provider": "internal",
  "package_details": {
    "special_instructions": "International shipment - handle with care",
    "items": [
      {
        "name": "{{item_name}}",
        "description": "{{item_description}}",
        "quantity": 1,
        "weight": 2.5,
        "length": 30,
        "width": 20,
        "height": 15
      }
    ]
  },
  "products": [
    {
      "name": "Widget A",
      "sku": "WIDGET-A",
      "barcode": "1234567890123",
      "unit_size": 1,
      "quantity": 2,
      "description": "Blue widget",
      "brand": "Acme",
      "model": "A1",
      "category": "Widgets",
      "subcategory": "Standard"
    },
    {
      "name": "Gadget B",
      "sku": "GADGET-B",
      "barcode": "9876543210987",
      "unit_size": 1,
      "quantity": 1
    }
  ],
  "sender": {
    "name": "{{sender_name}}",
    "phone": "{{sender_phone}}",
    "full_address": "{{sender_full_address}}"
  },
  "expected_arrival_date": "2024-01-19",
  "receiving_details": {
    "carrier": "DHL",
    "incoterms": "DAP"
  },
  "actual_arrival_date": null,
  "received_by": null,
  "supplier_id": null
}
```

### Response 201 Success - Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery created successfully",
  "delivery": {
    "id": "DEL-000001",
    "delivery_code": "DEL-000001"
  },
  "pickup_address_id": "warehouse-address-uuid",
  "delivery_address": {
    "id": "address-uuid",
    "full_address": "456 Broadway..."
  },
  "inventory_item": {
    "id": "item-uuid-1",
    "sku": "WIDGET-A",
    "name": "Widget A"
  },
  "receiving_order": {
    "id": "ro-uuid",
    "reference_number": "RIO-1700000000000",
    "status": "pending"
  }
}
```

### Response 400 Error - Unit Size Above Service Maximum

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "can_not_use_this_delivery_for_this_unit_size",
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum"
}
```

### Response 409 Error - Missing tenant_id

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "tenant_id_required",
  "message": "Tenant ID is required"
}
```

### Response 400 Error - Validation (products array)

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "products_array_cannot_be_empty",
  "message": "Products array cannot be empty"
}
```

### Response 404 Error - Warehouse Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "warehouse_not_found",
  "message": "Warehouse not found"
}
```

### Response 400 Error - Delivery Address Creation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_address_creation_failed",
  "message": "Delivery address creation failed"
}
```

### Response 400 Error - External Reference Already Exists

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "external_reference_id_already_exists",
  "message": "External reference ID already exists for this customer"
}
```

### Response 500 Error - Server Error (inventory creation)

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "internal_server_error",
  "message": "Internal Server Error"
}
```

## GET /external/delivery/price — Calculate Delivery Price

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/external-and-carrier-integrations#get-calculate-delivery-price)

Calculate delivery price based on pickup and dropoff coordinates. This endpoint is designed for external integrations and accepts latitude/longitude coordinates directly instead of address IDs.

- Requires X-API-Key authentication.
- Public endpoint accessible to external systems

### Pricing Logic
- Determines the pickup and dropoff delivery zones from the coordinates
- Looks up the applicable zone-pricing rule for the resolved origin/destination pair, including same-zone pairs
- Uses the service default price only when no zone rule matches and the effective pricing policy allows fallback
- If warehouse_id provided, uses warehouse address as pickup location
- Supplied units affect the per-unit portion of zone pricing and are no longer replaced by the service maximum
- Units must remain within the delivery service `unit_min` and `unit_max` range
- Currency is resolved from tenant currency settings, then system currency settings, and falls back to `IQD`.
- Returns error if locations are outside configured delivery zones

### Frontend integration contract
- Send the intended positive `units` value when quoting a known shipment size; the backend now honors it exactly.
- Omit `units` only when pricing should use the configured missing-unit fallback. Read `unit_size` and `unit_size_source` from the success response.
- Read `currency_code` for the OnDi currency code.
- Handle HTTP 400 `invalid_units_parameter` for zero, negative, non-numeric, or unresolved template values.
- Handle HTTP 400 `can_not_use_this_delivery_for_this_unit_size` by displaying the localized `message`; use `calculated_unit_size`, `allowed_unit_min`, `allowed_unit_max`, and `violation` for context.

Authentication: Send an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/external/delivery/price?delivery_service_id={{delivery_service_id}}&pickup_lat={{pickup_lat}}&pickup_lng={{pickup_lng}}&dropoff_lat={{dropoff_lat}}&dropoff_lng={{dropoff_lng}}&warehouse_id={{warehouse_id}}&units={{units}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_service_id | query string | Yes | ID of the delivery service to calculate price for (Required) UUID of the delivery service | {{delivery_service_id}} |
| pickup_lat | query string |  | Latitude of pickup location (Optional if warehouse_id provided, decimal format) (conditional): Latitude of pickup location (decimal format, e.g., 40.7128) - Required if warehouse_id not provided | {{pickup_lat}} |
| pickup_lng | query string |  | Longitude of pickup location (Optional if warehouse_id provided, decimal format) (conditional): Longitude of pickup location (decimal format, e.g., -74.0060) - Required if warehouse_id not provided | {{pickup_lng}} |
| dropoff_lat | query string | Yes | Latitude of dropoff location (Required, decimal format) Latitude of dropoff location (decimal format, e.g., 40.7589) | {{dropoff_lat}} |
| dropoff_lng | query string | Yes | Longitude of dropoff location (Required, decimal format) Longitude of dropoff location (decimal format, e.g., -73.9851) | {{dropoff_lng}} |
| warehouse_id | query string | No | ID of the warehouse for pickup location (Optional, alternative to pickup_lat/lng) UUID of warehouse for pickup location (alternative to pickup_lat/lng) | {{warehouse_id}} |
| units | query string | No | Number of units for pricing calculation. When provided, this exact positive value is used. When omitted, the configured missing-unit fallback is used (Optional) Positive number of units for pricing. When provided, the API uses that exact value. When omitted, `general.unit_size.settings.missing_unit_size_fallback` selects service minimum, half of service maximum, service maximum, or returns HTTP 400 `unit_size_or_dimensions_required`. | {{units}} |
| tenant_id | query string | Yes | UUID of the tenant |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Price calculated successfully",
  "price": 15.5,
  "currency_code": "IQD"
}
```

### Response 400 Error - Missing Parameters

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "missing_required_parameters",
  "message": "Missing required parameters"
}
```

### Response 400 Error - Invalid Coordinates

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "invalid_coordinates",
  "message": "Invalid coordinates"
}
```

### Response 400 Error - Service Not Found

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_service_not_found",
  "message": "Delivery service not found"
}
```

### Response 400 Error - Outside Delivery Zone

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_location_outside_delivery_zone",
  "message": "Delivery location outside delivery zone"
}
```

### Response 400 Error - Zone Pricing Not Found

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "no_zone_pricing_rule_matched",
  "message": "No zone pricing rule matched"
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "internal_server_error",
  "message": "Internal Server Error"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "price": 15.5,
  "currency_code": "IQD"
}
```

### Response 400 Missing Parameters (400 Bad Request)



```json
{
  "success": false,
  "error": "missing_required_parameters"
}
```

### Response 400 Invalid Coordinates (400 Bad Request)



```json
{
  "success": false,
  "error": "invalid_coordinates"
}
```

### Response 400 Invalid Units (400 Bad Request)



```json
{
  "success": false,
  "error": "invalid_units_parameter"
}
```

### Response 400 Units Outside Service Range (400 Bad Request)



```json
{
  "success": false,
  "error": "can_not_use_this_delivery_for_this_unit_size",
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum"
}
```

### Response 400 Service Not Found (400 Bad Request)



```json
{
  "success": false,
  "error": "delivery_service_not_found"
}
```

### Response 400 Outside Delivery Zone (400 Bad Request)



```json
{
  "success": false,
  "error": "delivery_location_outside_delivery_zone"
}
```

### Response 400 Zone Pricing Not Found (400 Bad Request)



```json
{
  "success": false,
  "error": "address_outside_delivery_zone"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "error": "internal_server_error"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
# Using coordinates
curl -X GET "https://your-domain.supabase.co/functions/v1/external/delivery/price?delivery_service_id=123e4567-e89b-12d3-a456-426614174000&pickup_lat=40.7128&pickup_lng=-74.0060&dropoff_lat=40.7589&dropoff_lng=-73.9851&tenant_id=abc123def-456-789-ghi-jklmnop&units=2" \
  -H "X-API-Key: your-api-key-here"

# Using warehouse ID
curl -X GET "https://your-domain.supabase.co/functions/v1/external/delivery/price?delivery_service_id=123e4567-e89b-12d3-a456-426614174000&warehouse_id=warehouse-uuid&dropoff_lat=40.7589&dropoff_lng=-73.9851&tenant_id=abc123def-456-789-ghi-jklmnop" \
  -H "X-API-Key: your-api-key-here"
```

## GET /external/zone — Get Zone by Coordinates

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/external-and-carrier-integrations#get-get-zone-by-coordinates)

Get the delivery zone that contains the given coordinates. This endpoint uses the get_zone_by_point database function to find which zone contains the specified latitude and longitude.

- Requires X-API-Key authentication.
- Public endpoint accessible to external systems

Authentication: Send an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/external/zone?lat={{latitude}}&lng={{longitude}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| lat | query string | Yes | Latitude of the location (Required, decimal format) Latitude of the location (decimal format, e.g., 40.7128) | {{latitude}} |
| lng | query string | Yes | Longitude of the location (Required, decimal format) Longitude of the location (decimal format, e.g., -74.0060) | {{longitude}} |
| tenant_id | query string | Yes | UUID of the tenant |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Zone found successfully",
  "zone": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "name": "Downtown Zone",
    "boundaries": {
      "type": "Polygon",
      "coordinates": [
        [
          [
            40.7128,
            -74.006
          ],
          [
            40.72,
            -74.01
          ]
        ]
      ]
    },
    "is_active": true
  }
}
```

### Response 400 Error - Missing Parameters

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "lat_lng_required",
  "message": "Latitude and longitude are required"
}
```

### Response 400 Error - Invalid Coordinates

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "invalid_coordinates",
  "message": "Invalid coordinates"
}
```

### Response 404 Error - Zone Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "zone_not_found_for_coordinates",
  "message": "Zone not found for coordinates"
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "internal_server_error",
  "message": "Internal Server Error"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "zone_found_successfully",
  "zone": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "name": "Downtown Zone",
    "boundaries": {
      "type": "Polygon",
      "coordinates": [[[lat, lng], [lat, lng]]]
    },
    "is_active": true
  }
}
```

### Response 400 Missing Parameters (400 Bad Request)



```json
{
  "success": false,
  "message": "lat_lng_required"
}
```

### Response 400 Missing Tenant ID (400 Bad Request)



```json
{
  "success": false,
  "message": "tenant_id_required"
}
```

### Response 400 Invalid Coordinates (400 Bad Request)



```json
{
  "success": false,
  "message": "invalid_coordinates"
}
```

### Response 404 Zone Not Found (404 Not Found)



```json
{
  "success": false,
  "message": "zone_not_found_for_coordinates"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "internal_server_error"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
curl -X GET "https://your-domain.supabase.co/functions/v1/external/zone?lat=40.7128&lng=-74.0060&tenant_id=abc123def-456-789-ghi-jklmnop" \
  -H "X-API-Key: your-api-key-here"
```

## POST /external/deliveries — Create Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/deliveries/overview#post-create-delivery-2)

Create a normal domestic delivery using pickup and delivery address objects. The endpoint accepts coordinates and address text in the request. On success, it reloads the created row using the same `delivery_list_view`-backed detail shape used by external get-delivery, then returns it under `delivery`.

### Success response
- `201` with `{ success, error, message, currency_code, delivery }`.
- `delivery` is detail-shaped like `GET /external/deliveries/{id}`.
- `delivery` includes `pickup_location`, `delivery_location`, `pickup_warehouse_id`, `dropoff_warehouse_id`, `pickup_warehouse`, `dropoff_warehouse`, `delivery_services`, `package_details`, `unit_size`, `cod_amount`, `notes`, and `special_instructions` when available.
- `delivery.id` remains `delivery_code` for external create compatibility.

### Important errors
- `400` validation failure, `delivery_service_not_found`, `external_reference_id_already_exists`, `unit_size_or_dimensions_required`, price/zone errors.
- `500` `internal_server_error`.

### External delivery response enrichment
- Top-level response includes `currency_code`. Each delivery object also includes `currency_code`.
- `delivery.track_order` is the public tracking page URL built as `{track_url}/tracking?code={delivery_code}`; `track_url` resolves from tenant `track_orders.track_url`, then the `WEB_URL` secret, then system `external_links.delivery_customer_portal_url`, then `https://staging-app.ondi.io`.
- `delivery.customer_receipt_url` is a signed receipt URL when a successful delivery payment receipt exists; otherwise `null`.
- `delivery.status_updated_at` is derived from the latest `status_updates[].timestamp`, falling back to `updated_at`.
- `delivery.delivery_services` includes `description` and `image_url` when available.
- `delivery.proofs` is grouped as `{ pickup: [], delivery: [] }` using OnDi proof records.
- `delivery.proof_of_pickup` and `delivery.proof_of_delivery` are aggregate proof summaries when captured, shaped as `{ signature, photos }`.
- `delivery.failure_reason` is populated from `delivery_failure_reason` when present.
- Top-level `delivery_type` is not returned; use `delivery.delivery_services.delivery_type`.
- Response `payment_method` is documented as `wallet | cash`.

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/external/deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pickup_address | object | Yes | Pickup address payload. |  |
| pickup_address.lat | number | Yes | Pickup latitude. |  |
| pickup_address.lng | number | Yes | Pickup longitude. |  |
| pickup_address.full_address | string | Yes | Full pickup address text. |  |
| pickup_address.city | string | No | City name. |  |
| pickup_address.state | string | No | State or province. |  |
| pickup_address.country | string | No | Country name or code. |  |
| pickup_address.postal_code | string | No | Postal or ZIP code. |  |
| pickup_address.address_line1 | string | No | Address line 1. |  |
| pickup_address.address_line2 | string | No | Address line 2. |  |
| pickup_address.contact_info | object | No | Pickup contact details. |  |
| pickup_address.contact_info.full_name | string | No | Contact full name. |  |
| pickup_address.contact_info.phone | string | No | Contact phone number. |  |
| pickup_address.contact_info.email | string | No | Contact email address. |  |
| delivery_address | object | Yes | Delivery address payload. |  |
| delivery_address.lat | number | Yes | Delivery latitude. |  |
| delivery_address.lng | number | Yes | Delivery longitude. |  |
| delivery_address.full_address | string | Yes | Full delivery address text. |  |
| delivery_address.city | string | No | City name. |  |
| delivery_address.state | string | No | State or province. |  |
| delivery_address.country | string | No | Country name or code. |  |
| delivery_address.postal_code | string | No | Postal or ZIP code. |  |
| delivery_address.address_line1 | string | No | Address line 1. |  |
| delivery_address.address_line2 | string | No | Address line 2. |  |
| delivery_address.contact_info | object | No | Receiver contact details. |  |
| delivery_address.contact_info.full_name | string | No | Contact full name. |  |
| delivery_address.contact_info.phone | string | No | Contact phone number. |  |
| delivery_address.contact_info.email | string | No | Contact email address. |  |
| customer_id | string |  | Required for non-customer callers. Auto-filled from auth for customer/no-role callers. |  |
| delivery_service_id | string | Yes | Delivery service UUID. |  |
| payer_type | string | Yes | Supported values: `sender`, `receiver`. |  |
| payment_method | string | Yes | Accepted values: `wallet`, `cash`, `cod`. `cod` is normalized to `cash`. |  |
| external_reference_id | string | Yes | External reference. When `delivery_external_reference.unique_per_customer=true` (default), it must be unique among root orders for the same tenant customer; when false, duplicates are allowed. |  |
| cod_amount | number | No | Minimum `0`. |  |
| notes | string | No | General delivery notes. |  |
| special_instructions | string | No | Top-level delivery handling instructions. |  |
| scheduled_pickup_time | string | No | ISO 8601 datetime for planned pickup. |  |
| scheduled_delivery_time | string | No | ISO 8601 datetime for planned delivery. |  |
| receiver_user_id | string | No | Receiver user UUID. |  |
| unit_size | number | No | Positive unit size. Measured package items take priority; when neither is available, `general.unit_size.settings.missing_unit_size_fallback` is applied. |  |
| package_details | object | No | Used to compute unit size from measured `items` when supplied. |  |
| package_details.special_instructions | string | No | Parcel-specific handling notes. |  |
| package_details.items | array<object> | No | Measured parcel items used for unit-size calculation. |  |
| package_details.items[].name | string | No | Item name. |  |
| package_details.items[].description | string | No | Item description. |  |
| package_details.items[].quantity | number | No | Quantity for unit-size calculation. Defaults to `1` when omitted. |  |
| package_details.items[].weight | number | No | Item weight. |  |
| package_details.items[].length | number | No | Item length. |  |
| package_details.items[].width | number | No | Item width. |  |
| package_details.items[].height | number | No | Item height. |  |
| tracking_provider | string | No | Tracking provider identifier. |  |
| is_draft | boolean | No | When `true`, delivery is created with status `draft`; otherwise it follows normal creation flow. |  |



### Request body (raw)

```json
{
  "pickup_address": {
    "lat": {{pickup_lat}}, // required
    "lng": {{pickup_lng}},
    "full_address": "{{pickup_full_address}}",
    "city": "{{pickup_city}}",
    "state": "{{pickup_state}}",
    "country": "{{pickup_country}}",
    "postal_code": "{{pickup_postal_code}}",
    "contact_info": {
      "full_name": "{{sender_name}}",
      "phone": "{{sender_phone}}",
      "email": "{{sender_email}}"
    }
  },
  "delivery_address": {
    "lat": {{delivery_lat}},
    "lng": {{delivery_lng}},
    "full_address": "{{delivery_full_address}}",
    "city": "{{delivery_city}}",
    "state": "{{delivery_state}}",
    "country": "{{delivery_country}}",
    "postal_code": "{{delivery_postal_code}}",
    "contact_info": {
      "full_name": "{{receiver_name}}",
      "phone": "{{receiver_phone}}",
      "email": "{{receiver_email}}"
    }
  },
  "customer_id": "{{customer_id}}",
  "delivery_service_id": "{{delivery_service_id}}",
  "payer_type": "sender",
  "payment_method": "wallet",
  "cod_amount": 0,
  "notes": "Package delivery",
  "external_reference_id": "EXT-REF-123",
  "is_draft": true,
  "scheduled_pickup_time": "2024-01-15T10:00:00Z",
  "scheduled_delivery_time": "2024-01-15T14:00:00Z",
  "receiver_user_id": "{{receiver_user_id}}",
  "unit_size": 1,
  "tracking_provider": "internal",
  "package_details": {
    "special_instructions": "Handle with care",
    "items": [
      {
        "name": "{{item_name}}",
        "description": "{{item_description}}",
        "quantity": 1,
        "weight": 2.5,
        "length": 30,
        "width": 20,
        "height": 15
      }
    ]
  },
  "special_instructions": "Handle with care"
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery created successfully",
  "delivery": {
    "id": "DEL-000001",
    "tenant_id": "tenant-uuid",
    "external_reference_id": "EXT-REF-123",
    "parent_delivery_id": null,
    "assignment_id": null,
    "requires_warehouse_staging": false,
    "staging_warehouse_id": null,
    "staging_warehouse": null,
    "pickup_location": {
      "lat": 40.7128,
      "lng": -74.006,
      "latitude": 40.7128,
      "longitude": -74.006,
      "full_address": "123 Main St, New York, NY 10001",
      "city": "New York",
      "state": "NY",
      "country": "US",
      "postal_code": "10001",
      "zone_id": "pickup-zone-uuid",
      "zone_name": "Downtown Zone",
      "zone_code": "DT",
      "contact_info": {
        "full_name": "John Sender",
        "phone": "+1-555-0100",
        "email": "sender@example.com",
        "zone_name": "Downtown Zone",
        "zone_code": "DT"
      }
    },
    "delivery_location": {
      "lat": 40.7589,
      "lng": -73.9851,
      "latitude": 40.7589,
      "longitude": -73.9851,
      "full_address": "456 Broadway, New York, NY 10013",
      "city": "New York",
      "state": "NY",
      "country": "US",
      "postal_code": "10013",
      "zone_id": "delivery-zone-uuid",
      "zone_name": "Midtown Zone",
      "zone_code": "MT",
      "contact_info": {
        "full_name": "Jane Receiver",
        "phone": "+1-555-0200",
        "email": "receiver@example.com",
        "zone_name": "Midtown Zone",
        "zone_code": "MT"
      }
    },
    "pickup_warehouse_id": null,
    "dropoff_warehouse_id": null,
    "pickup_warehouse": null,
    "dropoff_warehouse": null,
    "assignment": null,
    "cod": null,
    "delivery_services": {
      "id": "service-uuid",
      "name": "Standard Delivery",
      "description": "Standard delivery",
      "image_url": "https://cdn.example.com/services/standard.png",
      "delivery_type": {
        "id": "delivery-type-uuid",
        "name": "Delivery"
      }
    },
    "customer": {
      "id": "customer-uuid",
      "full_name": "Customer Name",
      "avatar_url": null,
      "phone": "+1-555-0000"
    },
    "driver": {
      "id": null,
      "full_name": null,
      "avatar_url": null,
      "phone": null,
      "latitude": null,
      "longitude": null
    },
    "vehicle": null,
    "payment_status": "pending",
    "payer_type": "sender",
    "cod_amount": 0,
    "delivery_fee": 15.5,
    "payment_method": "wallet",
    "status": "draft",
    "open_exception": null,
    "has_open_exception": false,
    "sequence_plan": null,
    "carrier_type": "internal",
    "is_parent": false,
    "direction": null,
    "pickup_zone_id": "pickup-zone-uuid",
    "dropoff_zone_id": "delivery-zone-uuid",
    "unit_size": 1,
    "notes": "Package delivery",
    "special_instructions": "Handle with care",
    "package_details": {
      "special_instructions": "Handle with care",
      "items": [
        {
          "name": "Item 1",
          "description": "Widget",
          "quantity": 1,
          "weight": 2.5,
          "length": 30,
          "width": 20,
          "height": 15
        }
      ]
    },
    "quantity": 1,
    "delivery_code": "DEL-000001",
    "barcode": "DEL-000001",
    "track_order": "https://staging-app.ondi.io/tracking?code=DEL-000001",
    "customer_receipt_url": null,
    "status_updated_at": "2024-01-15T08:30:00Z",
    "proofs": {
      "pickup": [],
      "delivery": []
    },
    "shipment_label_url": null,
    "shipment_label_status": null,
    "status_updates": null,
    "created_at": "2024-01-15T08:00:00Z",
    "updated_at": "2024-01-15T08:00:00Z",
    "delivery_flow": null
  }
}
```

### Response 201 Success - Default (Pending)

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery created successfully",
  "delivery": {
    "id": "DEL-000002",
    "tenant_id": "tenant-uuid",
    "external_reference_id": "EXT-REF-456",
    "parent_delivery_id": null,
    "assignment_id": null,
    "requires_warehouse_staging": false,
    "staging_warehouse_id": null,
    "staging_warehouse": null,
    "pickup_location": {
      "lat": 40.7128,
      "lng": -74.006,
      "latitude": 40.7128,
      "longitude": -74.006,
      "full_address": "123 Main St, New York, NY 10001",
      "city": "New York",
      "state": "NY",
      "country": "US",
      "postal_code": "10001",
      "zone_id": "pickup-zone-uuid",
      "zone_name": "Downtown Zone",
      "zone_code": "DT",
      "contact_info": {
        "full_name": "John Sender",
        "phone": "+1-555-0100",
        "email": "sender@example.com",
        "zone_name": "Downtown Zone",
        "zone_code": "DT"
      }
    },
    "delivery_location": {
      "lat": 40.7589,
      "lng": -73.9851,
      "latitude": 40.7589,
      "longitude": -73.9851,
      "full_address": "456 Broadway, New York, NY 10013",
      "city": "New York",
      "state": "NY",
      "country": "US",
      "postal_code": "10013",
      "zone_id": "delivery-zone-uuid",
      "zone_name": "Midtown Zone",
      "zone_code": "MT",
      "contact_info": {
        "full_name": "Jane Receiver",
        "phone": "+1-555-0200",
        "email": "receiver@example.com",
        "zone_name": "Midtown Zone",
        "zone_code": "MT"
      }
    },
    "pickup_warehouse_id": null,
    "dropoff_warehouse_id": null,
    "pickup_warehouse": null,
    "dropoff_warehouse": null,
    "assignment": null,
    "cod": null,
    "delivery_services": {
      "id": "service-uuid",
      "name": "Standard Delivery",
      "description": "Standard delivery",
      "image_url": "https://cdn.example.com/services/standard.png",
      "delivery_type": {
        "id": "delivery-type-uuid",
        "name": "Delivery"
      }
    },
    "customer": {
      "id": "customer-uuid-2",
      "full_name": "Customer Name",
      "avatar_url": null,
      "phone": "+1-555-0000"
    },
    "driver": {
      "id": null,
      "full_name": null,
      "avatar_url": null,
      "phone": null,
      "latitude": null,
      "longitude": null
    },
    "vehicle": null,
    "payment_status": "pending",
    "payer_type": "sender",
    "cod_amount": 0,
    "delivery_fee": 25,
    "payment_method": "wallet",
    "status": "pending",
    "open_exception": null,
    "has_open_exception": false,
    "sequence_plan": null,
    "carrier_type": "internal",
    "is_parent": false,
    "direction": null,
    "pickup_zone_id": "pickup-zone-uuid",
    "dropoff_zone_id": "delivery-zone-uuid",
    "unit_size": 1,
    "notes": "Package delivery",
    "special_instructions": "Handle with care",
    "package_details": {
      "special_instructions": "Handle with care",
      "items": [
        {
          "name": "Item 1",
          "description": "Widget",
          "quantity": 1,
          "weight": 2.5,
          "length": 30,
          "width": 20,
          "height": 15
        }
      ]
    },
    "quantity": 1,
    "delivery_code": "DEL-000002",
    "barcode": "DEL-000002",
    "track_order": "https://staging-app.ondi.io/tracking?code=DEL-000002",
    "customer_receipt_url": null,
    "status_updated_at": "2024-01-16T08:00:00Z",
    "proofs": {
      "pickup": [],
      "delivery": []
    },
    "shipment_label_url": null,
    "shipment_label_status": null,
    "status_updates": null,
    "created_at": "2024-01-16T08:00:00Z",
    "updated_at": "2024-01-16T08:00:00Z",
    "delivery_flow": null
  }
}
```

### Response 400 Error - Unit Size Above Service Maximum

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "can_not_use_this_delivery_for_this_unit_size",
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum"
}
```

### Response 400 Error - Invalid Address

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "pickup_address_lat_lng_must_be_numbers",
  "message": "Pickup address latitude and longitude must be numbers"
}
```

### Response 400 Error - Missing Required Fields

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_address_required",
  "message": "Delivery address is required"
}
```

## GET /external/deliveries — List Deliveries

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/deliveries/overview#get-list-deliveries-2)

List normal deliveries visible to the authenticated external caller. External customers are always restricted to deliveries whose customer ID matches the authenticated user; a supplied customer_id cannot override this ownership scope. The default list includes draft and other active deliveries. Delivered deliveries remain excluded by default unless requested with status=delivered or is_completed=true.

Tenant scope comes from the authenticated context, with tenant_id query fallback where supported.
### Success response

{ success, error, message, currency_code, deliveries, page, limit, total }. Each delivery object also includes currency_code for money fields such as delivery_fee and cod_amount. Deliveries are loaded from delivery_list_view, then enriched for the external contract.

### Frontend integration contract

read snake_case response fields only. Use delivery.delivery_services.delivery_type for delivery type, delivery.track_order for the public tracking page, delivery.customer_receipt_url when non-null, and delivery.status_updated_at for the last status change timestamp.

### External delivery response enrichment
- Top-level response includes currency_code. Each delivery object also includes currency_code.
- delivery.track_order is the public tracking page URL built as {track_url}/tracking?code={delivery_code}; track_url resolves from tenant track_orders.track_url, then WEB_URL secret, then system external_links.delivery_customer_portal_url, then https://staging-app.ondi.io.
- delivery.customer_receipt_url is a signed receipt URL when a successful delivery payment receipt exists; otherwise null.
- delivery.status_updated_at is derived from the latest status_updates[].timestamp, falling back to updated_at.
- delivery.delivery_services includes description and image_url when available.
- delivery.proofs is grouped as { pickup: [], delivery: [] } using OnDi proof records.
- delivery.proof_of_pickup and delivery.proof_of_delivery are aggregate proof summaries when captured, shaped as { signature, photos }.
- delivery.failure_reason is populated from delivery_failure_reason when present.
- Top-level delivery_type is not returned; use delivery.delivery_services.delivery_type.
- payment_method is documented as wallet|cash.

Authentication: Send an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/external/deliveries?page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (Optional, default: 1) | 1 |
| limit | query string | No | Number of deliveries per page (Optional, default: 10) | 10 |
| status | query string | No | Filter by delivery status, including draft, pending, assigned, picked_up, delivered, or cancelled (Optional) |  |
| customer_id | query string | No | Filter by customer ID (Optional) |  |
| search | query string | No | Search in delivery details (Optional) |  |
| driver_id | query string | No | Filter by driver ID (Optional) |  |
| zone_ids | query string | No | Filter by zone IDs (comma-separated) (Optional) |  |
| payer_type | query string | No | Filter by payer type (sender/receiver) (Optional) |  |
| is_completed | query string | No | Filter by completion status (true/false) (Optional) |  |
| payment_method | query string | No | Filter by payment method (wallet/cod/card) (Optional) |  |
| payment_status | query string | No | Filter by payment status (Optional) |  |
| delivery_service_id | query string | No | Filter by delivery service ID (Optional) |  |
| start_date | query string | No | Filter by start date (ISO format) (Optional) |  |
| end_date | query string | No | Filter by end date (ISO format) (Optional) |  |
| assignment_status | query string | No | Filter by assignment status (Optional) |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Deliveries retrieved successfully",
  "currency_code": "IQD",
  "deliveries": [
    {
      "id": "DEL-000001",
      "status": "pending",
      "tenant_id": "tenant-uuid",
      "external_reference_id": "EXT-REF-123",
      "customer_id": "customer-uuid",
      "delivery_service_id": "service-uuid",
      "payer_type": "sender",
      "payment_method": "wallet",
      "delivery_fee": 15.5,
      "cod_amount": 0,
      "currency_code": "IQD",
      "notes": "Package delivery",
      "unit_size": 1,
      "tracking_provider": "internal",
      "carrier_type": "internal",
      "pickup_warehouse_id": null,
      "dropoff_warehouse_id": null,
      "pickup_location_id": "address-uuid-1",
      "delivery_location_id": "address-uuid-2",
      "receiver_user_id": null,
      "assigned_carrier_id": null,
      "assigned_to_user_id": null,
      "package_details": {
        "special_instructions": "Handle with care",
        "items": [
          {
            "name": "Item 1",
            "description": "Widget",
            "quantity": 1,
            "weight": 2.5
          }
        ]
      },
      "scheduled_pickup_time": "2024-01-15T10:00:00Z",
      "scheduled_delivery_time": "2024-01-15T14:00:00Z",
      "started_at": null,
      "picked_up_at": null,
      "delivered_at": null,
      "cancelled_at": null,
      "delivery_failure_reason": null,
      "created_at": "2024-01-15T08:00:00Z",
      "updated_at": "2024-01-15T08:00:00Z",
      "barcode": "DEL-000001",
      "pickup_location": {
        "id": "address-uuid-1",
        "full_address": "123 Main St, New York, NY 10001",
        "latitude": 40.7128,
        "longitude": -74.006,
        "city": "New York",
        "state": "NY",
        "country": "US",
        "postal_code": "10001",
        "contact_info": {
          "full_name": "John Sender",
          "phone": "+1-555-0100",
          "email": "sender@example.com"
        }
      },
      "delivery_location": {
        "id": "address-uuid-2",
        "full_address": "456 Broadway, New York, NY 10013",
        "latitude": 40.7589,
        "longitude": -73.9851,
        "city": "New York",
        "state": "NY",
        "country": "US",
        "postal_code": "10013",
        "contact_info": {
          "full_name": "Jane Receiver",
          "phone": "+1-555-0200",
          "email": "receiver@example.com"
        }
      },
      "delivery_code": "DEL-000001"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 25
}
```

## GET /external/deliveries/{{delivery_code}} — Get Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/deliveries/overview#get-get-delivery-2)

Retrieve one normal delivery by public delivery code, UUID, or barcode. External customers can retrieve only their own delivery; another customer’s delivery returns 404.

- The path value is the public delivery code by default. UUIDs still work for legacy callers.

Tenant scope comes from the authenticated context, with tenant_id query fallback where supported.
### Success response

{ success, error, message, currency_code, delivery }. The delivery object also includes currency_code for money fields such as delivery_fee and cod_amount. Delivery is loaded from delivery_list_view, then enriched for the external contract.
### Responses

200 success, 404 delivery_not_found, 500 internal_server_error for a database/query failure.

### Frontend integration contract

read snake_case response fields only. Use delivery.delivery_services.delivery_type for delivery type, delivery.track_order for the public tracking page, delivery.customer_receipt_url when non-null, and delivery.status_updated_at for the last status change timestamp.

### External delivery response enrichment
- Top-level response includes currency_code. Each delivery object also includes currency_code.
- delivery.id is the public delivery code and matches delivery.delivery_code.
- delivery.track_order is the public tracking page URL built as {track_url}/tracking?code={delivery_code}; track_url resolves from tenant track_orders.track_url, then WEB_URL secret, then system external_links.delivery_customer_portal_url, then https://staging-app.ondi.io.
- delivery.customer_receipt_url is a signed receipt URL when a successful delivery payment receipt exists; otherwise null.
- delivery.status_updated_at is derived from the latest status_updates[].timestamp, falling back to updated_at.
- delivery.delivery_services includes description and image_url when available.
- delivery.proofs is grouped as { pickup: [], delivery: [] } using OnDi proof records.
- delivery.proof_of_pickup and delivery.proof_of_delivery are aggregate proof summaries when captured, shaped as { signature, photos }.
- delivery.failure_reason is populated from delivery_failure_reason when present.
- Top-level delivery_type is not returned; use delivery.delivery_services.delivery_type.
- payment_method is documented as wallet|cash.

Authentication: Send an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/external/deliveries/{{delivery_code}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_code | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| filter_type | query string | No | Optional override: id, delivery_code, or barcode. Without it, UUID paths use id and all other paths use delivery_code. id, delivery_code, or barcode. Defaults to delivery_code for non-UUID paths and id for UUID paths. | {{filter_type}} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery retrieved successfully",
  "currency_code": "IQD",
  "delivery": {
    "id": "DEL-000001",
    "status": "pending",
    "tenant_id": "tenant-uuid",
    "external_reference_id": "EXT-REF-123",
    "customer_id": "customer-uuid",
    "delivery_service_id": "service-uuid",
    "payer_type": "sender",
    "payment_method": "wallet",
    "delivery_fee": 15.5,
    "cod_amount": 0,
    "currency_code": "IQD",
    "notes": "Package delivery",
    "unit_size": 1,
    "tracking_provider": "internal",
    "carrier_type": "internal",
    "pickup_warehouse_id": null,
    "dropoff_warehouse_id": null,
    "pickup_location_id": "address-uuid-1",
    "delivery_location_id": "address-uuid-2",
    "receiver_user_id": null,
    "assigned_carrier_id": null,
    "assigned_to_user_id": null,
    "package_details": {
      "special_instructions": "Handle with care",
      "items": [
        {
          "name": "Item 1",
          "description": "Widget",
          "quantity": 1,
          "weight": 2.5
        }
      ]
    },
    "scheduled_pickup_time": "2024-01-15T10:00:00Z",
    "scheduled_delivery_time": "2024-01-15T14:00:00Z",
    "started_at": null,
    "picked_up_at": null,
    "delivered_at": null,
    "cancelled_at": null,
    "delivery_failure_reason": null,
    "created_at": "2024-01-15T08:00:00Z",
    "updated_at": "2024-01-15T08:00:00Z",
    "barcode": "DEL-000001",
    "pickup_location": {
      "id": "address-uuid-1",
      "full_address": "123 Main St, New York, NY 10001",
      "latitude": 40.7128,
      "longitude": -74.006,
      "city": "New York",
      "state": "NY",
      "country": "US",
      "postal_code": "10001",
      "contact_info": {
        "full_name": "John Sender",
        "phone": "+1-555-0100",
        "email": "sender@example.com"
      }
    },
    "delivery_location": {
      "id": "address-uuid-2",
      "full_address": "456 Broadway, New York, NY 10013",
      "latitude": 40.7589,
      "longitude": -73.9851,
      "city": "New York",
      "state": "NY",
      "country": "US",
      "postal_code": "10013",
      "contact_info": {
        "full_name": "Jane Receiver",
        "phone": "+1-555-0200",
        "email": "receiver@example.com"
      }
    },
    "delivery_code": "DEL-000001"
  }
}
```

### Response 404 Error - Delivery Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_not_found",
  "message": "Delivery not found"
}
```

## PUT /external/deliveries/{{delivery_code}} — Update Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/deliveries/overview#put-update-delivery-2)

Update an external customer's owned delivery order.

### Access and immutable fields
- An external customer can update only an order whose `customer_id` matches the authenticated customer. A different customer's identifier returns HTTP 404.
- Tenant staff using this endpoint require `manage:operations:tenant` or `manage:module:delivery`.
- `customer_id`, `delivery_service_id`, status, and internal `delivery_flow` are immutable.
- Use the public boolean `is_replacement` to enable or disable replacement handling.
- Status is not accepted here; use the existing external status/confirmation endpoints.

### Editable until final delivery
- `pickup_warehouse_id` or `dropoff_warehouse_id`
- `pickup_location_id` or `pickup_location`
- `delivery_location_id` or `delivery_location`
- `external_reference_id` (string or `null`; trimmed, and an empty string or `null` clears it)
- `cod_amount` (minimum 0)
- `payer_type` (`sender | receiver`)
- `payment_method` (`wallet | cash | cod`; `cod` is stored as `cash`)
- `delivery_fee` (minimum 0)
- `unit_size` (integer, minimum 1)
- `is_replacement` (boolean)
- `notes` (string or `null`; send an empty string or `null` to clear it)
- `package_details`, including `package_details.special_instructions` (string handling instructions) and replacement semantics for `items[]`; each item requires a name, a positive integer quantity, non-negative numeric dimensions, and a valid UUID when `id` is supplied

External references keep the tenant `delivery_external_reference.unique_per_customer` rule. When enabled (default), another root order for the same customer cannot use the same non-empty value; the API returns HTTP 400 with a localized duplicate-reference message and `field=external_reference_id`.

The external API is no longer draft-only. The same rule is used by internal and external updates: an inbound P&D leg delivered to the warehouse does not lock the order, but final parent/outbound `delivered` locks all editable fields.

A non-null warehouse ID takes priority over the matching address ID/JSON and resolves the tenant warehouse address. Send the warehouse field as `null` with a replacement customer address to switch back to a normal address. A missing, cross-tenant, or address-less warehouse fails before the atomic update. Saved non-warehouse address IDs must belong to the authenticated customer. Address and unit changes reuse the fixed service to validate countries/zones and recalculate the fee. An explicit `delivery_fee` wins. For P&D, endpoint warehouse/address changes propagate to the matching operational leg; external reference, notes, and special instructions are stored on the parent and both operational legs in the same atomic update. If an assigned driver cannot serve the new zone, or the larger unit size exceeds assigned vehicle capacity, the API returns HTTP 409 and nothing changes.

Unchanged endpoint submissions and contact-only location updates do not trigger automatic fee recalculation when the effective address and warehouse IDs and every normalized non-contact location field match the saved endpoint. Any extra submitted location field must also match its saved value. These saves preserve the saved non-contact snapshot, including omitted legacy fields, and still run the existing address resolution, coordinate/country validation, atomic assignment checks, and finance rules. Real endpoint or resolved unit-size changes still run service/zone pricing. An explicitly supplied `delivery_fee` remains supported under the existing rules.

### Replacement behavior
- `is_replacement: true` maps the logical order to `delivery_flow = replacement_forward`; `false` removes that classification. A child code updates its logical P&D parent.
- The toggle is editable until final delivery. Final completion and return creation recheck the same state atomically.
- Enabling replacement while the final payment method is cash remains subject to `allow_cod_on_replacement`; a forbidden change returns HTTP 400 `cod_not_allowed_for_replacement_orders`.
- Disabling after the return exists returns HTTP 409 `replacement_return_already_exists`.
- A replacement return cannot be converted; submitting `is_replacement` on it returns HTTP 400 `replacement_return_type_not_editable`.
- A replacement forward follows normal direct/P&D update rules. Its latest addresses, package, unit size, and fixed service are used when the return is generated.
- Return addresses, package details, notes, special instructions, unit size, payer type, and payment method remain editable until the return is delivered. Direct and staged return assignments receive normal zone and vehicle-capacity validation.
- Every replacement return remains zero-fee and zero-COD. Non-zero values return HTTP 400 `replacement_return_delivery_fee_must_be_zero` or `replacement_return_cod_must_be_zero`.

### Finance
- Draft wallet orders resize or move their reservation atomically as fee, payer, or method changes. No wallet transaction or payment is created until confirmation; changing away from wallet releases the hold.
- Non-draft sender-wallet updates refund the full previously settled fee first, then charge the full updated fee. These are two visible wallet transaction lines when both amounts are non-zero.
- Pending sender cash to wallet charges the full current delivery fee.
- Pending sender wallet to cash refunds the full settled wallet fee and leaves the fee pending for pickup collection.
- Sender cash remains a pickup obligation before pickup; later corrections refund any full previous wallet settlement, then charge the full corrected fee through the sender wallet without rewriting historical physical cash.
- Receiver cash or wallet refunds any settled sender fee and stays pending as a final-delivery obligation; no receiver collection/payment row is created by this update.
- COD updates only change the future goods-collection amount; no COD collection, payment, or wallet entry is created before final delivery.
- Successful delivery-fee payment rows are the settled-money source of truth; the delivery payment-status flag does not fabricate a wallet settlement.
- Refund, replacement charge, payment audit, and delivery changes reconcile atomically.
- Replacement-return payer or method changes move no money because its fee remains zero.
- `insufficient_available_balance` or insufficient wallet balance rolls back the update. A missing customer wallet returns HTTP 500.

### Webhook
- External-reference-only, notes-only, `package_details.special_instructions`-only, and replacement-only updates do not emit `order_update`; existing tracked fields remain status, COD, price, payment side, and payment method.
- One request emits at most one existing `order_update` webhook when tracked fields change; P&D child synchronization emits no duplicate request.

### Integration contract
- When a warehouse is selected, send its non-null `pickup_warehouse_id` or `dropoff_warehouse_id`; matching address input is ignored. To deselect it, send the warehouse field as `null` together with the replacement address.
- Send only changed fields and use `is_replacement` for the toggle; never send `delivery_flow`. Send special instructions only as `package_details.special_instructions`; top-level `special_instructions` is rejected.
- Use the returned canonical parent `delivery`; `delivery_flow = replacement_forward` means the toggle is on.
- Show the toggle until final delivery, except for `delivery_flow = replacement_return`, where it is read-only. Keep replacement-return `delivery_fee` and `cod_amount` read-only at zero.
- Handle HTTP 400 with `field=external_reference_id` as a duplicate external reference, plus `invalid_delivery_update_fields`, `invalid_package_details`, `cod_not_allowed_for_replacement_orders`, `replacement_return_type_not_editable`, and the zero-fee/zero-COD errors.
- Handle HTTP 409 `delivery_already_delivered`, `replacement_return_already_exists`, driver zone/capacity errors, `insufficient_available_balance`, `insufficient_balance`, and `delivery_update_conflict`.
- On a conflict, keep the previous local order; reload before retrying.

Authentication: Send an approved tenant API key in X-API-Key.

Request: PUT https://staging-api.ondi.io/functions/v1/external/deliveries/{{delivery_code}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_code | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pickup_warehouse_id | string |  |  |  |
| pickup_location | object |  |  |  |
| pickup_location.full_address | string |  |  |  |
| pickup_location.latitude | number |  |  |  |
| pickup_location.longitude | number |  |  |  |
| pickup_location.city | string |  |  |  |
| pickup_location.country | string |  |  |  |
| dropoff_warehouse_id | null |  |  |  |
| delivery_location_id | string |  |  |  |
| external_reference_id | string |  |  |  |
| cod_amount | number |  |  |  |
| payer_type | string |  |  |  |
| payment_method | string |  |  |  |
| delivery_fee | number |  |  |  |
| unit_size | number |  |  |  |
| is_replacement | boolean |  |  |  |
| package_details | object |  |  |  |
| package_details.special_instructions | string |  |  |  |
| package_details.items | array<object> |  |  |  |
| package_details.items.name | string |  |  |  |
| package_details.items.description | string |  |  |  |
| package_details.items.quantity | number |  |  |  |
| package_details.items.weight | number |  |  |  |
| package_details.items.length | number |  |  |  |
| package_details.items.width | number |  |  |  |
| package_details.items.height | number |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "pickup_warehouse_id": "pickup-warehouse-uuid",
  "pickup_location": {
    "full_address": "Updated pickup address",
    "latitude": 36.191,
    "longitude": 44.01,
    "city": "Erbil",
    "country": "Iraq"
  },
  "dropoff_warehouse_id": null,
  "delivery_location_id": "dropoff-address-uuid",
  "external_reference_id": "CUSTOMER-ORDER-456",
  "cod_amount": 25,
  "payer_type": "receiver",
  "payment_method": "cod",
  "delivery_fee": 31,
  "unit_size": 2,
  "is_replacement": true,
  "package_details": {
    "special_instructions": "Keep upright and avoid heat",
    "items": [
      {
        "name": "Item 1",
        "description": "Updated item",
        "quantity": 1,
        "weight": 3.5,
        "length": 35,
        "width": 25,
        "height": 20
      }
    ]
  },
  "notes": "Call the receiver before delivery"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery updated successfully",
  "currency_code": "IQD",
  "delivery": {
    "id": "parent-delivery-uuid",
    "delivery_code": "2123",
    "status": "in_transit",
    "delivery_flow": "replacement_forward",
    "external_reference_id": "CUSTOMER-ORDER-456",
    "pickup_warehouse_id": "pickup-warehouse-uuid",
    "dropoff_warehouse_id": null,
    "customer_id": "customer-uuid",
    "delivery_service_id": "service-uuid",
    "pickup_location": {
      "full_address": "Updated pickup address",
      "latitude": 36.191,
      "longitude": 44.01
    },
    "delivery_location_id": "dropoff-address-uuid",
    "payer_type": "receiver",
    "payment_method": "cash",
    "delivery_fee": 31,
    "cod_amount": 25,
    "unit_size": 2,
    "package_details": {
      "items": [
        {
          "name": "Item 1",
          "quantity": 1
        }
      ]
    },
    "updated_at": "2026-07-21T12:00:00Z",
    "notes": "Call the receiver before delivery"
  }
}
```

### Response 400 Error - Invalid Update Fields

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid delivery update fields: customer_id, status. Customer, delivery service, status, and internal delivery flow cannot be changed here",
  "field": "customer_id",
  "invalid_fields": [
    "customer_id",
    "status"
  ]
}
```

### Response 400 Error - Duplicate External Reference

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "External reference ID already exists for this customer",
  "field": "external_reference_id"
}
```

### Response 409 Error - Delivered Order Locked

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivered orders cannot be changed"
}
```

### Response 409 Error - Assigned Driver Not Eligible For New Zone

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "The assigned driver is not eligible for the updated pickup or drop-off zone"
}
```

### Response 409 Error - Concurrent Update

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "The order changed while it was being updated. Reload it and try again"
}
```

### Response 409 Wallet Error Contract - External Delivery Update

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_available_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

## POST /external/deliveries/code/{{delivery_code}}/status — Update Delivery Status By Code

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/deliveries/overview#post-update-delivery-status-by-code)

Update a normal delivery status using `delivery_code` instead of delivery ID. The authenticated external caller can update only deliveries they own.

### Behavior
- Uses the same status workflow as the internal delivery endpoint.
- Lookup is by `delivery_code`, but status changes are applied only when the delivery belongs to the authenticated external caller.
- On success, the response is `{ success, error, message, currency_code, delivery }` and `delivery` is loaded from `delivery_list_view`, then enriched for the external contract.

### Responses

`200` success, `400` validation failure, `404` `delivery_not_found`, `409` invalid transition / business conflict.

### External delivery response enrichment
- Top-level response includes `currency_code`. Each delivery object also includes `currency_code`.
- `delivery.track_order` is the public tracking page URL built as `{track_url}/tracking?code={delivery_code}`; `track_url` resolves from tenant `track_orders.track_url`, then the `WEB_URL` secret, then system `external_links.delivery_customer_portal_url`, then `https://staging-app.ondi.io`.
- `delivery.customer_receipt_url` is a signed receipt URL when a successful delivery payment receipt exists; otherwise `null`.
- `delivery.status_updated_at` is derived from the latest `status_updates[].timestamp`, falling back to `updated_at`.
- `delivery.delivery_services` includes `description` and `image_url` when available.
- `delivery.proofs` is grouped as `{ pickup: [], delivery: [] }` using OnDi proof records.
- `delivery.proof_of_pickup` and `delivery.proof_of_delivery` are aggregate proof summaries when captured, shaped as `{ signature, photos }`.
- `delivery.failure_reason` is populated from `delivery_failure_reason` when present.
- Top-level `delivery_type` is not returned; use `delivery.delivery_services.delivery_type`.
- Response `payment_method` is documented as `wallet | cash`.

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/external/deliveries/code/{{delivery_code}}/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_code | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| target_delivery_status | string |  | One of `draft`, `pending`, `assigned`, `en_route_to_pickup`, `picked_up`, `in_transit`, `delivered`, `cancelled`, `returned`. |  |
| reason | string | No | Transition reason. If `returned_items` is sent as a string array, this reason is copied to each item; otherwise the default is `returned`. |  |
| driver_id | string |  | Required when the requested status needs an assignment, the delivery has no active assignment, and tenant settings do not define `assignment_change_settings.default_driver_id`. |  |
| notes | string | No | Timeline / workflow notes. |  |
| proof_of_delivery | object | No | Proof payload for delivered flows. Only `signature_url` and `photo_urls` are accepted. |  |
| proof_of_delivery.signature_url | string | No | Signature asset URL. Some delivered flows may require it based on tenant settings. |  |
| proof_of_delivery.photo_urls | array<string> | No | Proof photo URLs. Some delivered flows may require at least one photo based on tenant settings. |  |
| bin_id | string | No | Internal bin identifier passed through when relevant to the status workflow. |  |
| returned_items | string | No | Used for return flows. (string form):  Inventory item ID. The backend converts it to `{ id, reason }` using top-level `reason` or `returned`. |  |
| returned_items[].id |  |  | Required when object form is used \| `string` \| Inventory item ID. |  |
| returned_items[].reason |  |  | Required when object form is used \| `string` \| Return reason for that item. |  |



### Request body (raw)

```json
{
  "target_delivery_status": "delivered",
  "reason": "Delivery completed successfully",
  "driver_id": "{{driver_id}}",
  "notes": "Delivered by external integration",
  "proof_of_delivery": {
    "signature_url": "{{signature_url}}",
    "photo_urls": [
      "{{proof_photo_url_1}}"
    ]
  }
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery status updated successfully",
  "delivery": {
    "id": "DEL-000001",
    "status": "delivered",
    "tenant_id": "tenant-uuid",
    "external_reference_id": "EXT-REF-123",
    "customer_id": "customer-uuid",
    "delivery_service_id": "service-uuid",
    "payer_type": "sender",
    "payment_method": "wallet",
    "delivery_fee": 15.5,
    "cod_amount": 0,
    "notes": "Package delivery",
    "unit_size": 1,
    "tracking_provider": "internal",
    "carrier_type": "internal",
    "pickup_warehouse_id": null,
    "dropoff_warehouse_id": null,
    "pickup_location_id": "address-uuid-1",
    "delivery_location_id": "address-uuid-2",
    "receiver_user_id": null,
    "assigned_carrier_id": null,
    "assigned_to_user_id": null,
    "package_details": {
      "special_instructions": "Handle with care"
    },
    "scheduled_pickup_time": "2024-01-15T10:00:00Z",
    "scheduled_delivery_time": "2024-01-15T14:00:00Z",
    "started_at": null,
    "picked_up_at": null,
    "delivered_at": null,
    "cancelled_at": null,
    "delivery_failure_reason": null,
    "created_at": "2024-01-15T08:00:00Z",
    "updated_at": "2024-01-15T08:30:00Z",
    "barcode": "DEL-000001",
    "delivery_code": "DEL-000001"
  }
}
```

### Response 400 Error - Driver Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "driver_id_required_for_delivery_status",
  "message": "Driver ID is required for delivery status"
}
```

### Response 404 Error - Delivery Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_not_found",
  "message": "Delivery not found"
}
```

### Response 500 Wallet Error Contract - External Draft Status

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_available_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

## DELETE /external/deliveries/{{delivery_code}} — Delete Delivery (Legacy Admin Cancellation)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/deliveries/overview#delete-delete-delivery-legacy-admin-cancellation)

Legacy tenant-admin cancellation endpoint. The delivery record is retained. This endpoint delegates to the canonical atomic logical-order cancellation flow, so a parent or operational child resolves to the root, the order family and active work are cancelled consistently, and paid family delivery fees or collected COD are refunded exactly once to the sender wallet. Separate replacement-return orders are excluded. Requires `manage:operations:tenant` or system-admin access; customer API keys must use `POST /external/deliveries/{delivery_code}/cancel`.

### Responses

`200` success, `403` unauthorized, `404` delivery not found, `409` terminal logical-order conflict. Admin callers may cancel assigned work.

### Frontend integration contract

treat success as cancellation, not physical deletion. Keep the record and render it with cancelled status. Prefer the explicit POST cancellation endpoint for new integrations.

Authentication: Send an approved tenant API key in X-API-Key.

Request: DELETE https://staging-api.ondi.io/functions/v1/external/deliveries/{{delivery_code}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_code | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery deleted successfully"
}
```

### Response 409 Error - Cancellation Conflict

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "cannot_cancel_delivery_in_terminal_status"
}
```

## POST /external/deliveries/{{delivery_code}}/cancel — Cancel Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/deliveries/overview#post-cancel-delivery-2)

Cancel a delivery owned by the authenticated external customer. Ownership is enforced before cancellation even if the auth context contains an accidental staff permission; another customer’s delivery returns 404. Cancellation is transactionally blocked with HTTP 409 `cannot_cancel_delivery_after_driver_assignment` once an assignment in the logical order is active, and with HTTP 409 `cannot_cancel_delivery_in_terminal_status` after final delivery or another terminal logical state.

- Empty body: Allowed | N/A | This endpoint accepts no JSON body.

### Staged/P&D behavior

the parent or any operational child identifier resolves to the canonical parent. A completed inbound warehouse leg does not block cancellation while final delivery remains incomplete. Parent, non-terminal operational children, assignments, receiving work, handoffs, and eligible picking work are cancelled atomically. Paid family delivery fees and collected COD are refunded once to the sender wallet. Separate replacement-return orders are excluded.

### Responses

`200` success, `400` validation failure, `403` unauthorized, `404` `delivery_not_found`, `409` assigned-work or terminal logical-order conflict.

### External delivery response enrichment
- Top-level response includes `currency_code`. Each delivery object also includes `currency_code`.
- The returned delivery is the canonical logical root; `delivery.id` is its public delivery code and matches `delivery.delivery_code`.
- `delivery.track_order` is the public tracking page URL built as `{track_url}/tracking?code={delivery_code}`; `track_url` resolves from tenant `track_orders.track_url`, then the `WEB_URL` secret, then system `external_links.delivery_customer_portal_url`, then `https://staging-app.ondi.io`.
- `delivery.customer_receipt_url` is a signed receipt URL when a successful delivery payment receipt exists; otherwise `null`.
- `delivery.status_updated_at` is derived from the latest `status_updates[].timestamp`, falling back to `updated_at`.
- `delivery.delivery_services` includes `description` and `image_url` when available.
- `delivery.proofs` is grouped as `{ pickup: [], delivery: [] }` using OnDi proof records.
- `delivery.proof_of_pickup` and `delivery.proof_of_delivery` are aggregate proof summaries when captured, shaped as `{ signature, photos }`.
- `delivery.failure_reason` is populated from `delivery_failure_reason` when present.
- Top-level `delivery_type` is not returned; use `delivery.delivery_services.delivery_type`.
- Response `payment_method` is documented as `wallet | cash`.

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/external/deliveries/{{delivery_code}}/cancel

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_code | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| failure_reason | string | No | Reason stored in `delivery_failure_reason` and the status timeline. |  |
| notes | string | No | Additional timeline notes. |  |



### Request body (raw)

```json
{
  "failure_reason": "Customer changed mind",
  "notes": "Order cancelled per customer request"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery cancelled successfully",
  "delivery": {
    "id": "DEL-000001",
    "status": "cancelled",
    "tenant_id": "tenant-uuid",
    "external_reference_id": "EXT-REF-123",
    "customer_id": "customer-uuid",
    "delivery_service_id": "service-uuid",
    "payer_type": "sender",
    "payment_method": "wallet",
    "delivery_fee": 15.5,
    "cod_amount": 0,
    "notes": "Order cancelled per customer request",
    "unit_size": 1,
    "tracking_provider": "internal",
    "carrier_type": "internal",
    "pickup_warehouse_id": null,
    "dropoff_warehouse_id": null,
    "pickup_location_id": "address-uuid-1",
    "delivery_location_id": "address-uuid-2",
    "receiver_user_id": null,
    "assigned_carrier_id": null,
    "assigned_to_user_id": null,
    "package_details": {
      "special_instructions": "Handle with care"
    },
    "scheduled_pickup_time": "2024-01-15T10:00:00Z",
    "scheduled_delivery_time": "2024-01-15T14:00:00Z",
    "started_at": null,
    "picked_up_at": null,
    "delivered_at": null,
    "cancelled_at": "2024-01-15T09:00:00Z",
    "delivery_failure_reason": "Customer changed mind",
    "created_at": "2024-01-15T08:00:00Z",
    "updated_at": "2024-01-15T09:00:00Z",
    "barcode": "DEL-000001",
    "delivery_code": "DEL-000001"
  }
}
```

### Response 400 Error - Cannot Cancel

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery cannot be cancelled"
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "unauthorized",
  "message": "Unauthorized"
}
```

### Response 404 Error - Delivery Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_not_found",
  "message": "Delivery not found"
}
```

## POST /external/deliveries/{{delivery_code}}/confirm — Confirm Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/deliveries/overview#post-confirm-delivery)

Confirm (execute) a draft delivery by transitioning its status from 'draft' to 'pending'. The delivery becomes available for assignment and execution.

This endpoint has no request body; it is fully driven by the delivery's current status.

### Business Rules
- External customers can confirm only a delivery they own; another customer’s delivery returns 404
- Delivery must have status = 'draft'
- A positive wallet fee must already be reserved; confirmation captures it and creates the delivery-fee transaction/payment atomically
- For staged parents, the outbound child must not be in 'draft' status (already executed)

### Responses

200 success, 409 invalid_status_transition / insufficient_available_balance / wallet reservation conflict, 404 delivery_not_found. The reservation contract returns no currency; use tenant configuration.

### External delivery response enrichment
- Top-level response includes currency_code. Each delivery object also includes currency_code.
- delivery.id is the public delivery code and matches delivery.delivery_code.
- delivery.track_order is the public tracking page URL built as {track_url}/tracking?code={delivery_code}; track_url resolves from tenant track_orders.track_url, then WEB_URL secret, then system external_links.delivery_customer_portal_url, then https://staging-app.ondi.io.
- delivery.customer_receipt_url is a signed receipt URL when a successful delivery payment receipt exists; otherwise null.
- delivery.status_updated_at is derived from the latest status_updates[].timestamp, falling back to updated_at.
- delivery.delivery_services includes description and image_url when available.
- delivery.proofs is grouped as { pickup: [], delivery: [] } using OnDi proof records.
- delivery.proof_of_pickup and delivery.proof_of_delivery are aggregate proof summaries when captured, shaped as { signature, photos }.
- delivery.failure_reason is populated from delivery_failure_reason when present.
- Top-level delivery_type is not returned; use delivery.delivery_services.delivery_type.
- payment_method is documented as wallet|cash.

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/external/deliveries/{{delivery_code}}/confirm

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_code | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery status updated successfully",
  "delivery": {
    "id": "DEL-000001",
    "status": "pending"
  }
}
```

### Response 409 Already Executed



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "invalid_status_transition",
  "message": "Invalid status transition"
}
```

### Response 404 Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_not_found",
  "message": "Delivery not found"
}
```

### Response 500 Wallet Error Contract - External Confirm



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_available_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

## POST /external/international-deliveries — Create International Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/deliveries/external-international-delivery#post-create-international-delivery-2)

Create an international delivery using a warehouse pickup and a delivery address object.

- Requires X-API-Key authentication.

### Key behavior
- Pickup address is resolved from `warehouse_id`; do not send `pickup_address`.
- Creates inventory items from `products`.
- Creates a receiving order tied to the created inventory item(s).
- Creates the delivery with status `draft`. If payment is wallet and the fee is positive, the fee is moved from usable wallet balance to `reserved` without a wallet transaction or payment. Confirmation captures it; draft cancellation releases it.

### Notes
- `reference_number` for the receiving order is auto-generated as `RIO-<timestamp>`.
- `receiving_orders.status` is created as `pending`.
- `delivery.status` is created as `draft`. Reservation data does not return currency; integrations use tenant configuration.

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/external/international-deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string | Yes | Warehouse UUID used as the pickup source. |  |
| delivery_address | object | Yes | Delivery address payload. |  |
| delivery_address.lat | number | Yes | Delivery latitude. |  |
| delivery_address.lng | number | Yes | Delivery longitude. |  |
| delivery_address.full_address | string | Yes | Full delivery address text. |  |
| delivery_address.city | string | No | City name. |  |
| delivery_address.state | string | No | State or province. |  |
| delivery_address.country | string | No | Country name or code. |  |
| delivery_address.postal_code | string | No | Postal or ZIP code. |  |
| delivery_address.contact_info | object | No | Receiver contact details. |  |
| delivery_address.contact_info.full_name | string | No | Receiver full name. |  |
| delivery_address.contact_info.phone | string | No | Receiver phone number. |  |
| delivery_address.contact_info.email | string | No | Receiver email address. |  |
| delivery_address.contact_info.tel_phone | string | No | Alternate receiver phone value used by some integrations. |  |
| customer_id | string |  | Required for non-customer callers. For customer/no-role callers it is auto-filled from the authenticated user. |  |
| delivery_service_id | string | Yes | Delivery service UUID. |  |
| payer_type | string | Yes | Supported values: `sender`, `receiver`. |  |
| payment_method | string | Yes | Accepted values: `wallet`, `cash`, `cod`. `cod` is normalized to `cash`. |  |
| external_reference_id | string | Yes | External reference. When `delivery_external_reference.unique_per_customer=true` (default), it must be unique among root orders for the same tenant customer; when false, duplicates are allowed. |  |
| order_number | string | Yes | Customer-facing order number used on shipment labels. |  |
| cod_amount | number | No | Cash-on-delivery amount. Use `0` when not applicable. |  |
| notes | string | No | General delivery / receiving notes. |  |
| scheduled_pickup_time | string | No | ISO 8601 datetime for planned pickup. |  |
| scheduled_delivery_time | string | No | ISO 8601 datetime for planned delivery. |  |
| unit_size | number | No | Positive unit size override. If omitted, the backend uses `products` totals and may derive size from `package_details.items`. |  |
| tracking_provider | string | No | Tracking provider identifier. |  |
| package_details | object | No | Parcel-level metadata. |  |
| package_details.special_instructions | string | No | Handling / packing notes. |  |
| package_details.items | array<object> | No | Parcel items used for unit-size calculation when needed. |  |
| package_details.items[].name | string | No | Item name. |  |
| package_details.items[].description | string | No | Item description. |  |
| package_details.items[].quantity | number | No | Quantity for unit-size calculation. Defaults to `1` when omitted. |  |
| package_details.items[].weight | number | No | Item weight. |  |
| package_details.items[].length | number | No | Item length. |  |
| package_details.items[].width | number | No | Item width. |  |
| package_details.items[].height | number | No | Item height. |  |
| products | array<object> | Yes | Minimum 1 product. Used to create inventory items. |  |
| products[].name | string | Yes | Product name. |  |
| products[].sku | string | Yes | SKU. |  |
| products[].barcode | string | Yes | Barcode. |  |
| products[].unit_size | number | Yes | Unit size for this product line. |  |
| products[].quantity | number | Yes | Quantity for this product line. |  |
| products[].description | string | No | Product description. |  |
| products[].brand | string | No | Brand name. |  |
| products[].model | string | No | Model name or number. |  |
| products[].category | string | No | Product category. |  |
| products[].subcategory | string | No | Product subcategory. |  |
| sender | object | Yes | Sender details used for labels / inventory linkage. |  |
| sender.name | string | Yes | Sender name. |  |
| sender.phone | string | Yes | Sender phone number. |  |
| sender.full_address | string | No | Sender full address. Recommended; omitted values fall back to an empty string in label data. |  |
| expected_arrival_date | string | No | Receiving order ETA date (`YYYY-MM-DD`). |  |
| receiving_details | object | No | Free-form receiving order metadata. Example keys: `carrier`, `incoterms`. |  |
| actual_arrival_date | string \| null | No | Present in the sample payload, but not currently consumed by the external handler. |  |
| received_by | string \| null | No | Present in the sample payload, but not currently consumed by the external handler. |  |
| supplier_id | string \| null | No | Supplier identifier passed into receiving-order creation. |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "delivery_address": {
    "lat": {{delivery_lat}},
    "lng": {{delivery_lng}},
    "full_address": "{{delivery_full_address}}",
    "city": "{{delivery_city}}",
    "state": "{{delivery_state}}",
    "country": "{{delivery_country}}",
    "postal_code": "{{delivery_postal_code}}",
    "contact_info": {
      "full_name": "{{receiver_name}}",
      "phone": "{{receiver_phone}}",
      "email": "{{receiver_email}}",
      "tel_phone": "{{receiver_tel}}"
    }
  },
  "customer_id": "{{customer_id}}",
  "delivery_service_id": "{{delivery_service_id}}",
  "payer_type": "sender",
  "payment_method": "wallet",
  "external_reference_id": "{{external_reference_id}}",
  "order_number": "{{order_number}}",
  "cod_amount": 0,
  "notes": "International shipment",
  "scheduled_pickup_time": "2024-01-15T10:00:00Z",
  "scheduled_delivery_time": "2024-01-20T18:00:00Z",
  "unit_size": 1,
  "tracking_provider": "internal",
  "package_details": {
    "special_instructions": "International shipment - handle with care",
    "items": [
      {
        "name": "{{item_name}}",
        "description": "{{item_description}}",
        "quantity": 1,
        "weight": 2.5,
        "length": 30,
        "width": 20,
        "height": 15
      }
    ]
  },
  "products": [
    {
      "name": "Widget A",
      "sku": "WIDGET-A",
      "barcode": "1234567890123",
      "unit_size": 1,
      "quantity": 2,
      "description": "Blue widget",
      "brand": "Acme",
      "model": "A1",
      "category": "Widgets",
      "subcategory": "Standard"
    },
    {
      "name": "Gadget B",
      "sku": "GADGET-B",
      "barcode": "9876543210987",
      "unit_size": 1,
      "quantity": 1
    }
  ],
  "sender": {
    "name": "{{sender_name}}",
    "phone": "{{sender_phone}}",
    "full_address": "{{sender_full_address}}"
  },
  "expected_arrival_date": "2024-01-19",
  "receiving_details": {
    "carrier": "DHL",
    "incoterms": "DAP"
  },
  "actual_arrival_date": null,
  "received_by": null,
  "supplier_id": null
}
```

### Response 201 Success - Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery created successfully",
  "delivery": {
    "id": "DEL-000001",
    "delivery_code": "DEL-000001"
  },
  "pickup_address_id": "warehouse-address-uuid",
  "delivery_address": {
    "id": "address-uuid",
    "full_address": "456 Broadway..."
  },
  "inventory_item": {
    "id": "item-uuid-1",
    "sku": "WIDGET-A",
    "name": "Widget A"
  },
  "receiving_order": {
    "id": "ro-uuid",
    "reference_number": "RIO-1700000000000",
    "status": "pending"
  }
}
```

### Response 400 Error - Missing tenant_id

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "tenant_id_required",
  "message": "Tenant ID is required"
}
```

### Response 400 Error - Validation (products array)

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "products_array_cannot_be_empty",
  "message": "Products array cannot be empty"
}
```

### Response 404 Error - Warehouse Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "warehouse_not_found",
  "message": "Warehouse not found"
}
```

### Response 400 Error - Delivery Address Creation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_address_creation_failed",
  "message": "Delivery address creation failed"
}
```

### Response 400 Error - External Reference Already Exists

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "external_reference_id_already_exists",
  "message": "External reference ID already exists for this customer"
}
```

### Response 500 Error - Server Error (inventory creation)

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "internal_server_error",
  "message": "Internal Server Error"
}
```

## POST /external/returns — Create Return Request

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/returns#post-create-return-request)

Create a return request for a delivered delivery (Customer-only endpoint).

### External API note

This endpoint accepts `delivery_code` instead of `delivery_id` (UUID). The system resolves the external delivery code internally. When auto-approved, `return_delivery_id` is also returned as the public return delivery code.

### Security
- This endpoint is customer-only. The authenticated user must be a customer.
- The delivery must belong to the authenticated customer. Returns `403` if the delivery belongs to a different customer.

### Business rules
- Delivery must be in `delivered` status.
- Only one non-cancelled return request is allowed per delivery.
- Inserts `return_requests` with status `pending` and updates `deliveries.return_status = return_requested`.
- If tenant setting `return_requests.auto_approval = true`, the system auto-approves and creates a return delivery using `return_requests.default_delivery_service_id` and destination rules from `return_requests.destination_mode`.

### Return delivery payment settings
- Return deliveries are always created with `payment_method = wallet` and `payer_type = sender` (customer pays via wallet), regardless of the original delivery's payment settings.

### Responses
- `201` success (pending or auto-approved)
- `400` validation / not eligible / missing fields
- `401` unauthorized (customer authentication required)
- `403` `unauthorized_delivery_access` (delivery does not belong to customer)
- `404` `delivery_not_found` / `tenant_not_found`

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/external/returns

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_code | string | Yes | External delivery code, for example `DEL-000001`. |  |
| return_reason | string | Yes | Non-empty reason for the return. |  |
| notes | string | No | Additional customer note. Empty strings are treated as `null`. |  |



### Request body (raw)

```json
{
  "delivery_code": "DEL-000001",
  "return_reason": "Damaged product",
  "notes": "Package arrived with visible damage"
}
```

### Response 201 Success - Pending

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Return request created",
  "return_request_id": "return-request-uuid",
  "auto_approved": false
}
```

### Response 201 Success - Auto Approved

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Return request created",
  "return_request_id": "return-request-uuid",
  "auto_approved": true,
  "return_delivery_id": "RET-000001"
}
```

### Response 404 Error - Delivery Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_not_found",
  "message": "Delivery not found"
}
```

### Response 400 Error - Delivery Not Eligible

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_not_eligible_for_return",
  "message": "Delivery not eligible for return"
}
```

### Response 400 Error - Missing delivery_code

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "field_required",
  "message": "Required field is missing"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "unauthorized",
  "message": "unauthorized"
}
```

### Response 403 Error - Unauthorized Delivery Access

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "unauthorized_delivery_access",
  "message": "Unauthorized delivery access"
}
```

## GET /external/warehouses — List Warehouses

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/warehouses#get-list-warehouses)

List tenant warehouses with filtering and pagination.

tenant_id

### Responses

200 success; 403 insufficient_permissions; 404 tenant_not_found; 500 internal_server_error.

Authentication: Send an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/external/warehouses?page=1&limit=10&status=&code=&search=&total_capacity_order=&available_capacity_order=&customer_id=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number (optional, default 1) | 1 |
| limit | query string | No | Items per page (optional, default 10) | 10 |
| status | query string | No | Filter by status (optional) |  |
| code | query string | No | Filter by code (optional) |  |
| search | query string | No | Search name/code (optional) |  |
| total_capacity_order | query string | No | Order by total_capacity (optional) (asc\|desc) | asc |
| available_capacity_order | query string | No | Order by available_capacity (optional) (asc\|desc) | asc |
| customer_id | query string | No | Filter by customer access (optional) |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Warehouses retrieved successfully",
  "warehouses": [
    {
      "id": "wh-uuid",
      "name": "Main WH",
      "code": "WH-001",
      "address": {
        "id": "addr",
        "full_address": "..."
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 404 Error - Missing tenant_id

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "tenant_not_found",
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "insufficient_permissions",
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "internal_server_error",
  "message": "Internal Server Error"
}
```

## GET /external/delivery-services — List Delivery Services

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/delivery-services#get-list-delivery-services-2)

List delivery services with filtering, access control, and pagination.

tenant_id

### Fields

id, name, code, description, service_level, default_price, estimated_delivery_time_min, estimated_delivery_time_max, service_options, image_url, unit_min, unit_max, delivery_type(id,name,code), active.

### Responses

200 success; 404 tenant_not_found; 500 internal_server_error.

Authentication: Send an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/external/delivery-services?page=1&limit=10&service_level=&search=&active=&delivery_type=&delivery_type_code=&price_order=&customer_id=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number (optional, default 1) | 1 |
| limit | query string | No | Items per page (optional, default 10) | 10 |
| service_level | query string | No | Filter by service level (optional) |  |
| search | query string | No | Search by name/code (optional) |  |
| active | query string | No | Filter by active status (true/false) (optional) |  |
| delivery_type | query string | No | Filter by delivery_type_id (optional) |  |
| delivery_type_code | query string | No | Filter by delivery type code (optional) |  |
| price_order | query string | No | Order by default_price (asc\|desc) (optional) |  |
| customer_id | query string | No | Customer filter for access control (optional) |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery services retrieved successfully",
  "deliveryServices": [
    {
      "id": "svc-uuid",
      "name": "Standard",
      "code": "STD",
      "description": "Standard delivery",
      "service_level": "standard",
      "default_price": 15.5,
      "estimated_delivery_time_min": 60,
      "estimated_delivery_time_max": 120,
      "service_options": {},
      "image_url": "https://cdn.example.com/services/std.png",
      "unit_min": 1,
      "unit_max": 10,
      "delivery_type": {
        "id": "dt-uuid",
        "name": "Local",
        "code": "LOCAL"
      },
      "active": true
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "tenant_not_found",
  "message": "Tenant not found"
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "internal_server_error",
  "message": "Internal Server Error"
}
```

## GET /external/delivery-types — List Delivery Types

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/delivery-types#get-list-delivery-types-2)

List all delivery types available.

### Responses

200 success; 500 internal_server_error.

Authentication: Send an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/external/delivery-types

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery types retrieved successfully",
  "deliveryTypes": [
    {
      "id": "dt-uuid-1",
      "name": "Local",
      "code": "LOCAL"
    },
    {
      "id": "dt-uuid-2",
      "name": "International",
      "code": "INTL"
    }
  ]
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "internal_server_error",
  "message": "Internal Server Error"
}
```

## GET /external/api-key/{{api_key_plain}}/health — API Key Health Check

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/api-keys#get-api-key-health-check)

Validates an API key and returns its metadata and enabled modules.

### Responses

200 valid; 400 api_key_required; 401 invalid/revoked/expired; 404 not found; 500 internal_server_error.

Authentication: Send an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/external/api-key/{{api_key_plain}}/health

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| api_key_plain | path string | Yes | The raw API key. If it starts with 'od_', the prefix is ignored during hashing. |  |



### Response 200 Success - Valid

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "API key is valid",
  "id": "key-uuid",
  "name": "Integration Key",
  "created_at": "2024-01-01T00:00:00Z",
  "created_by": "user-uuid",
  "expires_at": null,
  "is_active": true,
  "revoked_at": null,
  "modules": [
    "deliveries",
    "zones",
    "pricing"
  ]
}
```

### Response 400 Error - API Key Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "api_key_required",
  "message": "API key is required"
}
```

### Response 404 Error - Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "api_key_not_found",
  "message": "API key not found"
}
```

### Response 401 Error - Invalid/Revoked/Expired

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "api_key_unavailable",
  "message": "API key is revoked, expired, or inactive"
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "internal_server_error",
  "message": "Internal Server Error"
}
```

## POST /external/international-deliveries — Create International Delivery

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/external-carrier-integrations/external-international-delivery#post-create-international-delivery-3)

Create an international delivery using a warehouse pickup and a delivery address object.

- Requires X-API-Key authentication.

### Key behavior
- Pickup address is resolved from `warehouse_id`; do not send `pickup_address`.
- Creates inventory items from `products`.
- Creates a receiving order tied to the created inventory item(s).
- Creates the delivery with status `draft`.

### Notes
- `reference_number` for the receiving order is auto-generated as `RIO-<timestamp>`.
- `receiving_orders.status` is created as `pending`.
- `delivery.status` is created as `draft`.

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/external/international-deliveries

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{api_key}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string | Yes | Warehouse UUID used as the pickup source. |  |
| delivery_address | object | Yes | Delivery address payload. |  |
| delivery_address.lat | number | Yes | Delivery latitude. |  |
| delivery_address.lng | number | Yes | Delivery longitude. |  |
| delivery_address.full_address | string | Yes | Full delivery address text. |  |
| delivery_address.city | string | No | City name. |  |
| delivery_address.state | string | No | State or province. |  |
| delivery_address.country | string | No | Country name or code. |  |
| delivery_address.postal_code | string | No | Postal or ZIP code. |  |
| delivery_address.contact_info | object | No | Receiver contact details. |  |
| delivery_address.contact_info.full_name | string | No | Receiver full name. |  |
| delivery_address.contact_info.phone | string | No | Receiver phone number. |  |
| delivery_address.contact_info.email | string | No | Receiver email address. |  |
| delivery_address.contact_info.tel_phone | string | No | Alternate receiver phone value used by some integrations. |  |
| customer_id | string |  | Required for non-customer callers. For customer/no-role callers it is auto-filled from the authenticated user. |  |
| delivery_service_id | string | Yes | Delivery service UUID. |  |
| payer_type | string | Yes | Supported values: `sender`, `receiver`. |  |
| payment_method | string | Yes | Accepted values: `wallet`, `cash`, `cod`. `cod` is normalized to `cash`. |  |
| external_reference_id | string | Yes | External reference. When `delivery_external_reference.unique_per_customer=true` (default), it must be unique among root orders for the same tenant customer; when false, duplicates are allowed. |  |
| order_number | string | Yes | Customer-facing order number used on shipment labels. |  |
| cod_amount | number | No | Cash-on-delivery amount. Use `0` when not applicable. |  |
| notes | string | No | General delivery / receiving notes. |  |
| scheduled_pickup_time | string | No | ISO 8601 datetime for planned pickup. |  |
| scheduled_delivery_time | string | No | ISO 8601 datetime for planned delivery. |  |
| unit_size | number | No | Positive unit size override. If omitted, the backend uses `products` totals and may derive size from `package_details.items`. |  |
| tracking_provider | string | No | Tracking provider identifier. |  |
| package_details | object | No | Parcel-level metadata. |  |
| package_details.special_instructions | string | No | Handling / packing notes. |  |
| package_details.items | array<object> | No | Parcel items used for unit-size calculation when needed. |  |
| package_details.items[].name | string | No | Item name. |  |
| package_details.items[].description | string | No | Item description. |  |
| package_details.items[].quantity | number | No | Quantity for unit-size calculation. Defaults to `1` when omitted. |  |
| package_details.items[].weight | number | No | Item weight. |  |
| package_details.items[].length | number | No | Item length. |  |
| package_details.items[].width | number | No | Item width. |  |
| package_details.items[].height | number | No | Item height. |  |
| products | array<object> | Yes | Minimum 1 product. Used to create inventory items. |  |
| products[].name | string | Yes | Product name. |  |
| products[].sku | string | Yes | SKU. |  |
| products[].barcode | string | Yes | Barcode. |  |
| products[].unit_size | number | Yes | Unit size for this product line. |  |
| products[].quantity | number | Yes | Quantity for this product line. |  |
| products[].description | string | No | Product description. |  |
| products[].brand | string | No | Brand name. |  |
| products[].model | string | No | Model name or number. |  |
| products[].category | string | No | Product category. |  |
| products[].subcategory | string | No | Product subcategory. |  |
| sender | object | Yes | Sender details used for labels / inventory linkage. |  |
| sender.name | string | Yes | Sender name. |  |
| sender.phone | string | Yes | Sender phone number. |  |
| sender.full_address | string | No | Sender full address. Recommended; omitted values fall back to an empty string in label data. |  |
| expected_arrival_date | string | No | Receiving order ETA date (`YYYY-MM-DD`). |  |
| receiving_details | object | No | Free-form receiving order metadata. Example keys: `carrier`, `incoterms`. |  |
| actual_arrival_date | string \| null | No | Present in the sample payload, but not currently consumed by the external handler. |  |
| received_by | string \| null | No | Present in the sample payload, but not currently consumed by the external handler. |  |
| supplier_id | string \| null | No | Supplier identifier passed into receiving-order creation. |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "delivery_address": {
    "lat": {{delivery_lat}},
    "lng": {{delivery_lng}},
    "full_address": "{{delivery_full_address}}",
    "city": "{{delivery_city}}",
    "state": "{{delivery_state}}",
    "country": "{{delivery_country}}",
    "postal_code": "{{delivery_postal_code}}",
    "contact_info": {
      "full_name": "{{receiver_name}}",
      "phone": "{{receiver_phone}}",
      "email": "{{receiver_email}}",
      "tel_phone": "{{receiver_tel}}"
    }
  },
  "customer_id": "{{customer_id}}",
  "delivery_service_id": "{{delivery_service_id}}",
  "payer_type": "sender",
  "payment_method": "wallet",
  "external_reference_id": "{{external_reference_id}}",
  "order_number": "{{order_number}}",
  "cod_amount": 0,
  "notes": "International shipment",
  "scheduled_pickup_time": "2024-01-15T10:00:00Z",
  "scheduled_delivery_time": "2024-01-20T18:00:00Z",
  "unit_size": 1,
  "tracking_provider": "internal",
  "package_details": {
    "special_instructions": "International shipment - handle with care",
    "items": [
      {
        "name": "{{item_name}}",
        "description": "{{item_description}}",
        "quantity": 1,
        "weight": 2.5,
        "length": 30,
        "width": 20,
        "height": 15
      }
    ]
  },
  "products": [
    {
      "name": "Widget A",
      "sku": "WIDGET-A",
      "barcode": "1234567890123",
      "unit_size": 1,
      "quantity": 2,
      "description": "Blue widget",
      "brand": "Acme",
      "model": "A1",
      "category": "Widgets",
      "subcategory": "Standard"
    },
    {
      "name": "Gadget B",
      "sku": "GADGET-B",
      "barcode": "9876543210987",
      "unit_size": 1,
      "quantity": 1
    }
  ],
  "sender": {
    "name": "{{sender_name}}",
    "phone": "{{sender_phone}}",
    "full_address": "{{sender_full_address}}"
  },
  "expected_arrival_date": "2024-01-19",
  "receiving_details": {
    "carrier": "DHL",
    "incoterms": "DAP"
  },
  "actual_arrival_date": null,
  "received_by": null,
  "supplier_id": null
}
```

### Response 201 Success - Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "error": null,
  "message": "Delivery created successfully",
  "delivery": {
    "id": "DEL-INT-000001",
    "status": "draft",
    "tenant_id": "tenant-uuid",
    "external_reference_id": "EXT-REF-INT-123",
    "customer_id": "customer-uuid",
    "delivery_service_id": "service-uuid",
    "payer_type": "sender",
    "payment_method": "wallet",
    "delivery_fee": 45,
    "cod_amount": 0,
    "notes": "International shipment",
    "unit_size": 1,
    "tracking_provider": "internal",
    "carrier_type": "internal",
    "pickup_warehouse_id": "warehouse-uuid",
    "dropoff_warehouse_id": null,
    "pickup_location_id": "warehouse-address-uuid",
    "delivery_location_id": "address-uuid",
    "receiver_user_id": null,
    "assigned_carrier_id": null,
    "assigned_to_user_id": null,
    "package_details": {
      "special_instructions": "International shipment - handle with care",
      "items": [
        {
          "name": "Item 1",
          "description": "Widget",
          "quantity": 1,
          "weight": 2.5
        }
      ]
    },
    "scheduled_pickup_time": "2024-01-15T10:00:00Z",
    "scheduled_delivery_time": "2024-01-20T18:00:00Z",
    "started_at": null,
    "picked_up_at": null,
    "delivered_at": null,
    "cancelled_at": null,
    "delivery_failure_reason": null,
    "created_at": "2024-01-15T08:00:00Z",
    "updated_at": "2024-01-15T08:00:00Z",
    "barcode": "DEL-INT-000001",
    "delivery_code": "DEL-INT-000001"
  },
  "pickup_address_id": "warehouse-address-uuid",
  "delivery_address": {
    "id": "address-uuid",
    "full_address": "456 Broadway, New York, NY 10013",
    "latitude": 40.7589,
    "longitude": -73.9851,
    "city": "New York",
    "state": "NY",
    "country": "US",
    "postal_code": "10013",
    "contact_info": {
      "full_name": "Jane Receiver",
      "phone": "+1-555-0200",
      "email": "receiver@example.com"
    }
  },
  "inventory_item": {
    "id": "item-uuid-1",
    "sku": "WIDGET-A",
    "name": "Widget A"
  },
  "receiving_order": {
    "id": "ro-uuid",
    "reference_number": "RIO-1700000000000",
    "status": "pending"
  }
}
```

### Response 400 Error - Missing tenant_id

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "tenant_id_required",
  "message": "Tenant ID is required"
}
```

### Response 400 Error - Validation (products array)

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "products_array_cannot_be_empty",
  "message": "Products array cannot be empty"
}
```

### Response 404 Error - Warehouse Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "warehouse_not_found",
  "message": "Warehouse not found"
}
```

### Response 400 Error - Delivery Address Creation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "delivery_address_creation_failed",
  "message": "Delivery address creation failed"
}
```

### Response 400 Error - External Reference Already Exists

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "external_reference_id_already_exists",
  "message": "External reference ID already exists for this customer"
}
```

### Response 500 Error - Server Error (inventory creation)

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "internal_server_error",
  "message": "Internal Server Error"
}
```

## POST /messaging/dispatcher-conversations — Create Driver Dispatcher Order Conversation

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/conversations#post-create-driver-dispatcher-order-conversation)

Creates or returns an active private conversation for the authenticated driver, selected dispatcher, and order.

### Authentication and business rules
- Driver JWT authentication is required.
- `delivery_id` and `dispatcher_user_id` must be UUIDs.
- The driver must have a current operational assignment for the delivery; cancelled, rejected, unassigned, completed, and returned assignments are not eligible.
- The selected user must be an active dispatcher in the same tenant.
- When both actors have explicit team mappings, they must share at least one team. Existing all-team behavior is preserved for actors without mappings.
- The customer is never added to this conversation.
- Repeating the same request returns the same active conversation; it does not create duplicate conversations or participants.
- The conversation type is `dispatcher_order`.

### Success (HTTP 200)

returns `conversation.id`, `delivery_id`, `type`, `status`, and `dispatcher_user_id`.

### Important errors
- HTTP 400 for missing or invalid UUID fields.
- HTTP 403 when the caller is not an eligible driver, is not assigned to the delivery, or cannot contact the selected dispatcher.
- HTTP 404 when the delivery or dispatcher is unavailable.
- Every error `message` is localized; English for a non-driver is `Only drivers can perform this action`.

### Frontend integration contract

Call this endpoint after the driver selects Message for a dispatcher returned by `GET /delivery/dispatcher-contacts`. Navigate to the chat using the returned `conversation.id`. On HTTP 403 or 404, do not open a chat and display the returned plain-language `message`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/messaging/dispatcher-conversations

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | string |  |  |  |
| dispatcher_user_id | string |  |  |  |



### Request body (raw)

```json
{
  "delivery_id": "delivery-uuid",
  "dispatcher_user_id": "dispatcher-user-uuid"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "conversation": {
    "id": "conversation-uuid",
    "delivery_id": "delivery-uuid",
    "type": "dispatcher_order",
    "status": "active",
    "dispatcher_user_id": "dispatcher-user-uuid"
  }
}
```

### Response 403 Error - Different Team

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "dispatcher_not_in_driver_team"
}
```

## GET /messaging/conversations/unread-count — Get Chat Unread Count

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/conversations#get-get-chat-unread-count)

Returns one lightweight unread-chat summary for every chat icon in the authenticated user's application.

### Authentication and scope
- Requires a valid bearer token and tenant context.
- The user and tenant are always derived from the authenticated request; this endpoint accepts no `user_id`, `tenant_id`, body, or query parameters.
- Counts only conversations where the current user is an active participant (`left_at` is null) in the current tenant.
- Driver clients exclude support-ticket conversations, matching `GET /messaging/conversations`.

### Unread definition
- `unread_count` is the total number of qualifying unread messages.
- `unread_conversation_count` is the number of distinct conversations containing at least one qualifying unread message.
- Counts messages sent by another user after the participant's `last_read_message_id`.
- If the participant has not read any message, counting starts at `joined_at`; older history is not treated as unread.
- Excludes `system_notification` messages and messages sent by the current user.
- Muting affects push delivery, not unread state, so muted conversations remain included.

### Success

HTTP 200 returns `success`, a localized plain-language `message`, integer `unread_count`, and integer `unread_conversation_count`. A user with no unread messages receives both counts as `0`.

### Important errors
- HTTP 401 when authentication is missing or invalid.
- HTTP 404 when tenant context is unavailable.
- HTTP 500 when the unread summary cannot be retrieved.

### Frontend integration contract
- Store both counts in shared application state; do not call this endpoint separately for each icon.
- Use `unread_count` when the badge represents unread messages, or `unread_conversation_count` when the badge represents chats awaiting attention.
- Fetch after authenticated app startup and foreground resume.
- Refetch when a chat push notification arrives and after the existing conversation read-status request succeeds.
- Hide the badge when the selected count is `0`. A visual `99+` cap may be applied without changing the stored numeric value.
- The endpoint returns no action, entity ID, or navigation instruction.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/messaging/conversations/unread-count

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Unread chat count retrieved successfully",
  "unread_count": 12,
  "unread_conversation_count": 3
}
```

### Response 200 Success - No Unread Messages

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Unread chat count retrieved successfully",
  "unread_count": 0,
  "unread_conversation_count": 0
}
```

### Response 404 Error - Tenant Context Missing

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

## GET /messaging/conversations — List Conversations

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/conversations#get-list-conversations)

Retrieves conversations based on specified filter criteria. Supports pagination and filtering by type, status, user ID, and optional server-side search.

### Search behavior
- `search` is trimmed and matched case-insensitively as a substring
- A conversation matches when its `title` or `latest_message.content` contains the term
- Search combines with `tenant_id`, `type`, `status`, and `user_id` filters
- Search is applied before pagination and exact counting, so `page`, `limit`, and `total` remain valid for infinite pagination
- An empty or whitespace-only `search` behaves as if search was omitted

### Authorization and driver behavior
- Requires valid authentication token
- Users can view their own conversations
- Users with 'view:tenant' or 'manage:messaging:tenant' permission can view all conversations
- When the authenticated actor has the `driver` role, support-ticket conversations (`type=support`) are excluded before exact count and pagination, even if a historical active participant row exists

### Successful response
- HTTP 200 with `success`, `message`, `conversations`, `page`, `limit`, and `total`
- Returns the existing conversation objects with participants, `latest_message`, and `unread_count` unchanged
- For drivers, `total` counts only accessible non-support conversations

### Important errors
- HTTP 403 when requesting another user's conversations without permission
- HTTP 404 when tenant context is unavailable
- HTTP 500 when conversation retrieval fails

### Frontend integration contract
- Send the mobile search text as the `search` query parameter
- Keep the same `search`, `type`, `status`, and `user_id` values while incrementing `page`
- Use `total` with the accumulated result count to stop infinite pagination
- Driver clients must not expect support-ticket chats in this list
- No response field or navigation-action shape changes

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/messaging/conversations?tenant_id={{tenant_id}}&type=&status=&user_id=&search=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth token) | {{tenant_id}} |
| type | query string |  | Filter by conversation type (support, order, delivery, team, etc.) |  |
| status | query string |  | Filter by conversation status |  |
| user_id | query string |  | UUID of the user to filter conversations by (defaults to authenticated user) |  |
| search | query string | No | Optional case-insensitive substring search across conversation title and latest-message content. Applied with all existing filters before counting and pagination. |  |
| page | query string |  | Page number for pagination (default: 1) | 1 |
| limit | query string |  | Number of items per page (default: 20) | 20 |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Conversations retrieved successfully",
  "conversations": [
    {
      "id": "uuid",
      "tenant_id": "uuid",
      "title": "Support Chat #123",
      "type": "support",
      "status": "active",
      "metadata": {
        "order_id": "12345",
        "priority": "high"
      },
      "created_at": "2023-01-01T00:00:00Z",
      "updated_at": "2023-01-01T01:00:00Z",
      "created_by": "uuid",
      "is_system_conversation": false,
      "participants": [
        {
          "id": "uuid",
          "user_id": "uuid",
          "role": "admin",
          "joined_at": "2023-01-01T00:00:00Z",
          "left_at": null,
          "muted": false,
          "notification_preferences": {
            "email": true,
            "push": true
          },
          "user": {
            "id": "uuid",
            "full_name": "John Doe",
            "email": "john@example.com",
            "avatar_url": "https://example.com/avatar.jpg",
            "phone": "+1234567890"
          }
        }
      ],
      "latest_message": {
        "id": "uuid",
        "sender_id": "uuid",
        "message_type": "text",
        "content": "Hello!",
        "content_url": null,
        "sent_at": "2023-01-01T01:00:00Z"
      },
      "unread_count": 0
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## POST /messaging/conversations — Create Conversation

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/conversations#post-create-conversation)

Creates a new conversation.

### Rules
- If `delivery_id` is provided, only one conversation per delivery is allowed. If a conversation for that delivery exists, it will be reused and new participants will be added.
- For drivers/users/dispatchers, `delivery_id` is required.
- For free (non-delivery) conversations, only dispatcher/admin can create.
- **For delivery conversations, participants are auto-determined (driver + customer) and optional in request. If provided, they are added as additional participants.**
- **For free conversations, participants are required.**
- The delivery's customer (user who created the delivery) is auto-added as a participant for delivery conversations.
- Creator is automatically added as admin if not in participants list.
- All participants must belong to the tenant.
- **If `title` is not provided and `delivery_id` is present, the title will be auto-generated as `#<external reference id> - <customer name>`.**
- **The `type` field is always set to 'order' by the backend and is not required in the request.**

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/messaging/conversations?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth token) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | string |  |  |  |
| metadata | object |  |  |  |
| metadata.order_id | string |  |  |  |
| metadata.priority | string |  |  |  |



### Request body (raw)

```json
{
  "delivery_id": "delivery-uuid",
  "metadata": {
    "order_id": "12345",
    "priority": "high"
  }
}
```

### Request example 1

```json
{
  "title": "string", // optional, auto-generated if not provided and delivery_id is present
  "participants": ["uuid"], // optional for delivery conversations, required for free conversations
  "delivery_id": "uuid", // required for delivery conversations
  "metadata": {} // optional
}
```

### Response 201 Success Response (New or Existing Delivery Conversation)

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Conversation created successfully",
  "conversation": {
    "id": "uuid",
    "tenant_id": "uuid",
    "delivery_id": "delivery-uuid",
    "title": "Support Chat #123",
    "type": "order",
    "status": "active",
    "metadata": {
      "order_id": "12345",
      "priority": "high"
    },
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-01-01T00:00:00Z",
    "created_by": "uuid",
    "is_system_conversation": false,
    "participants": [
      {
        "id": "uuid",
        "user_id": "uuid1",
        "role": "admin",
        "joined_at": "2023-01-01T00:00:00Z",
        "muted": false,
        "notification_preferences": null,
        "user": {
          "id": "uuid1",
          "full_name": "John Doe",
          "email": "john@example.com",
          "profile_image_url": "https://example.com/avatar.jpg"
        }
      },
      {
        "id": "uuid2",
        "user_id": "uuid2",
        "role": "member",
        "joined_at": "2023-01-01T00:00:00Z",
        "muted": false,
        "notification_preferences": null,
        "user": {
          "id": "uuid2",
          "full_name": "Jane Smith",
          "email": "jane@example.com",
          "profile_image_url": "https://example.com/avatar2.jpg"
        }
      }
    ]
  }
}
```

### Response 200 Success Response (Existing Delivery Conversation)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Conversation found for delivery_id",
  "conversation": { ... }
}
```

### Response 403 Error - Free Conversation Not Allowed

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Only dispatcher/admin can create free conversation"
}
```

### Response 403 Error - Delivery Conversation Required

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Drivers/users must provide delivery_id"
}
```

## GET /messaging/conversations/:conversationId — Get Conversation

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/conversations#get-get-conversation)

Retrieves detailed information about a specific conversation including participants and latest message.

### Authorization
- Requires valid authentication token
- Standard conversations: user must be an active participant, have `view:tenant`, or be system admin
- Support-ticket conversations: only the ticket customer, a configured tenant support user while support is enabled, or a system admin can access
- A historical participant row does not grant a driver access to a support-ticket conversation
- Unauthorized support-ticket access returns HTTP 403 with `{"success":false,"message":"Insufficient Permissions"}`

### Response
- Returns conversation details with participant information and latest message
- Includes participant user details (name, email, avatar)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/messaging/conversations/:conversationId?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| conversationId | path string | Yes | UUID of the conversation to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth token) | {{tenant_id}} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "conversation": {
    "id": "uuid",
    "title": "Support Chat #123",
    "type": "order",
    "status": "active",
    "tenant_id": "uuid",
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-01-01T01:00:00Z",
    "metadata": {
      "order_id": "12345",
      "priority": "high"
    },
    "participants": [
      {
        "user_id": "uuid1"
      },
      {
        "user_id": "uuid2"
      }
    ],
    "latest_message": {
      "id": "uuid",
      "content": "Hello, how can I help you?",
      "sender_id": "uuid1",
      "created_at": "2023-01-01T01:00:00Z",
      "metadata": {},
      "status": "sent"
    },
    "participant_details": [
      {
        "id": "uuid1",
        "full_name": "John Doe",
        "email": "john@example.com",
        "avatar_url": "https://example.com/avatar.jpg",
        "phone": "+1234567890",
        "metadata": {}
      },
      {
        "id": "uuid2",
        "full_name": "Jane Smith",
        "email": "jane@example.com",
        "avatar_url": "https://example.com/avatar2.jpg",
        "phone": "+0987654321",
        "metadata": {}
      }
    ]
  }
}
```

### Response 404 Error - Conversation Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Conversation not found"
}
```

### Response 403 Error - Unauthorized Access

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## GET /messaging/conversations/:conversationId/messages — List Messages

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/messages#get-list-messages)

Retrieves messages from a specific conversation with filtering and pagination options.

### Authorization
- Requires valid authentication token
- Standard conversations: user must be a participant in the conversation OR be system admin
- Support-ticket conversations: only the ticket customer, a configured tenant support user while support is enabled, or a system admin can list messages
- Active participant membership alone does not grant driver access to a support-ticket conversation
- Unauthorized support-ticket access returns HTTP 403 with `{"success":false,"message":"Insufficient Permissions"}`

### Response
- Returns paginated list of messages with sender information
- Updates participant's last read message automatically

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/messaging/conversations/:conversationId/messages?tenant_id={{tenant_id}}&page=1&limit=20&timestamp_range=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| conversationId | path string | Yes | UUID of the conversation | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth token) | {{tenant_id}} |
| page | query string |  | Page number for pagination (default: 1) | 1 |
| limit | query string |  | Number of items per page (default: 20) | 20 |
| timestamp_range | query string |  | Filter by message timestamp range (format: 2023-01-01T00:00:00Z,2023-01-02T00:00:00Z) |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "messages": [
    {
      "id": "uuid",
      "conversation_id": "uuid",
      "sender_id": "uuid",
      "message_type": "text",
      "content": "Hello!",
      "content_url": null,
      "metadata": null,
      "sent_at": "2023-01-01T00:00:00Z",
      "delivered_status": {},
      "deleted_by": [],
      "parent_message_id": null,
      "sender": {
        "id": "uuid",
        "full_name": "John Doe",
        "email": "john@example.com",
        "avatar_url": "https://example.com/avatar.jpg"
      }
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 403 Error - Not Participant

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Not conversation participant"
}
```

## POST /messaging/conversations/:conversationId/messages — Send Message

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/messages#post-send-message)

Sends a new message to a conversation.

### Authorization
- Requires valid authentication token
- Standard conversations: user must be an active participant
- Support-ticket conversations: only the ticket customer, a configured/eligible support user, or a system admin can send
- Active participant membership alone does not grant driver access to a support-ticket conversation
- Unauthorized support-ticket access returns HTTP 403 with `{"success":false,"message":"Insufficient Permissions"}`
- Conversation must be in 'active' status

### Asynchronous Push Delivery
- After the message commits, a database trigger invokes the notifications Edge Function.
- OneSignal push is sent to active conversation participants except the sender.
- Muted participants, participants with `notification_preferences.push=false`, users with `notifications_enabled=false`, and participants who left the conversation are excluded.
- No `notifications` row is created for a chat message.
- Push failure does not roll back the message or change this endpoint's HTTP 201 response.
- Push data contains `action=open_conversation`, `conversation_id`, `message_id`, and `sender_id`.

### Frontend Integration
- Register the authenticated backend user UUID as the OneSignal `external_id`.
- On push tap, handle `action=open_conversation` and navigate using `conversation_id`.
- If that conversation is already open, suppress the foreground push banner and continue using message Realtime.

### Note
- `message_type=html` is reserved for server-generated messages (e.g., support acknowledgements) and is rejected if sent by clients.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/messaging/conversations/:conversationId/messages?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| conversationId | path string | Yes | UUID of the conversation | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth token) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| message_type | string |  |  |  |
| content | string |  |  |  |
| content_url | null |  |  |  |
| metadata | null |  |  |  |
| parent_message_id | null |  |  |  |



### Request body (raw)

```json
{
  "message_type": "text",
  "content": "Hello!",
  "content_url": null,
  "metadata": null,
  "parent_message_id": null
}
```

### Request example 1

```json
{
  "message_type": "text" | "image" | "file" | "audio" | "video" | "location", // required
  "content": "string", // required for text messages
  "content_url": "string", // required for media messages (image, file, audio, video)
  "metadata": {}, // optional additional data
  "parent_message_id": "uuid" // optional - for threaded replies
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": {
    "id": "uuid",
    "conversation_id": "uuid",
    "sender_id": "uuid",
    "message_type": "text",
    "content": "Hello!",
    "content_url": null,
    "metadata": null,
    "sent_at": "2023-01-01T00:00:00Z",
    "delivered_status": {},
    "deleted_by": [],
    "parent_message_id": null,
    "sender": {
      "id": "uuid",
      "first_name": "John",
      "last_name": "Doe",
      "email": "john@example.com",
      "avatar_url": "https://example.com/avatar.jpg"
    }
  }
}
```

### Response 400 Error - Missing Content

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Content required for text messages"
}
```

### Response 403 Error - Conversation Not Active

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Conversation not active"
}
```

## PUT /messaging/conversations/:conversationId/participants — Manage Participants

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/participants#put-manage-participants)

Manages participants in a conversation by adding, removing, or updating their roles. Supports bulk operations for multiple users.

### Authorization
- Requires valid authentication token
- User must be conversation admin OR have 'manage:tenant' permission OR be system admin
- Support-ticket conversations additionally require the actor to be the ticket customer, a configured tenant support user while support is enabled, or a system admin
- A driver participant cannot manage a support-ticket conversation; unauthorized access returns HTTP 403 `Insufficient Permissions`
- Conversation must be in 'active' status

### Special Behaviors
- Add: If user was previously removed, they will be reactivated
- Remove: Soft delete (sets left_at timestamp)
- Update: Changes participant role
- All target users must exist in the system

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/messaging/conversations/:conversationId/participants?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| conversationId | path string | Yes | UUID of the conversation | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth token) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| action | string |  |  |  |
| user_ids | array<string> |  |  |  |
| role | string |  |  |  |



### Request body (raw)

```json
{
  "action": "add",
  "user_ids": [
    "uuid1",
    "uuid2",
    "uuid3"
  ],
  "role": "member"
}
```

### Request example 1

```json
{
  "action": "add" | "remove" | "update", // required - the action to perform
  "user_ids": ["string"], // required - array of user IDs to add/remove/update
  "role": "string" // required for add and update actions (admin, member, etc.)
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Operation completed successfully",
  "total_processed": 3,
  "successful": 2,
  "failed": 1,
  "results": [
    {
      "user_id": "uuid1",
      "success": true,
      "message": "Participant added successfully",
      "participant": {
        "id": "uuid",
        "conversation_id": "uuid",
        "tenant_id": "uuid",
        "user_id": "uuid1",
        "role": "member",
        "joined_at": "2023-01-01T00:00:00Z",
        "left_at": null,
        "muted": false,
        "notification_preferences": {
          "email": true,
          "push": true
        },
        "user": {
          "id": "uuid1",
          "display_name": "John Doe",
          "email": "john@example.com",
          "avatar_url": "https://example.com/avatar.jpg"
        }
      }
    },
    {
      "user_id": "uuid2",
      "success": true,
      "message": "Participant added successfully",
      "participant": {
        "id": "uuid2",
        "conversation_id": "uuid",
        "tenant_id": "uuid",
        "user_id": "uuid2",
        "role": "member",
        "joined_at": "2023-01-01T00:00:00Z",
        "left_at": null,
        "muted": false,
        "notification_preferences": {
          "email": true,
          "push": true
        },
        "user": {
          "id": "uuid2",
          "display_name": "Jane Smith",
          "email": "jane@example.com",
          "avatar_url": "https://example.com/avatar2.jpg"
        }
      }
    }
  ],
  "errors": [
    {
      "user_id": "uuid3",
      "error": "participant_already_exists"
    }
  ]
}
```

### Response 207 Partial Success Response

Multi-Status

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Partial success",
  "total_processed": 2,
  "successful": 1,
  "failed": 1,
  "results": [
    {
      "user_id": "uuid1",
      "success": true,
      "message": "Participant removed successfully",
      "participant": {
        "id": "uuid",
        "conversation_id": "uuid",
        "tenant_id": "uuid",
        "user_id": "uuid1",
        "role": "member",
        "joined_at": "2023-01-01T00:00:00Z",
        "left_at": "2023-01-01T01:00:00Z",
        "muted": false,
        "notification_preferences": {
          "email": true,
          "push": true
        },
        "user": {
          "id": "uuid1",
          "display_name": "John Doe",
          "email": "john@example.com",
          "avatar_url": "https://example.com/avatar.jpg"
        }
      }
    }
  ],
  "errors": [
    {
      "user_id": "uuid2",
      "error": "participant_not_found"
    }
  ]
}
```

### Response 404 Error - Users Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Users not found",
  "not_found_user_ids": [
    "invalid-uuid1",
    "invalid-uuid2"
  ]
}
```

### Response 400 Error - Missing Role for Action

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Role required for action"
}
```

## GET /messaging/conversations/:conversationId/read-status — Update Read Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/participants#get-update-read-status)

Updates the last read message for the current user in a conversation. This endpoint helps track which messages have been read by each participant.

### Authorization
- Requires valid authentication token
- Standard conversations: user must be an active participant
- Support-ticket conversations: only the ticket customer, a configured tenant support user while support is enabled, or a system admin can update read status
- Active participant membership alone does not grant driver access; unauthorized support-ticket access returns HTTP 403 `Insufficient Permissions`

  - If provided, the message must exist in the specified conversation
  - If not provided, automatically uses the latest message in the conversation

### Behavior
- Updates the participant's `last_read_message_id` field
- If no message_id is provided, finds and uses the most recent message
- Validates that the provided message exists in the conversation
- Only authorized participants who haven't left the conversation can update their read status

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/messaging/conversations/:conversationId/read-status?message_id=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| conversationId | path string | Yes | UUID of the conversation | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| message_id | query string | No | UUID of the specific message to mark as read (optional). If not provided, marks the latest message as read. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Read status updated successfully",
  "data": {
    "participant_id": "456e7890-e89b-12d3-a456-426614174002",
    "last_read_message_id": "123e4567-e89b-12d3-a456-426614174001",
    "conversation_id": "789e0123-e89b-12d3-a456-426614174003"
  }
}
```

### Response 200 Success Response (Latest Message)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Read status updated successfully",
  "data": {
    "participant_id": "456e7890-e89b-12d3-a456-426614174002",
    "last_read_message_id": "987e6543-e89b-12d3-a456-426614174004",
    "conversation_id": "789e0123-e89b-12d3-a456-426614174003"
  }
}
```

### Response 403 Error - Not Participant

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "You are not a participant in this conversation"
}
```

### Response 404 Error - Message Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Message not found in this conversation"
}
```

### Response 404 Error - No Messages in Conversation

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "No messages found in this conversation"
}
```

## GET /messaging/notifications — List Notifications

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/notifications#get-list-notifications)

Retrieve the authenticated user's sent notifications, sorted by created_at descending.

### Authorization

JWT required. Rows are scoped to the authenticated recipient and tenant.

### Filters

`page`, `limit` (max 100), `read_status=true|false`, `notification_type`, `priority=normal|important|critical`, `display_mode=toast|banner|modal`, `acknowledged=true|false`, and optional `tenant_id`.

### Response contract

Each row includes `priority`, `display_mode`, `requires_acknowledgement`, `acknowledged_at`, `channel_status`, `template_data`, and the existing `action`. `action.secondary_actions` may contain `copy`, `call`, `navigate`, or `external_url`; these actions never acknowledge the row. The response also includes `unread_count` and `pending_critical_count`.

### Delivery exception decision notification

A `delivery_exception_resolved` row has `associated_entity_type=delivery`, `associated_entity_id=<deliveryId>`, and `template_data` containing `deliveryId`, `deliveryCode`, `actionKey` (`retry` or `return`), `isRetry`, `isReturn`, and `returnDestinationLabel`. It uses `priority=critical`, `display_mode=modal`, and `requires_acknowledgement=true`, so the driver app receives it through `GET /messaging/notifications?priority=critical&acknowledged=false&page=1&limit=100`. Show a dedicated modal using the returned localized `title` and `body`; the modal action is “I understand” and calls `PUT /messaging/notifications/:notificationId/acknowledge` with no body. Acknowledgement closes only the alert. `GET /delivery/driver-deliveries` remains the persistent operational source through each row's `returnStatus`.

### Frontend integration

On driver login, cold launch, and foreground resume call `GET /messaging/notifications?priority=critical&acknowledged=false&page=1&limit=100`. Use `notification_type` to choose the specialized component and `display_mode` to choose toast, banner, or modal presentation. Render title/body as plain text. Unknown types fall back to the generic title/body component. Show the oldest unacknowledged critical row first.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/messaging/notifications?page=1&limit=100&priority=critical&acknowledged=false&tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination (default: 1) | 1 |
| limit | query string |  | Number of notifications per page (max 100, default: 20) | 20 |
| read_status | query string |  | Filter by read status: 'true', 'false', or omit for all | false |
| notification_type | query string |  | Filter by notification type (delivery_status_update, new_assignment, etc.) |  |
| priority | query string |  | Filter by priority: normal, important, or critical | critical |
| display_mode | query string | No | Optional filter: toast, banner, or modal |  |
| acknowledged | query string |  | Filter by acknowledgement timestamp: true or false | false |
| tenant_id | query string |  | UUID of the tenant (required if not in auth token) | {{tenant_id}} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Notifications retrieved successfully",
  "notifications": [
    {
      "id": "123e4567-e89b-42d3-a456-426614174000",
      "notification_type": "assignment_driver_switched",
      "title": "Driver changed for delivery #3239A",
      "body": "You were switched from delivery #3239A after pickup. Coordinate with your dispatcher and transfer the package to Ahmed Hassan.",
      "read_status": false,
      "priority": "critical",
      "display_mode": "modal",
      "requires_acknowledgement": true,
      "acknowledged_at": null,
      "associated_entity_type": "delivery",
      "associated_entity_id": "123e4567-e89b-42d3-a456-426614174001",
      "template_data": {
        "deliveryId": "123e4567-e89b-42d3-a456-426614174001",
        "deliveryCode": "3239A",
        "newDriver": {
          "driverId": "123e4567-e89b-42d3-a456-426614174002",
          "userId": "123e4567-e89b-42d3-a456-426614174003",
          "fullName": "Ahmed Hassan",
          "avatarUrl": "https://example.com/ahmed.jpg",
          "phone": "+9647500000000"
        }
      },
      "action": {
        "target": "internal",
        "path": "/driver/pickup_and_delivery/123e4567-e89b-42d3-a456-426614174001",
        "secondary_actions": [
          {
            "id": "copy_driver_contact",
            "type": "copy",
            "label": "Copy contact",
            "value": "Ahmed Hassan — +9647500000000"
          },
          {
            "id": "call_driver",
            "type": "call",
            "label": "Call driver",
            "value": "+9647500000000"
          }
        ]
      },
      "channel_status": {
        "in_app": {
          "status": "available",
          "available_at": "2026-07-27T10:30:03Z"
        },
        "push": {
          "status": "failed",
          "retry_count": 1,
          "error": "push delivery failed"
        }
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 100,
    "total": 1,
    "total_pages": 1
  },
  "unread_count": 1,
  "pending_critical_count": 1
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

## PUT /messaging/notifications/{{notification_id}}/acknowledge — Acknowledge Notification

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/notifications#put-acknowledge-notification)

Acknowledge one persistent notification. No request body or user ID is accepted. The UUID is scoped to the authenticated recipient and tenant. The operation atomically sets `acknowledged_at`, `read_status=true`, and `updated_at`; repeated calls return HTTP 200 with the existing timestamp. HTTP 404 `notification_not_found` is used for missing or unauthorized rows. HTTP 409 `notification_acknowledgement_not_required` is used for non-acknowledgeable rows. Driver-switch notifications keep their existing acknowledgement behavior, but acknowledgement no longer controls delivery custody or status actions; custody already transferred during Switch Driver and the response returns `handoff=null`.

### Frontend integration contract

Continue showing the existing “I understand” action when a driver-switch notification has `requires_acknowledgement=true`. Treat HTTP 200 as notification acknowledgement only; do not wait for `handoff.completed` before allowing the replacement driver to continue. Queue valid acknowledgement requests while offline and retry safely. Secondary actions do not acknowledge a notification.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/messaging/notifications/{{notification_id}}/acknowledge

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| notification_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Notification acknowledged successfully",
  "notification": {
    "id": "123e4567-e89b-42d3-a456-426614174000",
    "acknowledged_at": "2026-07-27T10:35:00Z",
    "requires_acknowledgement": true,
    "read_status": true,
    "updated_at": "2026-07-27T10:35:00Z"
  },
  "already_acknowledged": false,
  "handoff": null
}
```

### Response 409 Acknowledgement Not Required

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "notification_acknowledgement_not_required",
  "message": "This notification does not require acknowledgement"
}
```

## PUT /messaging/notifications/mark-read — Mark Notifications as Read

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/notifications#put-mark-notifications-as-read)

Mark notifications as read for the current user. Users can only mark their own notifications as read.

- If `notification_ids` array is provided: marks only those specific notifications as read
- If request body is empty or not provided: marks all unread notifications as read

### Validation
- The system validates that all provided notification IDs belong to the authenticated user
- Only unread notifications are updated (already read notifications are skipped)
- Returns error `invalid_notification_ids` with list of invalid IDs if any provided IDs don't exist or don't belong to the user

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/messaging/notifications/mark-read?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth token) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| notification_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "notification_ids": [
    "uuid1",
    "uuid2",
    "uuid3"
  ]
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Notifications marked as read",
  "updated_count": 3
}
```

### Response 400 Error - Invalid Notification IDs

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid notification IDs provided",
  "invalid_ids": [
    "invalid-uuid1",
    "invalid-uuid2"
  ]
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

## GET /messaging/notifications/stats — Get Notification Statistics

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/notifications#get-get-notification-statistics)

Get notification statistics for the current user including total, unread, and read counts.

### Authorization
- Requires valid authentication token
- Users can only view their own notification statistics

### Response
- Returns notification counts (total, unread, read)
- Useful for displaying notification badges in UI

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/messaging/notifications/stats?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth token) | {{tenant_id}} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Notification stats retrieved",
  "stats": {
    "total": 45,
    "unread": 12,
    "read": 33
  }
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

## GET /messaging/support/tickets — List Support Tickets

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/support-tickets#get-list-support-tickets-2)

Lists support tickets from the shared support queue. Access requires the current user to be included in customer portal `support_user_ids`. Each ticket row includes `source` (`customer_portal` | `storefront` | `restaurant`), `related_delivery_id`, `related_order_id`, and `assigned_to_user` (when assigned). Use `source` and `order_id` to filter by ticket origin / specific order. Use `search` to filter by subject. `assignment_scope` narrows to claimed/unassigned tickets. Elevated support users who are also configured support users may optionally filter by `assigned_to`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/messaging/support/tickets?tenant_id={{tenant_id}}&page=1&limit=20&status=open&assignment_scope=all

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant context (required if not in token) | {{tenant_id}} |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| status | query string | No | open \| closed | open |
| assignment_scope | query string | No | all \| mine \| unassigned | all |
| assigned_to | query string | No | Exact assignee filter for elevated support users who are also configured support users | uuid |
| search | query string | No | Search tickets by subject (case-insensitive substring match) |  |
| source | query string | No | Filter by ticket origin: customer_portal \| storefront \| restaurant | storefront |
| order_id | query string | No | Filter to tickets linked to a specific storefront order (only meaningful for source=storefront) | uuid |



## GET /messaging/support/tickets/:id — Get Support Ticket

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/support-tickets#get-get-support-ticket-2)

Gets a support ticket including conversation summary and `assigned_to_user` details when the ticket is assigned. Access requires the current user to be included in customer portal `support_user_ids`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/messaging/support/tickets/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Support ticket ID | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant context (required if not in token) | {{tenant_id}} |



## POST /messaging/support/tickets/:id/assign-to-me — Assign Support Ticket To Me

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/support-tickets#post-assign-support-ticket-to-me)

Claims an unassigned support ticket for the current support user and adds them to the ticket conversation as an `admin` participant. Access requires the current user to be included in customer portal `support_user_ids`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/messaging/support/tickets/:id/assign-to-me?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Support ticket ID | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant context (required if not in token) | {{tenant_id}} |



## POST /messaging/support/tickets/:id/close — Close Support Ticket

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/messaging/support-tickets#post-close-support-ticket)

Closes the support ticket and emits a system message in the conversation.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/messaging/support/tickets/:id/close?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Support ticket ID | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant context (required if not in token) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| metadata | object |  |  |  |
| metadata.note | string |  |  |  |



### Request body (raw)

```json
{
  "metadata": {
    "note": "Issue resolved"
  }
}
```

## POST /notifications — Internal Background Worker Wake

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/communications-feedback/notifications#post-internal-background-worker-wake)

Internal request created by PostgreSQL `pg_net` after a queue transaction reserves a worker slot. Accepted `worker_key` values are `notification_normal` and `notification_priority`. The database validates the Vault-backed secret, slot number, and one-time wake token before granting a lease. A valid wake returns immediately and the Edge runtime drains work in the background. Duplicate or stale wakes are idempotently accepted with `accepted:false` and perform no work. This endpoint is not a frontend integration contract. Business clients enqueue only through their owning backend operation.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/notifications

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| X-Background-Worker-Secret | header string |  | Vault-backed internal secret. Never expose this value to frontend or mobile clients. | {{background_worker_wake_secret}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| request_type | string |  |  |  |
| worker_key | string |  |  |  |
| slot_number | number |  |  |  |
| wake_token | string |  |  |  |



### Request body (raw)

```json
{
  "request_type": "background_worker_wake",
  "worker_key": "notification_normal",
  "slot_number": 1,
  "wake_token": "{{notification_worker_wake_token}}"
}
```

### Response 202 Accepted

Accepted

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "accepted": true
}
```

### Response 202 Stale or Duplicate Wake

Accepted

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "accepted": false
}
```

### Response 401 Missing Internal Authentication

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "Missing worker authentication"
}
```

## GET /maps/static-preview — Static Route Preview Map

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/maps#get-static-route-preview-map)

Returns a Google Static Maps raster image for route preview backgrounds.

### Business rules
- Requires normal backend authentication.
- Uses the existing `google_maps_server` provider configuration through `MapProviderManager`.
- Does not return the Google Static Maps URL or provider key to the client.
- Forces `maptype=roadmap`.
- Forces Google map styles `feature:poi|visibility:off` and `feature:transit|visibility:off`.
- Sends only map-rendering query data to Google: center, zoom, size, scale, map type, and fixed styles.

### Frontend integration contract
- Use this URL as the React Native image source URI instead of constructing `https://maps.googleapis.com/maps/api/staticmap` on mobile.
- Pass `Authorization: Bearer <access_token>` with the image request.
- Keep existing overlay marker and route-line rendering unchanged because this endpoint returns only the background raster map image.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/maps/static-preview?center_lat=35.123&center_lng=44.123&zoom=13&width=390&height=168&scale=2

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| center_lat | query string | Yes | Required. Map center latitude. Must be between -90 and 90. | 35.123 |
| center_lng | query string | Yes | Required. Map center longitude. Must be between -180 and 180. | 44.123 |
| zoom | query string | No | Optional. Rounded and clamped to 1..21. Defaults to 13. | 13 |
| width | query string | Yes | Required. Logical image width in pixels. Rounded and clamped to 1..640. | 390 |
| height | query string | Yes | Required. Logical image height in pixels. Rounded and clamped to 1..640. | 168 |
| scale | query string | No | Optional. Rounded and clamped to 1..2. Defaults to 2. | 2 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | image/png |
| Cache-Control | header string |  |  | public, max-age=86400 |



### Response 400 Invalid params

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "center_lat_must_be_between_-90_and_90",
  "data": null
}
```

### Response 500 Provider unavailable

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "google_maps_provider_unavailable",
  "data": null
}
```

### Response 502 Google request failed

Bad Gateway

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "google_static_maps_request_failed",
  "data": null
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Get Route - Basic

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/route-calculation#get-get-route-basic)

Calculate optimized route from driver's stored location to pickup and delivery locations.

- Requires valid JWT token or API key
- User must be system admin, dispatcher, tenant admin, or the driver themselves

### Authorization
- System Admins: Full access to all routes
- Dispatchers/Tenant Admins: Access to routes within their tenant
- Drivers: Access to their own assigned routes only

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authentication | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | UUID of the driver |  |
| delivery_id | path string | Yes | UUID of the delivery |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Preferred language for response messages (default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route calculated successfully",
  "route": {
    "encoded_polyline": "u{~vFvyys@fS]...",
    "total_distance_meters": 15420,
    "total_duration_seconds": 1800,
    "waypoints": [
      {
        "lat": 40.7128,
        "lng": -74.006,
        "type": "pickup"
      },
      {
        "lat": 40.7589,
        "lng": -73.9851,
        "type": "dropoff"
      }
    ]
  }
}
```

### Response 400 Error - Invalid Parameters

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request parameters"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 404 Error - Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver or delivery not found"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Get Route - With Current Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/route-calculation#get-get-route-with-current-location)

Calculate optimized route using driver's real-time location instead of stored location.

- Requires valid JWT token or API key
- User must be system admin, dispatcher, tenant admin, or the driver themselves

### Use Case
- When driver's real-time location is more accurate than stored location
- For live route updates during delivery
- When driver location hasn't been updated in the database recently

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}?current_lat={{current_lat}}&current_lng={{current_lng}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authentication | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | UUID of the driver |  |
| delivery_id | path string | Yes | UUID of the delivery |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| current_lat | query string |  | Driver's current latitude (-90 to 90) | {{current_lat}} |
| current_lng | query string |  | Driver's current longitude (-180 to 180) | {{current_lng}} |
| language | query string | No | Preferred language for response messages (default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route calculated successfully",
  "route": {
    "encoded_polyline": "u{~vFvyys@fS]...",
    "total_distance_meters": 12350,
    "total_duration_seconds": 1620,
    "waypoints": [
      {
        "lat": 40.7128,
        "lng": -74.006,
        "type": "pickup"
      },
      {
        "lat": 40.7589,
        "lng": -73.9851,
        "type": "dropoff"
      }
    ]
  }
}
```

### Response 400 Error - Invalid Coordinates

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid coordinates"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Unauthorized Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-unauthorized-access)

Test unauthorized access without authentication token.

### Expected Response
- Status: 401 Unauthorized
- Message: Authentication required

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 401 Unauthorized Response

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Authentication required"
}
```

## GET /route/for-driver/invalid-uuid/delivery/{{delivery_id}} — Invalid Driver ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-invalid-driver-id)

Test with invalid driver ID format.

### Expected Response
- Status: 400 Bad Request
- Message: Invalid driver ID format

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/invalid-uuid/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 400 Invalid Driver ID Response

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid driver ID format"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/invalid-uuid — Invalid Delivery ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-invalid-delivery-id)

Test with invalid delivery ID format.

### Expected Response
- Status: 400 Bad Request
- Message: Invalid delivery ID format

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/invalid-uuid

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |



### Response 400 Invalid Delivery ID Response

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid delivery ID format"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Invalid Coordinates

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-invalid-coordinates)

Test with invalid coordinate values (out of valid range).

### Expected Response
- Status: 400 Bad Request
- Message: Invalid coordinates

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}?current_lat=91&current_lng=181

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| current_lat | query string |  | Invalid latitude (out of range) | 91 |
| current_lng | query string |  | Invalid longitude (out of range) | 181 |



### Response 400 Invalid Coordinates Response

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid coordinates"
}
```

## GET /route/for-driver/00000000-0000-0000-0000-000000000000/delivery/{{delivery_id}} — Driver Not Found

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-driver-not-found)

Test with non-existent driver ID.

### Expected Response
- Status: 404 Not Found
- Message: Driver not found

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/00000000-0000-0000-0000-000000000000/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 404 Driver Not Found Response

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not found"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/00000000-0000-0000-0000-000000000000 — Delivery Not Found

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-delivery-not-found)

Test with non-existent delivery ID.

### Expected Response
- Status: 404 Not Found
- Message: Delivery not found

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/00000000-0000-0000-0000-000000000000

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |



### Response 404 Delivery Not Found Response

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery not found"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Google Maps API Error

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-google-maps-api-error)

Simulate Google Maps API error scenarios (when API key is invalid, quota exceeded, etc.).

### No-route contract

Google Maps `ZERO_RESULTS` returns HTTP 404 with stable `code=no_route_found` and a localized plain-language `message`; it is not an internal server error. Frontends should display `message`, may branch on `code`, and should not retry the same coordinates as a server failure. The driver and delivery IDs remain unchanged and no navigation action is required.

### Other possible error responses
- API quota exceeded
- Invalid API key
- Location not found

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 500 API Quota Exceeded

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "API quota exceeded"
}
```

### Response 422 No Route Found

Unprocessable Entity

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "no_route_found",
  "message": "No route found between driver location and pickup location. The driver cannot reach the pickup location."
}
```

### Response 500 Location Not Found

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Location not found"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — System Admin Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-system-admin-access)

Test route calculation with system admin privileges.

### Prerequisites
- User must have `is_system_admin: true`
- Should have access to all routes regardless of tenant

### Expected Behavior
- Full access to any driver/delivery combination
- No tenant restrictions

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | System admin JWT token | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Dispatcher Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-dispatcher-access)

Test route calculation with dispatcher privileges.

### Prerequisites
- User must have `delivery_dispatch` permission OR
- User must have `dispatcher` role OR
- User must have `tenant_admin` role
- Driver and delivery must belong to same tenant as user

### Expected Behavior
- Access to routes within their tenant only
- Cannot access routes from other tenants

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Dispatcher JWT token | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Driver Self Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-driver-self-access)

Test route calculation where driver accesses their own route.

### Prerequisites
- The authenticated user must be linked to the driver record
- Driver's `user_id` must match the authenticated user's ID
- Delivery must belong to same tenant

### Expected Behavior
- Driver can only access their own routes
- Cannot access routes for other drivers

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Driver's own JWT token | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — API Key Authentication

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-api-key-authentication)

Test route calculation using API key authentication instead of JWT.

### Prerequisites
- Valid API key with appropriate permissions
- API key must belong to same tenant as driver/delivery

### Expected Behavior
- Same authorization rules apply as JWT authentication
- API key tenant context is used for authorization

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  | API key for authentication | {{api_key}} |
| Content-Type | header string |  |  | application/json |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Cross-Tenant Access Denied

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-cross-tenant-access-denied)

Test that users cannot access routes from different tenants.

### Prerequisites
- User belongs to Tenant A
- Driver/Delivery belongs to Tenant B
- User is not system admin

### Expected Response
- Status: 403 Forbidden
- Message: Insufficient permissions

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token from different tenant | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 403 Cross-Tenant Access Denied

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /permissions — List Permissions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/permissions#get-list-permissions)

Retrieves a paginated list of permissions with optional filtering by role ID, scope, and search term.

### Response Behavior
- When `role_id` is provided: Returns permissions assigned to the specified role
- When `scope` is provided: Returns permissions filtered by the specified scope
- When `search` is provided: Returns only permissions whose name or description contains the search term
- When neither filter is provided: Returns all permissions ordered by name

### Frontend Integration Contract
- Endpoint: `GET /permissions`
- To search tenant permissions, call `/permissions?language=en&scope=tenant&search=<term>&page=1&limit=100`
- Read filtered results from `permissions`; use `total`, `page`, and `limit` from the same filtered query for pagination
- Empty matches return `success: true`, `permissions: []`, and `total: 0`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/permissions?language=en&page=1&limit=10&role_id=&scope=&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |
| page | query string | No | Page number for pagination (Optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of records per page (Optional, default: 10) Number of records per page (default: 10) | 10 |
| role_id | query string | No | Filter permissions by role ID (Optional) |  |
| scope | query string | No | Filter permissions by scope (Optional, e.g., 'system', 'tenant') Filter permissions by scope (e.g., 'system', 'tenant') |  |
| search | query string | No | Search permissions by name or description (Optional, case-insensitive, applied before pagination) Case-insensitive search across permission `name` and `description`; applies together with `role_id` and/or `scope` before pagination and count |  |



### Response 200 Success Response (All Permissions)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | max-age=300 |



```json
{
  "success": true,
  "message": "Permissions retrieved successfully",
  "permissions": [
    {
      "id": "perm-001",
      "name": "create:users",
      "description": "Create new users",
      "scope": "system"
    },
    {
      "id": "perm-002",
      "name": "delete:users",
      "description": "Delete existing users",
      "scope": "system"
    },
    {
      "id": "perm-003",
      "name": "manage:tenants",
      "description": "Manage tenant organizations",
      "scope": "system"
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 10
}
```

### Response 200 Success Response (By Role ID)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | max-age=300 |



```json
{
  "success": true,
  "message": "Permissions retrieved successfully",
  "permissions": [
    {
      "id": "perm-001",
      "name": "create:users",
      "description": "Create new users",
      "scope": "system",
      "role_permissions": {
        "role_id": "role-123"
      }
    },
    {
      "id": "perm-002",
      "name": "delete:users",
      "description": "Delete existing users",
      "scope": "system",
      "role_permissions": {
        "role_id": "role-123"
      }
    }
  ],
  "total": 5,
  "page": 1,
  "limit": 10
}
```

### Response 200 Success Response (By Scope)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | max-age=300 |



```json
{
  "success": true,
  "message": "Permissions retrieved successfully",
  "permissions": [
    {
      "id": "perm-004",
      "name": "manage:branches",
      "description": "Manage tenant branches",
      "scope": "tenant"
    },
    {
      "id": "perm-005",
      "name": "view:reports",
      "description": "View tenant reports",
      "scope": "tenant"
    }
  ],
  "total": 15,
  "page": 1,
  "limit": 10
}
```

### Response 500 Error Response

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Permissions retrieved successfully",
  "permissions": [
    {
      "id": "permission-uuid",
      "name": "create:users",
      "description": "Create new users",
      "scope": "system"
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 10
}
```

## GET /report/delivery — Delivery Report

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/overview#get-delivery-report)

## Summary
GET `/report/delivery`

Delivery dashboard report.

## Storage model
- One row per tenant in `public.delivery_report` (refreshed nightly).
- API reads the stored lifetime payload; there is no `range` query param.
- If stored `zone_stats` are still on the legacy shape, the API rebuilds zone stats live from `public.deliveries`.

- Header `Authorization: Bearer {{access_token}}`
- All auth failures come from middleware and return `{ "success": false, "message": string, "data": null }` with `401/403`.

## Failure aggregation
- Delivery failure KPIs and series include the distinct delivery statuses `failed`, `cancelled`, and `returned`. Raw delivery rows and exports keep those statuses separate; an RTS-failed hybrid outbound leg remains `failed` and is not rewritten as `cancelled`.

## Zone stats rules
- All tenant zones are returned in `zone_stats`, even when a zone has no deliveries in the reporting window
- Zones without deliveries return `0` for all totals
- `total_inbound_deliveries` counts deliveries where `pickup_zone_id = zone_id`
- `total_outbound_deliveries` counts deliveries where `dropoff_zone_id = zone_id`
- `total_deliveries` counts unique deliveries touching the zone by pickup and/or dropoff
- If both pickup and dropoff point to the same zone, that delivery is counted once in `total_deliveries`
- `total_completed_deliveries` counts delivered rows from those unique deliveries

## Error responses
- `401/403` (auth/middleware): `{ "success": false, "message": string, "data": null }`
- `404` (report not ready): `{ "success": false, "message": "report_not_ready", "data": null }`
- `404` (unknown module): `{ "success": false, "message": string, "data": null }`
- `405` (method not allowed): `{ "success": false, "message": string, "data": null }`
- `500`: `{ "success": false, "message": string, "data": null }`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/delivery

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "data": {
    "kpis": {
      "total_deliveries": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "completed": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "in_progress": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      },
      "failed": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      }
    },
    "charts": {
      "status_breakdown": {
        "completed": 0,
        "in_progress": 0,
        "pending": 0,
        "failed": 0
      },
      "weekly_performance": {
        "dates": [
          "2025-12-08",
          "2025-12-09",
          "2025-12-10",
          "2025-12-11",
          "2025-12-12",
          "2025-12-13",
          "2025-12-14"
        ],
        "completed": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ],
        "failed": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "metrics_trends": {
        "success_rate": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ],
        "avg_delivery_time": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ],
        "active_drivers": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      }
    },
    "performance": {
      "success_rate": {
        "value": 0,
        "percentage": 0
      },
      "on_time_delivery": {
        "value": 0,
        "percentage": 0
      },
      "customer_satisfaction": {
        "value": 0,
        "percentage": 0
      },
      "first_attempt_success": {
        "value": 0,
        "percentage": 0
      }
    },
    "cod": {
      "collected": 0,
      "remitted": 0,
      "outstanding": 0,
      "wallet_settled": 0
    },
    "top_drivers": [
      {
        "driver_id": "00000000-0000-0000-0000-000000000001",
        "name": "",
        "avatar_url": "",
        "completed_jobs": 0,
        "failed_jobs": 0,
        "avg_time_minutes": 0
      }
    ],
    "zone_stats": [
      {
        "zone_id": "11111111-1111-1111-1111-111111111111",
        "zone_code": "ZN-001",
        "zone_name": "Central Zone",
        "total_deliveries": 0,
        "total_inbound_deliveries": 0,
        "total_outbound_deliveries": 0,
        "total_completed_deliveries": 0
      }
    ],
    "recent_activity": [
      {
        "id": "00000000-0000-0000-0000-000000000100",
        "order_ref": "",
        "type": "pending",
        "message": "",
        "timestamp": "2025-12-14T00:00:00.000Z"
      }
    ]
  }
}
```

### Response 401 401 Unauthorized (middleware/auth)

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_token",
  "data": null
}
```

### Response 404 404 Not Found (unknown module)

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Endpoint for module 'delivery' not found",
  "data": null
}
```

### Response 405 405 Method Not Allowed

Method Not Allowed

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "method_not_allowed",
  "data": null
}
```

### Response 500 500 Internal Server Error (router catch)

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal Server Error",
  "data": null
}
```

### Response 200 200 Response (JSON)



```json
type ResponseEnvelope<T> = { success: boolean; message: string; data: T | null };

type DeliveryDashboardEnvelope = ResponseEnvelope<DeliveryDashboardResponse>;

interface DeliveryDashboardResponse {
  kpis: {
    total_deliveries: KPIItem;
    completed: KPIItem;
    in_progress: KPIItem;
    failed: KPIItem;
  };
  charts: {
    status_breakdown: { completed: number; in_progress: number; pending: number; failed: number };
    weekly_performance: { dates: string[]; completed: number[]; failed: number[] };
    metrics_trends: { success_rate: number[]; avg_delivery_time: number[]; active_drivers: number[] };
  };
  performance: {
    success_rate: { value: number; percentage: number };
    on_time_delivery: { value: number; percentage: number };
    customer_satisfaction: { value: number; percentage: number };
    first_attempt_success: { value: number; percentage: number };
  };
  cod: { collected: number; remitted: number; outstanding: number; wallet_settled: number };
  top_drivers: Array<{ driver_id: string; name: string; avatar_url: string; completed_jobs: number; failed_jobs: number; avg_time_minutes: number }>;
  zone_stats: Array<{
    zone_id: string;
    zone_code: string;
    zone_name: string;
    total_deliveries: number;
    total_inbound_deliveries: number;
    total_outbound_deliveries: number;
    total_completed_deliveries: number;
  }>;
  recent_activity: Array<{ id: string; order_ref: string; type: 'completed'|'failed'|'in_transit'|'pending'|'assigned'; message: string; timestamp: string }>;
}

type KPIItem = {
  value: number;
  trend_percentage: number;
  trend_direction: 'up' | 'down';
  chart_data: number[];
};
```

## GET /report/storefront — Storefront Report

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/overview#get-storefront-report)

## Summary
GET `/report/storefront`

Storefront dashboard report.

## Storage model
- One row per tenant in `public.storefront_report` (refreshed nightly).
- API reads the stored lifetime payload; there is no `range` query param.

- Header `Authorization: Bearer {{access_token}}`

### Same envelope
- `{ "success": false, "message": string, "data": null }`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/storefront

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "data": {
    "kpis": {
      "weekly_sales": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "new_customers": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      },
      "purchase_orders": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      },
      "messages": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      }
    },
    "summary": {
      "product_sold": {
        "value": 0,
        "trend_percentage": 0
      },
      "total_balance": {
        "value": 0,
        "trend_percentage": 0
      },
      "sales_profit": {
        "value": 0,
        "trend_percentage": 0
      }
    },
    "charts": {
      "sale_by_gender": [
        {
          "label": "Men",
          "value": 0
        },
        {
          "label": "Women",
          "value": 0
        },
        {
          "label": "Kids",
          "value": 0
        }
      ],
      "yearly_sales": {
        "categories": [
          "Jan",
          "Feb",
          "Mar",
          "Apr",
          "May",
          "Jun",
          "Jul",
          "Aug",
          "Sep",
          "Oct",
          "Nov",
          "Dec"
        ],
        "series": [
          {
            "name": "2023",
            "data": [
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0
            ]
          },
          {
            "name": "2024",
            "data": [
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0,
              0
            ]
          }
        ]
      }
    },
    "finances": {
      "sales_overview": {
        "total_profit": {
          "value": 0,
          "percentage": 0
        },
        "total_income": {
          "value": 0,
          "percentage": 0
        },
        "total_expenses": {
          "value": 0,
          "percentage": 0
        }
      },
      "current_balance": {
        "total": 0,
        "breakdown": {
          "order_total": 0,
          "earning": 0,
          "refunded": 0
        }
      }
    },
    "best_sellers": [
      {
        "seller_id": "00000000-0000-0000-0000-000000000001",
        "name": "",
        "avatar_url": "",
        "product_category": "",
        "country_code": "",
        "total_sales": 0,
        "rank": 1
      }
    ],
    "latest_products": [
      {
        "id": "00000000-0000-0000-0000-000000000010",
        "name": "",
        "image_url": "",
        "price": 0,
        "original_price": 0,
        "is_active": true
      }
    ]
  }
}
```

### Response 401 401 Unauthorized (middleware/auth)

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_token",
  "data": null
}
```

### Response 405 405 Method Not Allowed

Method Not Allowed

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "method_not_allowed",
  "data": null
}
```

### Response 500 500 Internal Server Error (router catch)

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal Server Error",
  "data": null
}
```

### Response 200 200 Response (JSON)



```json
type ResponseEnvelope<T> = { success: boolean; message: string; data: T | null };

type StorefrontDashboardEnvelope = ResponseEnvelope<StorefrontDashboardResponse>;

interface StorefrontDashboardResponse {
  kpis: {
    weekly_sales: KPIItem;
    new_customers: KPIItem;
    purchase_orders: KPIItem;
    messages: KPIItem;
  };
  summary: {
    product_sold: { value: number; trend_percentage: number };
    total_balance: { value: number; trend_percentage: number };
    sales_profit: { value: number; trend_percentage: number };
  };
  charts: {
    sale_by_gender: Array<{ label: string; value: number }>;
    yearly_sales: { categories: string[]; series: Array<{ name: string; data: number[] }> };
  };
  finances: {
    sales_overview: {
      total_profit: { value: number; percentage: number };
      total_income: { value: number; percentage: number };
      total_expenses: { value: number; percentage: number };
    };
    current_balance: {
      total: number;
      breakdown: { order_total: number; earning: number; refunded: number };
    };
  };
  best_sellers: Array<{ seller_id: string; name: string; avatar_url: string; product_category: string; country_code: string; total_sales: number; rank: number }>;
  latest_products: Array<{ id: string; name: string; image_url: string; price: number; original_price?: number; is_active: boolean }>;
}

type KPIItem = { value: number; trend_percentage: number; trend_direction: 'up'|'down'; chart_data: number[] };
```

## GET /report/warehouse — Warehouse Report

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/overview#get-warehouse-report)

## Summary
GET `/report/warehouse`

Warehouse dashboard report.

## Storage model
- One row per tenant in `public.warehouse_report` (refreshed nightly).
- API reads the stored lifetime payload; there is no `range` query param.

- Header `Authorization: Bearer {{access_token}}`

### Same envelope
- `{ "success": false, "message": string, "data": null }`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/warehouse

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "data": {
    "kpis": {
      "items_received": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "items_dispatched": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "total_stock": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      },
      "picking_orders": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      }
    },
    "charts": {
      "weekly_receiving": [
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "weekly_dispatching": [
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "completed_audits": [
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "capacity_utilization": {
        "used": 0,
        "available": 0,
        "percentage": 0
      },
      "inventory_movement": {
        "dates": [
          "2025-12-08",
          "2025-12-09",
          "2025-12-10",
          "2025-12-11",
          "2025-12-12",
          "2025-12-13",
          "2025-12-14"
        ],
        "received": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ],
        "dispatched": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      }
    },
    "performance": {
      "items_received": {
        "current": 0,
        "target": 0,
        "percentage": 0
      },
      "items_dispatched": {
        "current": 0,
        "target": 0,
        "percentage": 0
      },
      "picking_orders": {
        "current": 0,
        "target": 0,
        "percentage": 0
      },
      "audits_completed": {
        "current": 0,
        "target": 0,
        "percentage": 0
      }
    },
    "summary": {
      "total_stock_items": 0,
      "total_stock_value": 0,
      "capacity_used_percentage": 0
    },
    "stock_levels": [
      {
        "item_name": "",
        "sku": "",
        "quantity": 0,
        "capacity_max": 0,
        "status": "In Stock",
        "status_color": "green"
      }
    ],
    "recent_activity": [
      {
        "id": "00000000-0000-0000-0000-000000000001",
        "type": "received",
        "message": "",
        "timestamp": "2025-12-14T00:00:00.000Z",
        "reference_id": ""
      }
    ]
  }
}
```

### Response 401 401 Unauthorized (middleware/auth)

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_token",
  "data": null
}
```

### Response 405 405 Method Not Allowed

Method Not Allowed

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "method_not_allowed",
  "data": null
}
```

### Response 500 500 Internal Server Error (router catch)

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal Server Error",
  "data": null
}
```

### Response 200 200 Response (JSON)



```json
type ResponseEnvelope<T> = { success: boolean; message: string; data: T | null };

type WarehouseDashboardEnvelope = ResponseEnvelope<WarehouseDashboardResponse>;

interface WarehouseDashboardResponse {
  kpis: {
    items_received: KPIItem;
    items_dispatched: KPIItem;
    total_stock: KPIItem;
    picking_orders: KPIItem;
  };
  charts: {
    weekly_receiving: number[];
    weekly_dispatching: number[];
    completed_audits: number[];
    capacity_utilization: { used: number; available: number; percentage: number };
    inventory_movement: { dates: string[]; received: number[]; dispatched: number[] };
  };
  performance: {
    items_received: { current: number; target: number; percentage: number };
    items_dispatched: { current: number; target: number; percentage: number };
    picking_orders: { current: number; target: number; percentage: number };
    audits_completed: { current: number; target: number; percentage: number };
  };
  summary: {
    total_stock_items: number;
    total_stock_value: number;
    capacity_used_percentage: number;
  };
  stock_levels: Array<{ item_name: string; sku: string; quantity: number; capacity_max: number; status: 'In Stock'|'Low Stock'|'Out of Stock'; status_color: 'green'|'yellow'|'red' }>;
  recent_activity: Array<{ id: string; type: 'received'|'dispatched'|'picking'|'audit'|'transfer'; message: string; timestamp: string; reference_id: string }>;
}

type KPIItem = { value: number; trend_percentage: number; trend_direction: 'up'|'down'; chart_data: number[] };
```

## GET /report/restaurant — Restaurant Report

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/overview#get-restaurant-report)

## Summary
GET `/report/restaurant`

Restaurant dashboard report.

## Storage model
- One row per tenant in `public.restaurant_report` (refreshed nightly).
- API reads the stored lifetime payload; there is no `range` query param.

- Header `Authorization: Bearer {{access_token}}`

### Same envelope
- `{ "success": false, "message": string, "data": null }`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/restaurant

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "data": {
    "kpis": {
      "todays_orders": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "todays_revenue": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "active_tables": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      },
      "avg_prep_time": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      }
    },
    "charts": {
      "weekly_orders": [
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "weekly_revenue": [
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "avg_order_value": [
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "order_status_breakdown": {
        "completed": 0,
        "in_progress": 0,
        "pending": 0,
        "cancelled": 0
      },
      "revenue_by_type": {
        "dates": [
          "2025-12-08",
          "2025-12-09",
          "2025-12-10",
          "2025-12-11",
          "2025-12-12",
          "2025-12-13",
          "2025-12-14"
        ],
        "dine_in": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ],
        "takeaway": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      }
    },
    "performance": {
      "total_revenue": {
        "current": 0,
        "target": 0,
        "percentage": 0
      },
      "total_orders": {
        "current": 0,
        "target": 0,
        "percentage": 0
      },
      "total_expenses": {
        "current": 0,
        "target": 0,
        "percentage": 0
      },
      "todays_summary": {
        "revenue": 0,
        "orders": 0,
        "aov": 0
      }
    },
    "popular_items": [
      {
        "item_id": "00000000-0000-0000-0000-000000000001",
        "name": "",
        "image_url": "",
        "category": "",
        "orders_count": 0,
        "revenue": 0,
        "status": "Available",
        "status_color": "green"
      }
    ],
    "recent_orders": [
      {
        "id": "00000000-0000-0000-0000-000000000010",
        "order_ref": "",
        "description": "",
        "status": "Pending",
        "timestamp": "2025-12-14T00:00:00.000Z"
      }
    ]
  }
}
```

### Response 401 401 Unauthorized (middleware/auth)

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_token",
  "data": null
}
```

### Response 405 405 Method Not Allowed

Method Not Allowed

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "method_not_allowed",
  "data": null
}
```

### Response 500 500 Internal Server Error (router catch)

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal Server Error",
  "data": null
}
```

### Response 200 200 Response (JSON)



```json
type ResponseEnvelope<T> = { success: boolean; message: string; data: T | null };

type RestaurantDashboardEnvelope = ResponseEnvelope<RestaurantDashboardResponse>;

interface RestaurantDashboardResponse {
  kpis: {
    todays_orders: KPIItem;
    todays_revenue: KPIItem;
    active_tables: KPIItem;
    avg_prep_time: KPIItem;
  };
  charts: {
    weekly_orders: number[];
    weekly_revenue: number[];
    avg_order_value: number[];
    order_status_breakdown: { completed: number; in_progress: number; pending: number; cancelled: number };
    revenue_by_type: { dates: string[]; dine_in: number[]; takeaway: number[] };
  };
  performance: {
    total_revenue: { current: number; target: number; percentage: number };
    total_orders: { current: number; target: number; percentage: number };
    total_expenses: { current: number; target: number; percentage: number };
    todays_summary: { revenue: number; orders: number; aov: number };
  };
  popular_items: Array<{ item_id: string; name: string; image_url: string; category: string; orders_count: number; revenue: number; status: 'Available'|'Low Stock'|'Out of Stock'; status_color: 'green'|'orange'|'red' }>;
  recent_orders: Array<{ id: string; order_ref: string; description: string; status: 'Completed'|'In Progress'|'Preparing'|'Pending'|'Cancelled'; timestamp: string }>;
}

type KPIItem = { value: number; trend_percentage: number; trend_direction: 'up'|'down'; chart_data: number[] };
```

## GET /report/express — Express Shipping Report

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/overview#get-express-shipping-report)

## Summary
GET `/report/express`

Express (shipping) dashboard report.

## Storage model
- One row per tenant in `public.express_report` (refreshed nightly).
- API reads the stored lifetime payload; there is no `range` query param.

- Header `Authorization: Bearer {{access_token}}`

### Same envelope
- `{ "success": false, "message": string, "data": null }`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/express

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "data": {
    "kpis": {
      "total_packages": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "delivered": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      },
      "in_transit": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      },
      "pending_claim": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      }
    },
    "charts": {
      "weekly_volume": [
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "total_weight_kg": [
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "avg_delivery_time": [
        0,
        0,
        0,
        0,
        0,
        0,
        0
      ],
      "status_distribution": {
        "delivered": 0,
        "in_transit": 0,
        "processing": 0,
        "received": 0,
        "pending_claim": 0
      },
      "shipping_volume_bar": {
        "dates": [
          "2025-12-08",
          "2025-12-09",
          "2025-12-10",
          "2025-12-11",
          "2025-12-12",
          "2025-12-13",
          "2025-12-14"
        ],
        "received": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ],
        "delivered": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      }
    },
    "revenue": {
      "domestic_fees": 0,
      "international_fees": 0,
      "delivery_fees": 0,
      "total_revenue": 0
    },
    "metrics": {
      "delivered": {
        "value": 0,
        "percentage": 0
      },
      "in_transit": {
        "value": 0,
        "percentage": 0
      },
      "processing": {
        "value": 0,
        "percentage": 0
      },
      "pending_claim": {
        "value": 0,
        "percentage": 0
      }
    },
    "top_stores": [
      {
        "store_name": "",
        "package_count": 0
      }
    ],
    "recent_packages": [
      {
        "tracking_number": "",
        "store_name": "",
        "customer_name": "",
        "weight_kg": 0,
        "status": "Received",
        "status_color": "orange",
        "payment_status": "Pending",
        "created_at": "2025-12-14T00:00:00.000Z"
      }
    ],
    "recent_activity": [
      {
        "id": "00000000-0000-0000-0000-000000000001",
        "message": "",
        "type": "received",
        "timestamp": "2025-12-14T00:00:00.000Z"
      }
    ]
  }
}
```

### Response 401 401 Unauthorized (middleware/auth)

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_token",
  "data": null
}
```

### Response 405 405 Method Not Allowed

Method Not Allowed

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "method_not_allowed",
  "data": null
}
```

### Response 500 500 Internal Server Error (router catch)

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal Server Error",
  "data": null
}
```

### Response 200 200 Response (JSON)



```json
type ResponseEnvelope<T> = { success: boolean; message: string; data: T | null };

type ExpressDashboardEnvelope = ResponseEnvelope<ExpressDashboardResponse>;

interface ExpressDashboardResponse {
  kpis: {
    total_packages: KPIItem;
    delivered: KPIItem;
    in_transit: KPIItem;
    pending_claim: KPIItem;
  };
  charts: {
    weekly_volume: number[];
    total_weight_kg: number[];
    avg_delivery_time: number[];
    status_distribution: { delivered: number; in_transit: number; processing: number; received: number; pending_claim: number };
    shipping_volume_bar: { dates: string[]; received: number[]; delivered: number[] };
  };
  revenue: { domestic_fees: number; international_fees: number; delivery_fees: number; total_revenue: number };
  metrics: {
    delivered: { value: number; percentage: number };
    in_transit: { value: number; percentage: number };
    processing: { value: number; percentage: number };
    pending_claim: { value: number; percentage: number };
  };
  top_stores: Array<{ store_name: string; package_count: number }>;
  recent_packages: Array<{ tracking_number: string; store_name: string; customer_name: string; weight_kg: number; status: 'Delivered'|'In transit'|'Processing'|'Received'|'Claimed'; status_color: 'green'|'red'|'blue'|'orange'|'purple'; payment_status: 'Paid'|'Pending'|'Unpaid'; created_at: string }>;
  recent_activity: Array<{ id: string; message: string; type: 'delivered'|'in_transit'|'processing'|'received'|'claimed'; timestamp: string }>;
}

type KPIItem = { value: number; trend_percentage: number; trend_direction: 'up'|'down'; chart_data: number[] };
```

## GET /report/app — Tenant App Report (Common)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/overview#get-tenant-app-report-common)

## Summary
GET `/report/app`

Tenant application (common) report that returns the **exact dashboard widget payload** the portal uses.

## Storage model
- One row per tenant in `public.tenant_app_report` (refreshed nightly).

- Header `Authorization: Bearer {{access_token}}`

### Same envelope
- `{ "success": false, "message": string, "data": null }`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/app

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "data": {
    "delivery_overview": {
      "total_deliveries_period": 0,
      "completed_deliveries_period": 0,
      "pending_deliveries_current": 0,
      "in_progress_deliveries_current": 0,
      "failed_deliveries_period": 0,
      "delivery_success_rate_period": 0,
      "avg_delivery_time_minutes_period": 0,
      "avg_pickup_to_delivery_time_minutes_period": 0
    },
    "cod_summary": {
      "total_cod_collected_period": 0,
      "total_cod_remitted_period": 0,
      "total_cod_outstanding_current": 0,
      "total_cod_settled_by_wallet_period": 0
    },
    "warehouse_overview": {
      "total_items_received_period": 0,
      "total_items_dispatched_period": 0,
      "current_total_stock_items": 0,
      "current_total_stock_value": 0,
      "overall_capacity_utilization_percent_current": 0,
      "total_picking_orders_completed_period": 0,
      "total_batches_dispatched_period": 0
    },
    "warehouse_activity_by_warehouse": {
      "data": [
        {
          "warehouse_id": "00000000-0000-0000-0000-000000000001",
          "warehouse_name": "",
          "items_received_period": 0,
          "items_dispatched_period": 0,
          "current_stock_items": 0,
          "capacity_utilization_percent": 0
        }
      ],
      "columns": [
        {
          "field": "warehouse_name",
          "label": "Warehouse",
          "description": "Warehouse name",
          "type": "string"
        },
        {
          "field": "items_received_period",
          "label": "Received",
          "description": "Items received in period",
          "type": "number"
        },
        {
          "field": "items_dispatched_period",
          "label": "Dispatched",
          "description": "Items dispatched in period",
          "type": "number"
        },
        {
          "field": "current_stock_items",
          "label": "Stock",
          "description": "Current stock items",
          "type": "number"
        },
        {
          "field": "capacity_utilization_percent",
          "label": "Capacity %",
          "description": "Capacity utilization percent",
          "type": "number"
        }
      ]
    },
    "warehouse_stock_by_category": {
      "data": [
        {
          "category_id": "00000000-0000-0000-0000-000000000010",
          "category_name": {
            "en": ""
          },
          "stock_items": 0
        }
      ],
      "columns": [
        {
          "field": "category_name",
          "label": "Category",
          "description": "Inventory category",
          "type": "jsonb"
        },
        {
          "field": "stock_items",
          "label": "Stock Items",
          "description": "Stock items in category",
          "type": "number"
        }
      ]
    }
  }
}
```

### Response 401 401 Unauthorized (middleware/auth)

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_token",
  "data": null
}
```

### Response 404 404 Not Found (report not ready)

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "report_not_ready",
  "data": null
}
```

### Response 405 405 Method Not Allowed

Method Not Allowed

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "method_not_allowed",
  "data": null
}
```

### Response 500 500 Internal Server Error (router catch)

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal Server Error",
  "data": null
}
```

### Response 200 200 Response (JSON)



```json
type ResponseEnvelope<T> = { success: boolean; message: string; data: T | null };

type TenantAppDashboardEnvelope = ResponseEnvelope<TenantAppDashboardResponse>;

interface TenantAppDashboardResponse {
  delivery_overview: {
    total_deliveries_period: number;
    completed_deliveries_period: number;
    pending_deliveries_current: number;
    in_progress_deliveries_current: number;
    failed_deliveries_period: number;
    delivery_success_rate_period: number;
    avg_delivery_time_minutes_period: number;
    avg_pickup_to_delivery_time_minutes_period: number;
  };
  cod_summary: {
    total_cod_collected_period: number;
    total_cod_remitted_period: number;
    total_cod_outstanding_current: number;
    total_cod_settled_by_wallet_period: number;
  };
  warehouse_overview: {
    total_items_received_period: number;
    total_items_dispatched_period: number;
    current_total_stock_items: number;
    current_total_stock_value: number;
    overall_capacity_utilization_percent_current: number;
    total_picking_orders_completed_period: number;
    total_batches_dispatched_period: number;
  };
  warehouse_activity_by_warehouse: {
    data: Array<{
      warehouse_id: string;
      warehouse_name: string;
      items_received_period: number;
      items_dispatched_period: number;
      current_stock_items: number;
      capacity_utilization_percent: number;
    }>;
    columns: Array<{ field: string; label: string; description: string; type: string }>;
  };
  warehouse_stock_by_category: {
    data: Array<{ category_id: string; category_name: unknown; stock_items: number }>;
    columns: Array<{ field: string; label: string; description: string; type: string }>;
  };
}
```

## POST /report — Trigger Daily Aggregation (Cron)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/overview#post-trigger-daily-aggregation-cron)

## Summary
POST `/report`

Refreshes stored lifetime-to-date reports for each tenant across delivery, storefront, warehouse, restaurant, express, and tenant-app modules.

Internal backend/cron only. Send `apikey: {{supabase_secret_key}}` with the default managed Supabase secret key (`SUPABASE_SECRET_KEYS.default`). Never put this value in a frontend or mobile application. Missing, invalid, publishable, legacy anon, and legacy service-role bearer credentials return HTTP 401 before work is dispatched. `Authorization: Bearer ...` alone is not accepted. Frontend handoff: none; application clients must not call this internal endpoint. The unauthorized message is localized using the request language.

Only literal `recalculate_all: true` expands the driver-performance recalculation and top-driver range to start at 2020-01-01. Omitted/false/unparseable bodies use the current UTC month for that range. Lifetime delivery totals, recent chart windows, and other module reports still refresh. `date` is not read by this handler.

HTTP 200 may report `success: false` when one or more tenants fail; inspect `tenants_failed` and `failed[]` in addition to HTTP status.

## Error responses
- HTTP 401: `{ "success": false, "message": "Unauthorized" }` (localized).
- Scheduler failures use the same response envelope:
- `{ "success": false, "message": string, "data": null }`

Authentication: Internal backend or cron only. Send the managed Supabase secret in apikey. Application clients must not call this endpoint.

Request: POST https://staging-api.ondi.io/functions/v1/report

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| apikey | header string |  | Default managed Supabase secret key; backend only. | {{supabase_secret_key}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| recalculate_all | boolean |  |  |  |



### Request body (raw)

```json
{
  "recalculate_all": false
}
```

### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Lifetime report refresh completed",
  "data": {
    "duration_ms": "12.34",
    "tenants_ok": 0,
    "tenants_failed": 0,
    "failed": []
  }
}
```

### Response 500 500 Tenant Loading Failed

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Failed to load tenants: database error",
  "data": null
}
```

### Response 500 500 Unexpected Scheduler Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unknown",
  "data": null
}
```

### Response 401 401 Missing or invalid internal secret

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 200 200 Response (JSON)



```json
type ResponseEnvelope<T> = { success: boolean; message: string; data: T | null };

type CronTriggerData = {
  duration_ms: string;
  tenants_ok: number;
  tenants_failed: number;
  failed: Array<{ tenant_id: string; ok: boolean; error?: string }>;
};

type CronTriggerEnvelope = ResponseEnvelope<CronTriggerData>;
```

## GET /report/tenant/delivery/overview — Legacy - Delivery Overview

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/overview#get-legacy-delivery-overview)

## Summary
GET `/report/tenant/delivery/overview`

Legacy route. Router maps any `/report/tenant/delivery/*` to module `delivery`.

## Effective route
- `GET /report/delivery`

- Header `Authorization: Bearer {{access_token}}`

Same as `Delivery Report` but wrapped envelope:
- `{ success: true, message: "", data: DeliveryDashboardResponse }`

### Same envelope
- `{ success: false, message: string, data: null }`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/tenant/delivery/overview

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 200 OK (same as Delivery Report)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "data": {
    "kpis": {
      "total_deliveries": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "completed": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "in_progress": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": []
      },
      "failed": {
        "value": 0,
        "trend_percentage": 0,
        "trend_direction": "up",
        "chart_data": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      }
    },
    "charts": {
      "status_breakdown": {
        "completed": 0,
        "in_progress": 0,
        "pending": 0,
        "failed": 0
      },
      "weekly_performance": {
        "dates": [
          "2025-12-08",
          "2025-12-09",
          "2025-12-10",
          "2025-12-11",
          "2025-12-12",
          "2025-12-13",
          "2025-12-14"
        ],
        "completed": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ],
        "failed": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      },
      "metrics_trends": {
        "success_rate": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ],
        "avg_delivery_time": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ],
        "active_drivers": [
          0,
          0,
          0,
          0,
          0,
          0,
          0
        ]
      }
    },
    "performance": {
      "success_rate": {
        "value": 0,
        "percentage": 0
      },
      "on_time_delivery": {
        "value": 0,
        "percentage": 0
      },
      "customer_satisfaction": {
        "value": 0,
        "percentage": 0
      },
      "first_attempt_success": {
        "value": 0,
        "percentage": 0
      }
    },
    "cod": {
      "collected": 0,
      "remitted": 0,
      "outstanding": 0,
      "wallet_settled": 0
    },
    "top_drivers": [
      {
        "driver_id": "00000000-0000-0000-0000-000000000001",
        "name": "",
        "avatar_url": "",
        "completed_jobs": 0,
        "failed_jobs": 0,
        "avg_time_minutes": 0
      }
    ],
    "zone_stats": [
      {
        "zone_id": "11111111-1111-1111-1111-111111111111",
        "zone_code": "ZN-001",
        "zone_name": "Central Zone",
        "total_deliveries": 0,
        "total_inbound_deliveries": 0,
        "total_outbound_deliveries": 0,
        "total_completed_deliveries": 0
      }
    ],
    "recent_activity": [
      {
        "id": "00000000-0000-0000-0000-000000000100",
        "order_ref": "",
        "type": "pending",
        "message": "",
        "timestamp": "2025-12-14T00:00:00.000Z"
      }
    ]
  }
}
```

## GET /report/catalogs/:module — Catalogs - List for module

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/report-exports-async#get-catalogs-list-for-module)

### Returns all registered catalogs for a module

category, columns (with relationOptions), filterable statuses, scope definitions, `multiSelectFilters`, and supported formats. Each multiSelectFilters entry contains key, label, valueType, and valueSource for building a searchable picker. Requires `view:reports`; the `tenant_admin` role receives this permission directly. Each scope/filter `valueSource` includes `itemsField`, which tells the FE which top-level array key to read from the endpoint response. Zone Pricing declares origin_zone_ids, origin_tag_ids, destination_zone_ids, destination_tag_ids, and service_ids. Warehouse exports include `receiving-orders`, `picking-orders`, `inventory-transactions`, `inventory-stock`, `warehouses`, and `inventory-items`.

Delivery Orders exposes optional string columns sender_name, sender_phone, receiver_name, receiver_phone. Sender values come from pickup_location.contact_info.full_name/phone; receiver values come from delivery_location.contact_info.full_name/phone. Missing contacts export as empty cells. The frontend should render these from catalog metadata and submit selected keys unchanged. No extra list API calls are required.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/catalogs/:module?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| module | path string | Yes | Module name (e.g. delivery, warehouse). | delivery |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "module": "delivery",
  "catalogs": [
    {
      "key": "delivery-orders",
      "category": "operational",
      "label": "Delivery Orders",
      "description": "All delivery orders with customer, driver, zone and payment fields.",
      "supportedFormats": [
        "csv",
        "xlsx"
      ],
      "dateField": {
        "key": "created_at",
        "label": "Created at"
      },
      "statuses": [
        {
          "value": "pending",
          "label": "pending"
        },
        {
          "value": "delivered",
          "label": "delivered"
        }
      ],
      "scopes": [
        {
          "type": "zone",
          "label": "Zone",
          "multi": true,
          "valueSource": {
            "endpoint": "/delivery/zones",
            "itemsField": "zones",
            "valueField": "id",
            "labelField": "name",
            "searchParam": "search"
          }
        },
        {
          "type": "service",
          "label": "Service",
          "multi": true,
          "valueSource": {
            "endpoint": "/delivery/services",
            "itemsField": "deliveryServices",
            "valueField": "id",
            "labelField": "name",
            "searchParam": "search"
          }
        },
        {
          "type": "driver",
          "label": "Driver",
          "multi": true,
          "valueSource": {
            "endpoint": "/delivery/drivers",
            "itemsField": "drivers",
            "valueField": "id",
            "labelField": "user.full_name",
            "searchParam": "search"
          }
        },
        {
          "type": "customer",
          "label": "Customer",
          "multi": true,
          "valueSource": {
            "endpoint": "/users/customers",
            "itemsField": "users",
            "valueField": "id",
            "labelField": "full_name",
            "searchParam": "search"
          }
        }
      ],
      "columns": [
        {
          "key": "delivery_code",
          "label": "Delivery Code",
          "type": "string",
          "defaultSelected": true,
          "relationOptions": []
        },
        {
          "key": "driver",
          "label": "Driver",
          "type": "json",
          "defaultSelected": false,
          "relationOptions": []
        },
        {
          "key": "sender_name",
          "label": "Sender Name",
          "type": "string",
          "defaultSelected": false,
          "relationOptions": []
        },
        {
          "key": "sender_phone",
          "label": "Sender Phone",
          "type": "string",
          "defaultSelected": false,
          "relationOptions": []
        },
        {
          "key": "receiver_name",
          "label": "Receiver Name",
          "type": "string",
          "defaultSelected": false,
          "relationOptions": []
        },
        {
          "key": "receiver_phone",
          "label": "Receiver Phone",
          "type": "string",
          "defaultSelected": false,
          "relationOptions": []
        }
      ]
    },
    {
      "key": "receiving-orders",
      "category": "operational",
      "label": "Receiving Orders",
      "description": "Inbound receiving orders with supplier, warehouse, status and arrival dates.",
      "supportedFormats": [
        "csv",
        "xlsx"
      ],
      "dateField": {
        "key": "expected_arrival_date",
        "label": "Expected arrival date"
      },
      "statuses": [
        {
          "value": "pending",
          "label": "pending"
        },
        {
          "value": "received",
          "label": "received"
        }
      ],
      "scopes": [
        {
          "type": "warehouse",
          "label": "Warehouse",
          "multi": true,
          "valueSource": {
            "endpoint": "/warehousing/warehouses",
            "itemsField": "warehouses",
            "valueField": "id",
            "labelField": "name",
            "searchParam": "search"
          }
        }
      ],
      "columns": [
        {
          "key": "reference_number",
          "label": "Reference Number",
          "type": "string",
          "defaultSelected": true,
          "relationOptions": []
        },
        {
          "key": "warehouse_id",
          "label": "Warehouse",
          "type": "uuid",
          "defaultSelected": true,
          "relationOptions": [
            {
              "output": "id",
              "label": "Warehouse ID"
            },
            {
              "output": "reference_value",
              "label": "Warehouse name",
              "table": "warehouses",
              "referenceField": "name"
            }
          ]
        }
      ]
    },
    {
      "key": "inventory-items",
      "category": "master-data",
      "label": "Inventory Items",
      "description": "Inventory item master data. Kept intentionally simple for now with warehouse scope only.",
      "supportedFormats": [
        "csv",
        "xlsx"
      ],
      "dateField": {
        "key": "updated_at",
        "label": "Updated at"
      },
      "scopes": [
        {
          "type": "warehouse",
          "label": "Warehouse",
          "multi": true,
          "valueSource": {
            "endpoint": "/warehousing/warehouses",
            "itemsField": "warehouses",
            "valueField": "id",
            "labelField": "name",
            "searchParam": "search"
          }
        }
      ],
      "columns": [
        {
          "key": "name",
          "label": "Name",
          "type": "string",
          "defaultSelected": true,
          "relationOptions": []
        },
        {
          "key": "sku",
          "label": "SKU",
          "type": "string",
          "defaultSelected": true,
          "relationOptions": []
        },
        {
          "key": "categories",
          "label": "Categories",
          "type": "string",
          "defaultSelected": false,
          "relationOptions": []
        }
      ]
    }
  ]
}
```

### Response 404 404 Module has no catalogs

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "module_has_no_catalogs",
  "module": "unknown"
}
```

## POST /report/exports/preview — Exports - Preview

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/report-exports-async#post-exports-preview)

Returns the first preview page for the configured export without creating a job. Uses the same validation and relation-resolution rules as the real export pipeline. Requires `view:reports`.

For module=delivery and catalog=delivery-orders, columns may include sender_name, sender_phone, receiver_name, receiver_phone. Both CSV and XLSX support these strings, including service-scoped exports. Omitted or null contacts produce empty cells.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/report/exports/preview?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| module | string |  |  |  |
| catalog | string |  |  |  |
| format | string |  |  |  |
| dateRange | object |  |  |  |
| dateRange.from | string |  |  |  |
| dateRange.to | string |  |  |  |
| statuses | array<string> |  |  |  |
| scope | object |  |  |  |
| scope.type | string |  |  |  |
| scope.values | array<string> |  |  |  |
| columns | array<string> |  |  |  |
| relationFields | array<object> |  |  |  |
| relationFields.field | string |  |  |  |
| relationFields.output | string |  |  |  |
| relationFields.table | string |  |  |  |
| relationFields.referenceField | string |  |  |  |



### Request body (raw)

```json
{
  "module": "delivery",
  "catalog": "driver-assignments",
  "format": "csv",
  "dateRange": {
    "from": "2026-05-01",
    "to": "2026-05-07"
  },
  "statuses": [
    "pending",
    "accepted"
  ],
  "scope": {
    "type": "driver",
    "values": [
      "<driver-uuid>"
    ]
  },
  "columns": [
    "assignment_time",
    "driver_id",
    "delivery_id",
    "status"
  ],
  "relationFields": [
    {
      "field": "driver_id",
      "output": "reference_value",
      "table": "drivers",
      "referenceField": "user.full_name"
    },
    {
      "field": "delivery_id",
      "output": "reference_value",
      "table": "deliveries",
      "referenceField": "delivery_code"
    }
  ]
}
```

### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "module": "delivery",
    "catalog": "driver-assignments",
    "category": "operational",
    "format": "csv",
    "previewLimit": 10,
    "totalRows": 24,
    "done": false,
    "columns": [
      {
        "key": "assignment_time",
        "label": "Assignment Time",
        "type": "datetime",
        "relationOutput": null
      },
      {
        "key": "driver_id",
        "label": "Driver",
        "type": "uuid",
        "relationOutput": "reference_value"
      },
      {
        "key": "delivery_id",
        "label": "Delivery",
        "type": "uuid",
        "relationOutput": "reference_value"
      },
      {
        "key": "status",
        "label": "Status",
        "type": "enum",
        "relationOutput": null
      }
    ],
    "rows": [
      {
        "assignment_time": "2026-05-07T10:30:00Z",
        "driver_id": "John Driver",
        "delivery_id": "DEL-1001",
        "status": "accepted"
      }
    ]
  }
}
```

### Response 400 400 Validation Error

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_request",
  "errors": [
    {
      "field": "relationFields.driver_id",
      "message": "relation_target_not_whitelisted"
    }
  ]
}
```

## POST /report/exports — Exports - Create Zone Pricing

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/report-exports-async#post-exports-create-zone-pricing)

Queues an XLSX or CSV export of filtered Zone Pricing rules for the authenticated tenant. Each rule produces one row. Origins, destinations and services are ordered readable strings; inactive historical relationship rows are excluded. Optional `filters` supports UUID arrays for origin_zone_ids, origin_tag_ids, destination_zone_ids, destination_tag_ids, and service_ids. Semantics: (origin zones OR origin tags) AND (destination zones OR destination tags) AND (services), then AND with optional statuses and updated_at dateRange. Empty arrays are ignored. Unknown filter keys, non-array values, and invalid UUIDs return 400 invalid_request. Requires `export:reports`.

For module=delivery and catalog=delivery-orders, columns may include sender_name, sender_phone, receiver_name, receiver_phone. Both CSV and XLSX support these strings, including service-scoped exports. Omitted or null contacts produce empty cells.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/report/exports?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| module | string |  |  |  |
| catalog | string |  |  |  |
| format | string |  |  |  |
| statuses | array<string> |  |  |  |
| dateRange | object |  |  |  |
| dateRange.from | string |  |  |  |
| dateRange.to | string |  |  |  |
| filters | object |  |  |  |
| filters.origin_zone_ids | array<string> |  |  |  |
| filters.origin_tag_ids | array |  |  |  |
| filters.destination_zone_ids | array<string> |  |  |  |
| filters.destination_tag_ids | array |  |  |  |
| filters.service_ids | array<string> |  |  |  |
| columns | array<string> |  |  |  |



### Request body (raw)

```json
{
  "module": "delivery",
  "catalog": "zone-pricing",
  "format": "xlsx",
  "statuses": [
    "active"
  ],
  "dateRange": {
    "from": "2026-08-01",
    "to": "2026-08-31T23:59:59Z"
  },
  "filters": {
    "origin_zone_ids": [
      "11111111-1111-4111-8111-111111111111"
    ],
    "origin_tag_ids": [],
    "destination_zone_ids": [
      "22222222-2222-4222-8222-222222222222"
    ],
    "destination_tag_ids": [],
    "service_ids": [
      "33333333-3333-4333-8333-333333333333"
    ]
  },
  "columns": [
    "origin_selector_type",
    "origins",
    "origin_count",
    "destination_selector_type",
    "destinations",
    "destination_count",
    "services",
    "base_price",
    "per_unit_price",
    "carrier_price",
    "effective_from",
    "effective_to",
    "status",
    "updated_at"
  ]
}
```

### Response 202 202 Accepted

Accepted

```json
{
  "success": true,
  "data": {
    "id": "export-job-uuid",
    "module": "delivery",
    "catalog": "zone-pricing",
    "category": "pricing",
    "format": "xlsx",
    "status": "pending"
  }
}
```

## POST /report/exports — Exports - Create

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/report-exports-async#post-exports-create)

Creates and queues an async export job. Returns immediately with `id` + `status:"pending"`. Subscribe to realtime on `report_export_jobs` filtered by `tenant_id` to track progress. Requires `export:reports`.

**relationFields** is an array of `{ field, output, table?, referenceField? }`:
- `field`: column key from `columns` (must be an ID column with `relationOptions` in the catalog).
- `output`: `"id"` keeps the raw UUID; `"reference_value"` resolves to a label.
- `table` + `referenceField` are required when `output === "reference_value"` and must match a whitelisted `relationOption` declared on the column. `referenceField` supports one nested level (e.g. `user.full_name` for `drivers` → `users`).

Use `GET /report/catalogs/:module` to discover the allowed `relationOptions` per column.

For module=delivery and catalog=delivery-orders, columns may include sender_name, sender_phone, receiver_name, receiver_phone. Both CSV and XLSX support these strings, including service-scoped exports. Omitted or null contacts produce empty cells.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/report/exports?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| module | string |  |  |  |
| catalog | string |  |  |  |
| format | string |  |  |  |
| dateRange | object |  |  |  |
| dateRange.from | string |  |  |  |
| dateRange.to | string |  |  |  |
| statuses | array<string> |  |  |  |
| scope | object |  |  |  |
| scope.type | string |  |  |  |
| scope.values | array<string> |  |  |  |
| columns | array<string> |  |  |  |
| relationFields | array<object> |  |  |  |
| relationFields.field | string |  |  |  |
| relationFields.output | string |  |  |  |
| relationFields.table | string |  |  |  |
| relationFields.referenceField | string |  |  |  |



### Request body (raw)

```json
{
  "module": "delivery",
  "catalog": "delivery-orders",
  "format": "csv",
  "dateRange": {
    "from": "2026-05-01",
    "to": "2026-05-07"
  },
  "statuses": [
    "assigned",
    "in_transit"
  ],
  "scope": {
    "type": "zone",
    "values": [
      "<zone-uuid-1>",
      "<zone-uuid-2>"
    ]
  },
  "columns": [
    "delivery_code",
    "customer_id",
    "status",
    "driver_id"
  ],
  "relationFields": [
    {
      "field": "customer_id",
      "output": "reference_value",
      "table": "users",
      "referenceField": "full_name"
    },
    {
      "field": "driver_id",
      "output": "reference_value",
      "table": "drivers",
      "referenceField": "user.full_name"
    }
  ]
}
```

### Response 202 202 Accepted

Accepted

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "00000000-0000-0000-0000-000000000010",
    "module": "delivery",
    "catalog": "delivery-orders",
    "category": "operational",
    "format": "csv",
    "status": "pending",
    "fileName": null,
    "fileSizeBytes": null,
    "rowCount": null,
    "progress": {
      "total": null,
      "done": 0
    },
    "downloadUrl": null,
    "requestedAt": "2025-01-15T12:00:00Z",
    "startedAt": null,
    "completedAt": null,
    "errorSummary": null,
    "metadata": null
  }
}
```

### Response 400 400 Validation Error

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_request",
  "errors": [
    {
      "field": "format",
      "message": "format must be one of csv, xlsx"
    },
    {
      "field": "columns",
      "message": "unknown column: foo"
    }
  ]
}
```

### Response 403 403 Insufficient permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "insufficient_permissions"
}
```

### Response 404 404 Catalog not found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "catalog_not_found",
  "module": "delivery",
  "catalog": "foo"
}
```

## GET /report/exports — Exports - List

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/report-exports-async#get-exports-list)

Paginated history of export jobs for the tenant. Filters by module/catalog/status. Requires `view:reports`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/exports?tenant_id={{tenant_id}}&language=en&module=delivery&status=ready&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |
| module | query string |  |  | delivery |
| catalog | query string | No |  | delivery-orders |
| status | query string |  |  | ready |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": [
    {
      "id": "00000000-0000-0000-0000-000000000010",
      "module": "delivery",
      "catalog": "delivery-orders",
      "category": "operational",
      "format": "csv",
      "status": "ready",
      "fileName": "delivery-orders-2025-01-15.csv",
      "fileSizeBytes": 52431,
      "rowCount": 1240,
      "progress": {
        "total": 1240,
        "done": 1240
      },
      "downloadUrl": null,
      "requestedAt": "2025-01-15T12:00:00Z",
      "startedAt": "2025-01-15T12:00:01Z",
      "completedAt": "2025-01-15T12:00:09Z",
      "errorSummary": null,
      "metadata": null
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1
}
```

## GET /report/exports/:id — Exports - Get one

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/report-exports-async#get-exports-get-one)

Single job. Use this for polling fallback when realtime is unavailable.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/exports/:id?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | 00000000-0000-0000-0000-000000000010 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "00000000-0000-0000-0000-000000000010",
    "module": "delivery",
    "catalog": "delivery-orders",
    "category": "operational",
    "format": "csv",
    "status": "processing",
    "fileName": null,
    "fileSizeBytes": null,
    "rowCount": null,
    "progress": {
      "total": 1240,
      "done": 500
    },
    "downloadUrl": null,
    "requestedAt": "2025-01-15T12:00:00Z",
    "startedAt": "2025-01-15T12:00:01Z",
    "completedAt": null,
    "errorSummary": null,
    "metadata": null
  }
}
```

### Response 404 404 Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "not_found"
}
```

## GET /report/exports/:id/download — Exports - Download (signed URL)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/report-exports-async#get-exports-download-signed-url)

Returns a short-lived (5 min) signed URL for the generated file. Job must be in `ready` status.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/exports/:id/download?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | 00000000-0000-0000-0000-000000000010 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "00000000-0000-0000-0000-000000000010",
    "module": "delivery",
    "catalog": "delivery-orders",
    "category": "operational",
    "format": "csv",
    "status": "ready",
    "fileName": "delivery-orders-2025-01-15.csv",
    "fileSizeBytes": 52431,
    "rowCount": 1240,
    "progress": {
      "total": 1240,
      "done": 1240
    },
    "downloadUrl": "https://<project>.supabase.co/storage/v1/object/sign/report-exports/exports/<tenant>/<job>/delivery-orders-2025-01-15.csv?token=...",
    "requestedAt": "2025-01-15T12:00:00Z",
    "startedAt": "2025-01-15T12:00:01Z",
    "completedAt": "2025-01-15T12:00:09Z",
    "errorSummary": null,
    "metadata": null
  }
}
```

### Response 409 409 Not Ready

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "not_ready",
  "status": "processing"
}
```

## POST /report/exports/:id/cancel — Exports - Cancel

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/report-exports-async#post-exports-cancel)

Cancels a `pending` or `processing` job. Requires `export:reports`. Returns 409 if the job is already in a terminal state.

For module=delivery and catalog=delivery-orders, columns may include sender_name, sender_phone, receiver_name, receiver_phone. Both CSV and XLSX support these strings, including service-scoped exports. Omitted or null contacts produce empty cells.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/report/exports/:id/cancel?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | 00000000-0000-0000-0000-000000000010 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "00000000-0000-0000-0000-000000000010",
    "status": "cancelled",
    "completedAt": "2025-01-15T12:00:30Z"
  }
}
```

### Response 409 409 Not Cancellable

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "not_cancellable_or_not_found"
}
```

## POST /report/exports/:id/_resume — Exports - Resume (Internal Cron)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/report-exports-async#post-exports-resume-internal-cron)

Re-dispatches an existing report-export job after its worker lease expires. The worker claims only pending jobs or jobs with an expired lease, preserving idempotency. `id` is the export-job UUID. HTTP 200 acknowledges dispatch, not completed file generation. Unauthorized messages are localized using the request language.

Internal backend/cron only. Send `apikey: {{supabase_secret_key}}` with the default managed Supabase secret key (`SUPABASE_SECRET_KEYS.default`). Never put this value in a frontend or mobile application. Missing, invalid, publishable, legacy anon, and legacy service-role bearer credentials return HTTP 401 before work is dispatched. `Authorization: Bearer ...` alone is not accepted. Frontend handoff: none; application clients must not call this internal endpoint.

Authentication: Internal backend or cron only. Send the managed Supabase secret in apikey. Application clients must not call this endpoint.

Request: POST https://staging-api.ondi.io/functions/v1/report/exports/:id/_resume

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| apikey | header string |  | Default managed Supabase secret key; backend only. | {{supabase_secret_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Existing report-export job UUID. | {{report_export_id}} |



### Request body (raw)

```json
{}
```

### Response 200 200 Dispatched

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "dispatched": true
}
```

### Response 401 401 Missing or invalid internal secret

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## GET /report/tenant/delivery/driver-performance — Driver Performance

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/report/overview#get-driver-performance)

## Summary
GET `/report/tenant/delivery/driver-performance`

Returns driver performance metrics computed by the nightly report scheduler. Data is read from the `driver_performance_metrics` table (populated during cron).

- Header `Authorization: Bearer {{access_token}}`

## Response fields

| Field | Type | Description |
|-------|------|-------------|
| `driver_id` | UUID | Driver identifier |
| `driver_name` | string | Driver's full name |
| `total_deliveries` | integer | Total terminal-status assignments (completed + failed) |
| `successful_deliveries` | integer | Assignments with status completed/delivered/closed |
| `failed_deliveries` | integer | Assignments with status failed/cancelled/rejected/returned |
| `on_time_deliveries` | integer | Completed before scheduled_delivery_time |
| `late_deliveries` | integer | Completed after scheduled_delivery_time |
| `success_rate` | decimal | (successful / total) × 100 |
| `on_time_rate` | decimal | (on_time / (on_time + late)) × 100 |
| `average_delivery_time_minutes` | integer | Mean minutes from pickup to drop-off |
| `total_distance_km` | decimal | Sum of distance_traveled across assignments |
| `km_per_delivery` | decimal | total_distance / successful_deliveries |
| `total_delivery_value` | decimal | Sum of (cod_amount + delivery_fee) for completed |

## KPI Cards (aggregate across all drivers)
- **Total Deliveries** — sum of total_deliveries
- **Overall Success Rate** — sum(successful) / sum(total) × 100
- **Overall On-Time Rate** — sum(on_time) / sum(on_time + late) × 100
- **Avg Delivery Time** — weighted average of average_delivery_time_minutes
- **Total Revenue Handled** — sum of total_delivery_value
- **Active Drivers** — count of drivers with total_deliveries > 0

## Column Filters (planned)
All numeric columns support `{min, max}` range filtering:
- `total_deliveries`, `success_rate`, `on_time_rate`, `average_delivery_time`, `km_per_delivery`, `total_delivery_value`

## Error responses
- `400`: `{ "success": false, "message": "tenantId missing in request context", "data": null }`
- `500`: `{ "success": false, "message": "query_failed", "data": null }`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/report/tenant/delivery/driver-performance?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "data": [
    {
      "driver_id": "a1b2c3d4-0000-0000-0000-000000000001",
      "driver_name": "Ahmed Ali",
      "total_deliveries": 120,
      "successful_deliveries": 108,
      "failed_deliveries": 12,
      "on_time_deliveries": 95,
      "late_deliveries": 13,
      "success_rate": 90,
      "on_time_rate": 87.96,
      "average_delivery_time_minutes": 28,
      "total_distance_km": 840.5,
      "km_per_delivery": 7.78,
      "total_delivery_value": 45000
    },
    {
      "driver_id": "a1b2c3d4-0000-0000-0000-000000000002",
      "driver_name": "Mohammed Saeed",
      "total_deliveries": 95,
      "successful_deliveries": 90,
      "failed_deliveries": 5,
      "on_time_deliveries": 82,
      "late_deliveries": 8,
      "success_rate": 94.74,
      "on_time_rate": 91.11,
      "average_delivery_time_minutes": 24,
      "total_distance_km": 620.3,
      "km_per_delivery": 6.89,
      "total_delivery_value": 38500
    }
  ]
}
```

### Response 200 200 OK (empty - no data yet)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "data": []
}
```

### Response 400 400 Bad Request (missing tenant)

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "tenantId missing in request context",
  "data": null
}
```

### Response 500 500 Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "query_failed",
  "data": null
}
```

## GET /roles — List Roles

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/roles#get-list-roles)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

user_access:system or manage:roles:system to list both scopes. manage:user_access:tenant permits tenant role definitions only. Other callers receive 403. A tenant caller explicitly requesting scope=system receives 403.

### Parameters

scope (system/tenant), search, page (default 1), limit (default 10), language. Filtering happens in SQL before pagination and counting; total describes the same roles as the returned page. Each role includes permissions [{id,name}]. Response remains {success:true,message,roles,total,page,limit}.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. No navigation action is added.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/roles?language=en&scope=&page=1&limit=10&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |
| scope | query string | No | Filter roles by scope (Optional, values: 'system', 'tenant') |  |
| search | query string | No | Search roles by name or description (Optional) |  |
| page | query string | No | Page number for pagination (Optional, default: 1) | 1 |
| limit | query string | No | Number of records per page (Optional, default: 10) | 10 |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | max-age=60 |



```json
{
  "success": true,
  "message": "Roles retrieved successfully",
  "roles": [
    {
      "id": "role-001",
      "name": "System Admin",
      "description": "System administrator with full access",
      "scope": "system",
      "created_at": "2023-01-01T00:00:00Z",
      "updated_at": "2023-01-01T00:00:00Z"
    },
    {
      "id": "role-002",
      "name": "System Auditor",
      "description": "Read-only access to system resources",
      "scope": "system",
      "created_at": "2023-01-01T00:00:00Z",
      "updated_at": "2023-01-01T00:00:00Z"
    }
  ],
  "total": 2,
  "page": 1,
  "limit": 10
}
```

### Response 500 Error Response

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Error retrieving roles"
}
```

### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

## POST /roles — Create Role

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/roles#post-create-role)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

roles:system. Both system and tenant role definitions are globally shared; tenant-scoped user-access permission does not allow editing these definitions.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. No navigation action is added.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/roles?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string | Yes | (required non-empty string) |  |
| description | string | No | (optional string). Duplicate name/scope returns 409. Success remains 201 {success:true,message,role}. |  |
| scope | string | Yes | (required system/tenant) |  |



### Request body (raw)

```json
{
  "name": "example_role",
  "description": "Example role description",
  "scope": "tenant"
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Role created successfully",
  "role": {
    "id": "new-role-uuid",
    "name": "example_role",
    "description": "Example role description",
    "scope": "tenant",
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-01-01T00:00:00Z"
  }
}
```

### Response 400 Error - Bad Request



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid input data"
}
```

### Response 409 Error - Conflict



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Role with this name already exists"
}
```

### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

## PUT /roles/:roleId — Update Role

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/roles#put-update-role)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

roles:system. Both system and tenant role definitions are globally shared; tenant-scoped user-access permission does not allow editing these definitions.

role ID. Body: name and/or description. Role scope is immutable. Unknown role: 404. Duplicate name/scope: 409. Success remains 200 {success:true,message,role}.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. No navigation action is added.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/roles/:roleId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| roleId | path string | Yes | UUID of the role to update (Required) | role-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| description | string |  |  |  |



### Request body (raw)

```json
{
  "name": "updated_role_name",
  "description": "Updated role description"
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Role updated successfully",
  "role": {
    "id": "role-uuid",
    "name": "updated_role_name",
    "description": "Updated role description",
    "scope": "system",
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-01-01T00:00:00Z"
  }
}
```

### Response 404 Error - Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Role not found"
}
```

### Response 409 Error - Conflict



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Role with this name already exists"
}
```

### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

## DELETE /roles/:roleId — Delete Role

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/roles#delete-delete-role)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

roles:system. Both system and tenant role definitions are globally shared; tenant-scoped user-access permission does not allow editing these definitions.

Record removal is blocked by the preservation policy. Unauthorized callers receive 403. Authorized callers receive 409 rbac_preservation_conflict. No role, permission assignment, user assignment, or dependent record is deleted. There is no success response while this policy is active.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. No navigation action is added.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/roles/:roleId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| roleId | path string | Yes | UUID of the role to delete (Required) | role-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 409 rbac_preservation_conflict



```json
{
  "success": false,
  "code": "rbac_preservation_conflict",
  "message": "This operation would remove existing access records and is not permitted."
}
```

## POST /roles/:roleId/permissions — Assign Permission to Role

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/roles#post-assign-permission-to-role)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

roles:system. Both system and tenant role definitions are globally shared; tenant-scoped user-access permission does not allow editing these definitions.

{"permission_ids":["permission-uuid", "another-permission-uuid"]}. This is the complete desired permission set, not a single permission_id. All IDs must exist and every permission scope must match the role scope. Duplicate IDs are normalized. Existing assignments omitted from the array produce 409 rbac_preservation_conflict before any write; no removals occur. An unchanged set succeeds with an empty role_permission array. New permissions are inserted in one batch; duplicate assignments are ignored using the existing unique constraint. Authorization uses middleware permissions, and request auditing uses the existing middleware. Non-array or empty permission_ids returns 400; unknown permission IDs return 404. Success: 201 {success:true,message,role_permission:[newlyInsertedAssignments]}.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. No navigation action is added.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/roles/:roleId/permissions?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| roleId | path string | Yes | UUID of the role to assign the permission to (Required) | role-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| permission_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "permission_ids": [
    "{{permission_id}}"
  ]
}
```

### Response 201 Success



```json
{
  "success": true,
  "message": "Permission assigned to role successfully",
  "role_permission": []
}
```

### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 403 rbac_invalid_scope



```json
{
  "success": false,
  "code": "rbac_invalid_scope",
  "message": "The role, permission, or tenant scope is not allowed."
}
```

### Response 409 rbac_preservation_conflict



```json
{
  "success": false,
  "code": "rbac_preservation_conflict",
  "message": "This operation would remove existing access records and is not permitted."
}
```

## DELETE /roles/:roleId/permissions/:permissionId — Remove Permission from Role

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/roles#delete-remove-permission-from-role)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

roles:system. Both system and tenant role definitions are globally shared; tenant-scoped user-access permission does not allow editing these definitions.

Record removal is blocked by the preservation policy. Unauthorized callers receive 403. Authorized callers receive 409 rbac_preservation_conflict. No role, permission assignment, user assignment, or dependent record is deleted. There is no success response while this policy is active.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. No navigation action is added.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/roles/:roleId/permissions/:permissionId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| roleId | path string | Yes | UUID of the role to remove the permission from (Required) | role-uuid |
| permissionId | path string | Yes | UUID of the permission to remove (Required) | permission-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 409 rbac_preservation_conflict



```json
{
  "success": false,
  "code": "rbac_preservation_conflict",
  "message": "This operation would remove existing access records and is not permitted."
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Get Route - Basic

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/route-calculation#get-get-route-basic-2)

Calculate optimized route from driver's stored location to pickup and delivery locations.

- Requires valid JWT token or API key
- User must be system admin, dispatcher, tenant admin, or the driver themselves

### Authorization
- System Admins: Full access to all routes
- Dispatchers/Tenant Admins: Access to routes within their tenant
- Drivers: Access to their own assigned routes only

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authentication | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | UUID of the driver |  |
| delivery_id | path string | Yes | UUID of the delivery |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Preferred language for response messages (default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route calculated successfully",
  "route": {
    "encoded_polyline": "u{~vFvyys@fS]...",
    "total_distance_meters": 15420,
    "total_duration_seconds": 1800,
    "waypoints": [
      {
        "lat": 40.7128,
        "lng": -74.006,
        "type": "pickup"
      },
      {
        "lat": 40.7589,
        "lng": -73.9851,
        "type": "dropoff"
      }
    ]
  }
}
```

### Response 400 Error - Invalid Parameters

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request parameters"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 404 Error - Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver or delivery not found"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Get Route - With Current Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/route-calculation#get-get-route-with-current-location-2)

Calculate optimized route using driver's real-time location instead of stored location.

- Requires valid JWT token or API key
- User must be system admin, dispatcher, tenant admin, or the driver themselves

### Use Case
- When driver's real-time location is more accurate than stored location
- For live route updates during delivery
- When driver location hasn't been updated in the database recently

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}?current_lat={{current_lat}}&current_lng={{current_lng}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authentication | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | UUID of the driver |  |
| delivery_id | path string | Yes | UUID of the delivery |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| current_lat | query string |  | Driver's current latitude (-90 to 90) | {{current_lat}} |
| current_lng | query string |  | Driver's current longitude (-180 to 180) | {{current_lng}} |
| language | query string | No | Preferred language for response messages (default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Route calculated successfully",
  "route": {
    "encoded_polyline": "u{~vFvyys@fS]...",
    "total_distance_meters": 12350,
    "total_duration_seconds": 1620,
    "waypoints": [
      {
        "lat": 40.7128,
        "lng": -74.006,
        "type": "pickup"
      },
      {
        "lat": 40.7589,
        "lng": -73.9851,
        "type": "dropoff"
      }
    ]
  }
}
```

### Response 400 Error - Invalid Coordinates

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid coordinates"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Unauthorized Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-unauthorized-access-2)

Test unauthorized access without authentication token.

### Expected Response
- Status: 401 Unauthorized
- Message: Authentication required

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 401 Unauthorized Response

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Authentication required"
}
```

## GET /route/for-driver/invalid-uuid/delivery/{{delivery_id}} — Invalid Driver ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-invalid-driver-id-2)

Test with invalid driver ID format.

### Expected Response
- Status: 400 Bad Request
- Message: Invalid driver ID format

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/invalid-uuid/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 400 Invalid Driver ID Response

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid driver ID format"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/invalid-uuid — Invalid Delivery ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-invalid-delivery-id-2)

Test with invalid delivery ID format.

### Expected Response
- Status: 400 Bad Request
- Message: Invalid delivery ID format

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/invalid-uuid

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |



### Response 400 Invalid Delivery ID Response

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid delivery ID format"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Invalid Coordinates

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-invalid-coordinates-2)

Test with invalid coordinate values (out of valid range).

### Expected Response
- Status: 400 Bad Request
- Message: Invalid coordinates

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}?current_lat=91&current_lng=181

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| current_lat | query string |  | Invalid latitude (out of range) | 91 |
| current_lng | query string |  | Invalid longitude (out of range) | 181 |



### Response 400 Invalid Coordinates Response

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid coordinates"
}
```

## GET /route/for-driver/00000000-0000-0000-0000-000000000000/delivery/{{delivery_id}} — Driver Not Found

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-driver-not-found-2)

Test with non-existent driver ID.

### Expected Response
- Status: 404 Not Found
- Message: Driver not found

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/00000000-0000-0000-0000-000000000000/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 404 Driver Not Found Response

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not found"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/00000000-0000-0000-0000-000000000000 — Delivery Not Found

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-delivery-not-found-2)

Test with non-existent delivery ID.

### Expected Response
- Status: 404 Not Found
- Message: Delivery not found

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/00000000-0000-0000-0000-000000000000

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |



### Response 404 Delivery Not Found Response

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery not found"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Google Maps API Error

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/error-scenarios#get-google-maps-api-error-2)

Simulate Google Maps API error scenarios (when API key is invalid, quota exceeded, etc.).

### No-route contract

Google Maps `ZERO_RESULTS` returns HTTP 404 with stable `code=no_route_found` and a localized plain-language `message`; it is not an internal server error. Frontends should display `message`, may branch on `code`, and should not retry the same coordinates as a server failure. The driver and delivery IDs remain unchanged and no navigation action is required.

### Other possible error responses
- API quota exceeded
- Invalid API key
- Location not found

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 500 API Quota Exceeded

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "API quota exceeded"
}
```

### Response 422 No Route Found

Unprocessable Entity

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "no_route_found",
  "message": "No route found between driver location and pickup location. The driver cannot reach the pickup location."
}
```

### Response 500 Location Not Found

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Location not found"
}
```

## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — System Admin Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-system-admin-access-2)

Test route calculation with system admin privileges.

### Prerequisites
- User must have `is_system_admin: true`
- Should have access to all routes regardless of tenant

### Expected Behavior
- Full access to any driver/delivery combination
- No tenant restrictions

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | System admin JWT token | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Dispatcher Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-dispatcher-access-2)

Test route calculation with dispatcher privileges.

### Prerequisites
- User must have `delivery_dispatch` permission OR
- User must have `dispatcher` role OR
- User must have `tenant_admin` role
- Driver and delivery must belong to same tenant as user

### Expected Behavior
- Access to routes within their tenant only
- Cannot access routes from other tenants

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Dispatcher JWT token | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Driver Self Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-driver-self-access-2)

Test route calculation where driver accesses their own route.

### Prerequisites
- The authenticated user must be linked to the driver record
- Driver's `user_id` must match the authenticated user's ID
- Delivery must belong to same tenant

### Expected Behavior
- Driver can only access their own routes
- Cannot access routes for other drivers

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Driver's own JWT token | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — API Key Authentication

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-api-key-authentication-2)

Test route calculation using API key authentication instead of JWT.

### Prerequisites
- Valid API key with appropriate permissions
- API key must belong to same tenant as driver/delivery

### Expected Behavior
- Same authorization rules apply as JWT authentication
- API key tenant context is used for authorization

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  | API key for authentication | {{api_key}} |
| Content-Type | header string |  |  | application/json |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



## GET /route/for-driver/{{driver_id}}/delivery/{{delivery_id}} — Cross-Tenant Access Denied

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/delivery-last-mile/testing-scenarios#get-cross-tenant-access-denied-2)

Test that users cannot access routes from different tenants.

### Prerequisites
- User belongs to Tenant A
- Driver/Delivery belongs to Tenant B
- User is not system admin

### Expected Response
- Status: 403 Forbidden
- Message: Insufficient permissions

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/route/for-driver/{{driver_id}}/delivery/{{delivery_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token from different tenant | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| driver_id | path string | Yes | Variable used inside the request path. |  |
| delivery_id | path string | Yes | Variable used inside the request path. |  |



### Response 403 Cross-Tenant Access Denied

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## POST /restaurant/locations/:locationId/state — Set Location State

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/availability#post-set-location-state)

Toggles operational state for a location. When is_manual_mode=true, manual_is_open controls open/closed regardless of schedules.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/state

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_manual_mode | boolean |  |  |  |
| manual_is_open | boolean |  |  |  |



### Request body (raw)

```json
{
  "is_manual_mode": true,
  "manual_is_open": false
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{locationId}}",
    "is_manual_mode": true,
    "manual_is_open": false,
    "availability_version": 42
  }
}
```

## POST /restaurant/menus/:menuId/state — Set Menu State

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/availability#post-set-menu-state)

Set a menu to automatic (schedule-driven) or manual (manual_is_active=true/false). Bumps location availability_version by trigger.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/state

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| activation_mode | string |  |  |  |
| manual_is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "activation_mode": "manual",
  "manual_is_active": true
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{menuId}}",
    "location_id": "{{locationId}}",
    "activation_mode": "manual",
    "manual_is_active": true
  }
}
```

## GET /restaurant/locations/:locationId/menus/active — List Active Menus

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/availability#get-list-active-menus)

Returns menus active 'now' in the location time zone. Manual active menus are included.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/menus/active

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{menuId}}",
      "location_id": "{{locationId}}",
      "name": {
        "en": "Lunch"
      },
      "activation_mode": "automatic",
      "manual_is_active": false
    }
  ]
}
```

## GET /restaurant/locations/:locationId/products/available — List Available Products (now)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/availability#get-list-available-products-now)

Lists currently available products (Location Open ∧ Any Menu Active ∧ Product Enabled) using optimized RPC.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/products/available?category_id={{category_id}}&limit=12&page=1

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| category_id | query string |  |  | {{category_id}} |
| limit | query string |  |  | 12 |
| page | query string |  |  | 1 |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{productId}}",
      "location_id": "{{locationId}}",
      "category": {
        "id": "{{category_id}}",
        "name": {
          "en": "Burgers"
        }
      },
      "name": {
        "en": "Cheeseburger"
      },
      "description": {},
      "is_active": true,
      "images": [],
      "primary_variant": {
        "id": "{{variantId}}",
        "price": 8.99,
        "is_primary": true
      },
      "variant_count": 2,
      "created_at": "2025-11-10T10:00:00Z"
    }
  ],
  "page": 1,
  "limit": 12
}
```

## GET /restaurant/locations/:locationId/products/available — List Available Products (at)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/availability#get-list-available-products-at)

Lists available products at a specific timestamp (time-travel) using RPC.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/products/available?at=2025-12-25T12:00:00Z

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| at | query string |  |  | 2025-12-25T12:00:00Z |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [ ... ],
  "page": 1,
  "limit": 12
}
```

## GET /restaurant/cashier/tables — List Tables (Cashier)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cashier#get-list-tables-cashier)

Show the tables at a restaurant location in the cashier workspace.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/cashier/tables?location_id={{location_id}}&page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | query string |  |  | {{location_id}} |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |



## GET /restaurant/cashier/orders/dine-in — List Dine-In Orders (Cashier)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cashier#get-list-dine-in-orders-cashier)

Browse dine-in orders at a location, with status and pagination filters for the cashier view.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/cashier/orders/dine-in?location_id={{location_id}}&status=&page=1&limit=10&append=false

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | query string |  |  | {{location_id}} |
| status | query string |  |  |  |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |
| append | query string |  |  | false |



## GET /restaurant/cashier/orders/takeout — List Takeout Orders (Cashier)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cashier#get-list-takeout-orders-cashier)

Browse takeout orders at a location for cashier processing.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/cashier/orders/takeout?location_id={{location_id}}&status=&page=1&limit=10&append=false

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | query string |  |  | {{location_id}} |
| status | query string |  |  |  |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |
| append | query string |  |  | false |



## GET /restaurant/cashier/orders/delivery — List Delivery Orders (Cashier)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cashier#get-list-delivery-orders-cashier)

### Returns delivery orders including delivery_status_timeline

[{ status, from_status, at }].

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/cashier/orders/delivery?location_id={{location_id}}&status=&page=1&limit=10&append=false

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | query string |  |  | {{location_id}} |
| status | query string |  |  |  |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |
| append | query string |  |  | false |



## GET /restaurant/cashier/orders/:orderId — Get Order Detail (Cashier)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cashier#get-get-order-detail-cashier)

### Returns order detail including delivery_status_timeline

[{ status, from_status, at }].

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/cashier/orders/:orderId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| orderId | path string | Yes | Dynamic path value from the request URL. |  |



## POST /restaurant/cashier/orders/:orderId/mark-paid — Mark Pay-At-Counter Order Paid

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cashier#post-mark-pay-at-counter-order-paid)

Record payment for an order using pay-at-counter. A pending order becomes approved; an order using another payment method is rejected.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/cashier/orders/:orderId/mark-paid

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| orderId | path string | Yes | Dynamic path value from the request URL. |  |



## POST /restaurant/cashier/sessions/:sessionId/close — Close Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cashier#post-close-session)

Close a dine-in session and record its closing time.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/cashier/sessions/:sessionId/close

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sessionId | path string | Yes | Dynamic path value from the request URL. |  |



## GET /restaurant/kds/orders — List KDS Orders

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/kds#get-list-kds-orders)

Load the kitchen display queue for a location, filtered by kitchen ticket status.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/kds/orders?location_id={{location_id}}&statuses=queued,preparing,ready&page=1&limit=10&append=false

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | query string |  |  | {{location_id}} |
| statuses | query string |  |  | queued,preparing,ready |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |
| append | query string |  |  | false |



## POST /restaurant/kds/tickets/:id/print — Print Ticket

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/kds#post-print-ticket)

Mark a kitchen ticket as printing and record its print time. The response returns the updated ticket.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/kds/tickets/:id/print

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



## POST /restaurant/kds/tickets/:id/status — Update Ticket Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/kds#post-update-ticket-status)

Move a kitchen ticket to a supported status and optionally update its preparation estimate.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/kds/tickets/:id/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "status": "preparing"
}
```

## POST /restaurant/kds/tickets/:id/eta — Update Ticket ETA

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/kds#post-update-ticket-eta)

Set the kitchen ticket’s estimated preparation time in minutes.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/kds/tickets/:id/eta

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| eta_minutes | number |  |  |  |



### Request body (raw)

```json
{
  "eta_minutes": 15
}
```

## POST /restaurant/locations — Create Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/locations#post-create-location)

Creates a restaurant location under the tenant in draft status. Address (latitude, longitude, full_address) is required. Optional: is_manual_mode, manual_is_open.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| slug | string |  |  |  |
| languages | array<string> |  |  |  |
| time_zone | string |  |  |  |
| branding_settings | object |  |  |  |
| branding_settings.logo | string |  |  |  |
| branding_settings.cover_image | string |  |  |  |
| branding_settings.banners | array<string> |  |  |  |
| address | object |  |  |  |
| address.latitude | number |  |  |  |
| address.longitude | number |  |  |  |
| address.full_address | string |  |  |  |
| address.city | string |  |  |  |
| address.country | string |  |  |  |
| is_manual_mode | boolean |  |  |  |
| manual_is_open | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Downtown Diner"
  },
  "slug": "downtown-diner",
  "languages": [
    "en"
  ],
  "time_zone": "UTC",
  "branding_settings": {
    "logo": "https://example.com/logo.png",
    "cover_image": "https://example.com/cover.png",
    "banners": [
      "https://example.com/banner1.png"
    ]
  },
  "address": {
    "latitude": 12.9716,
    "longitude": 77.5946,
    "full_address": "MG Road, Bengaluru, IN",
    "city": "Bengaluru",
    "country": "IN"
  },
  "is_manual_mode": false,
  "manual_is_open": true
}
```

### Response 201 201 Created

Created

```json
{
  "success": true,
  "data": {
    "id": "{{locationId}}",
    "tenant_id": "{{tenantId}}",
    "name": {
      "en": "Downtown Diner"
    },
    "slug": "downtown-diner",
    "active": true,
    "admin_status": "draft",
    "languages": [
      "en"
    ],
    "time_zone": "UTC",
    "branding_settings": {
      "logo": "https://example.com/logo.png",
      "cover_image": "https://example.com/cover.png",
      "banners": [
        "https://example.com/banner1.png"
      ]
    },
    "address": {
      "latitude": 12.9716,
      "longitude": 77.5946,
      "full_address": "MG Road, Bengaluru, IN"
    },
    "is_manual_mode": false,
    "manual_is_open": true,
    "availability_version": 1,
    "created_at": "2025-11-10T10:00:00Z"
  }
}
```

## GET /restaurant/locations — List Locations

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/locations#get-list-locations)

Lists locations for the tenant with pagination. Includes is_open computed field.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations?page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{locationId}}",
      "name": {
        "en": "Downtown Diner"
      },
      "slug": "downtown-diner",
      "active": true,
      "admin_status": "draft",
      "languages": [
        "en"
      ],
      "time_zone": "UTC",
      "is_open": true,
      "availability_version": 1
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

## GET /restaurant/locations/:id — Get Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/locations#get-get-location)

Fetch a single location by id. Includes is_open computed field.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{locationId}}",
    "tenant_id": "{{tenantId}}",
    "name": {
      "en": "Downtown Diner"
    },
    "slug": "downtown-diner",
    "active": true,
    "admin_status": "draft",
    "languages": [
      "en"
    ],
    "time_zone": "UTC",
    "address": {
      "latitude": 12.9716,
      "longitude": 77.5946,
      "full_address": "MG Road, Bengaluru, IN"
    },
    "is_manual_mode": false,
    "manual_is_open": true,
    "is_open": true,
    "availability_version": 1
  }
}
```

## PUT /restaurant/locations/:id — Update Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/locations#put-update-location)

Updates a location. Returns updated location object including availability_version.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/locations/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| slug | string |  |  |  |
| languages | array<string> |  |  |  |
| time_zone | string |  |  |  |
| branding_settings | object |  |  |  |
| branding_settings.logo | string |  |  |  |
| branding_settings.cover_image | string |  |  |  |
| branding_settings.banners | array<string> |  |  |  |
| active | boolean |  |  |  |
| address | object |  |  |  |
| address.latitude | number |  |  |  |
| address.longitude | number |  |  |  |
| address.full_address | string |  |  |  |
| address.city | string |  |  |  |
| address.country | string |  |  |  |
| is_manual_mode | boolean |  |  |  |
| manual_is_open | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Downtown Diner"
  },
  "slug": "downtown-diner",
  "languages": [
    "en"
  ],
  "time_zone": "UTC",
  "branding_settings": {
    "logo": "https://example.com/logo_updated.png",
    "cover_image": "https://example.com/cover_updated.png",
    "banners": [
      "https://example.com/banner_updated.png"
    ]
  },
  "active": true,
  "address": {
    "latitude": 12.9716,
    "longitude": 77.5946,
    "full_address": "MG Road, Bengaluru, IN",
    "city": "Bengaluru",
    "country": "IN"
  },
  "is_manual_mode": false,
  "manual_is_open": true
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{locationId}}",
    "name": {
      "en": "Downtown Diner"
    },
    "slug": "downtown-diner",
    "active": true,
    "admin_status": "draft",
    "languages": [
      "en"
    ],
    "time_zone": "UTC",
    "is_manual_mode": false,
    "manual_is_open": true,
    "is_open": true,
    "availability_version": 2
  }
}
```

## POST /restaurant/locations/:locationId/submit-for-approval — Submit Location For Approval

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/locations#post-submit-location-for-approval)

Submits a draft restaurant location to system admin review. Changes admin_status from draft to pending_approval.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/submit-for-approval

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{locationId}}",
    "tenant_id": "{{tenantId}}",
    "slug": "downtown-diner",
    "admin_status": "pending_approval"
  },
  "message": "Restaurant location submitted for approval"
}
```

### Response 400 400 Bad Request

Bad Request

```json
{
  "success": false,
  "message": "Restaurant location cannot be submitted"
}
```

## POST /restaurant/locations/:locationId/pause — Pause Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/locations#post-pause-location)

Temporarily pause the location (deactivate current time window). Requires automatic schedule mode.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/pause

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "message": "Location paused successfully"
}
```

## POST /restaurant/locations/:locationId/resume — Resume Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/locations#post-resume-location)

Resume the location (reactivate current time window) if manually paused.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/resume

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "message": "Location resumed successfully"
}
```

## POST /restaurant/locations/:locationId/state — Set Location State (manual)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/locations#post-set-location-state-manual)

Operational toggle for the location; bumps availability_version for instant cache invalidation.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/state

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_manual_mode | boolean |  |  |  |
| manual_is_open | boolean |  |  |  |



### Request body (raw)

```json
{
  "is_manual_mode": true,
  "manual_is_open": false
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{locationId}}",
    "is_manual_mode": true,
    "manual_is_open": false,
    "availability_version": 43
  }
}
```

## POST /restaurant/modifier-groups — Create Modifier Group

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#post-create-modifier-group)

Create a group of product choices, such as extras or preparation options, with selection limits and optional initial choices.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/modifier-groups

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | string |  |  |  |
| name | object |  |  |  |
| name.en | string |  |  |  |
| required | boolean |  |  |  |
| min_select | number |  |  |  |
| max_select | number |  |  |  |
| active | boolean |  |  |  |
| options | array<object> |  |  |  |
| options.name | object |  |  |  |
| options.name.en | string |  |  |  |
| options.price_delta | number |  |  |  |
| options.allows_quantity | boolean |  |  |  |
| options.active | boolean |  |  |  |



### Request body (raw)

```json
{
  "location_id": "{{location_id}}",
  "name": {
    "en": "Sauces"
  },
  "required": false,
  "min_select": 0,
  "max_select": 2,
  "active": true,
  "options": [
    {
      "name": {
        "en": "Ketchup"
      },
      "price_delta": 0,
      "allows_quantity": false,
      "active": true
    },
    {
      "name": {
        "en": "Mayo"
      },
      "price_delta": 0.2,
      "allows_quantity": false,
      "active": true
    }
  ]
}
```

## PUT /restaurant/modifier-groups/:id — Update Modifier Group (with options upsert)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#put-update-modifier-group-with-options-upsert)

Change a modifier group’s name or selection rules, and create or update its supplied options.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/modifier-groups/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| required | boolean |  |  |  |
| min_select | number |  |  |  |
| max_select | number |  |  |  |
| active | boolean |  |  |  |
| options | array<object> |  |  |  |
| options.id | string |  |  |  |
| options.name | object |  |  |  |
| options.name.en | string |  |  |  |
| options.price_delta | number |  |  |  |
| options.allows_quantity | boolean |  |  |  |
| options.active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Sauces"
  },
  "required": true,
  "min_select": 1,
  "max_select": 2,
  "active": true,
  "options": [
    {
      "id": "{{option_id_1}}",
      "name": {
        "en": "Ketchup"
      },
      "price_delta": 0.1,
      "allows_quantity": false,
      "active": true
    },
    {
      "name": {
        "en": "BBQ"
      },
      "price_delta": 0.3,
      "allows_quantity": false,
      "active": true
    }
  ]
}
```

## POST /restaurant/modifier-groups/:groupId/options — Create Modifier Option

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#post-create-modifier-option)

Add a choice to a modifier group, including its price adjustment and quantity behavior.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/modifier-groups/:groupId/options

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| groupId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| price_delta | number |  |  |  |
| allows_quantity | boolean |  |  |  |
| active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Ketchup"
  },
  "price_delta": 0,
  "allows_quantity": false,
  "active": true
}
```

## PUT /restaurant/modifier-options/:id — Update Modifier Option

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#put-update-modifier-option)

Change a modifier choice’s name, price adjustment, availability or quantity behavior.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/modifier-options/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| price_delta | number |  |  |  |
| allows_quantity | boolean |  |  |  |
| active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Ketchup"
  },
  "price_delta": 0.25,
  "allows_quantity": true,
  "active": true
}
```

## POST /restaurant/products/:productId/modifier-groups/:groupId — Link Group to Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#post-link-group-to-product)

Make a modifier group available on a product and set its display order.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/modifier-groups/:groupId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |
| groupId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sort_order | number |  |  |  |



### Request body (raw)

```json
{
  "sort_order": 0
}
```

## DELETE /restaurant/products/:productId/modifier-groups/:groupId — Unlink Group from Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#delete-unlink-group-from-product)

Remove the relationship between a product and a modifier group.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/modifier-groups/:groupId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |
| groupId | path string | Yes | Dynamic path value from the request URL. |  |



## GET /restaurant/modifier-groups/:id — Get Modifier Group (with options)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#get-get-modifier-group-with-options)

Read a modifier group together with the choices customers can select.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/modifier-groups/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



## GET /restaurant/locations/{{location_id}}/modifier-groups — List Modifier Groups (with options, by location)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#get-list-modifier-groups-with-options-by-location)

Browse a location’s modifier groups and their options before linking them to products.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/{{location_id}}/modifier-groups?page={{page}}&limit={{limit}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | {{page}} |
| limit | query string |  |  | {{limit}} |



## GET /restaurant/products/:productId/modifier-groups — Get Product Modifier Groups (with options)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#get-get-product-modifier-groups-with-options)

Load the modifier groups and choices linked to one product.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/modifier-groups?page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |



## DELETE /restaurant/modifier-groups/:id — Delete Modifier Group

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#delete-delete-modifier-group)

Remove a modifier group from the restaurant’s configuration.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/modifier-groups/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



## GET /restaurant/modifier-options/:id — Get Modifier Option

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#get-get-modifier-option)

Read the details of one modifier choice before displaying or editing it.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/modifier-options/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



## GET /restaurant/modifier-groups/:groupId/options — List Modifier Options (by group)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#get-list-modifier-options-by-group)

Browse the choices belonging to one modifier group.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/modifier-groups/:groupId/options?page={{page}}&limit={{limit}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| groupId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | {{page}} |
| limit | query string |  |  | {{limit}} |



## DELETE /restaurant/modifier-options/:id — Delete Modifier Option

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/modifiers#delete-delete-modifier-option)

Remove a choice from its modifier group.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/modifier-options/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



## POST /restaurant/locations/:locationId/orders — Create Order from Cart (Dine-in)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#post-create-order-from-cart-dine-in)

Submit a dine-in cart as an order associated with a table and dining session.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/orders

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| cart_id | string |  |  |  |
| order_type | string |  |  |  |
| payment_method_id | string |  |  |  |
| table_id | string |  |  |  |
| session_id | string |  |  |  |
| tip_amount | number |  |  |  |
| notes | null |  |  |  |



### Request body (raw)

```json
{
  "cart_id": "{{cart_id}}",
  "order_type": "dine_in",
  "payment_method_id": "{{pay_at_counter_payment_method_id}}",
  "table_id": "{{table_id}}",
  "session_id": "{{session_id}}",
  "tip_amount": 0,
  "notes": null
}
```

## POST /restaurant/locations/:locationId/orders — Create Order from Cart (Take-out)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#post-create-order-from-cart-take-out)

Submit a cart as a takeout order using the selected payment method.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/orders

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| cart_id | string |  |  |  |
| order_type | string |  |  |  |
| payment_method_id | string |  |  |  |
| tip_amount | number |  |  |  |
| notes | null |  |  |  |
| delivery_data | object |  |  |  |
| delivery_data.contact_info | object |  |  |  |
| delivery_data.contact_info.name | string |  |  |  |
| delivery_data.contact_info.phone | string |  |  |  |



### Request body (raw)

```json
{
  "cart_id": "{{cart_id}}",
  "order_type": "take_out",
  "payment_method_id": "{{pay_at_counter_payment_method_id}}",
  "tip_amount": 0,
  "notes": null,
  "delivery_data": {
    "contact_info": {
      "name": "Walk-in Customer",
      "phone": "+971500000000"
    }
  }
}
```

## POST /public/restaurant/:slug/cart/calculate-fee — Calculate Delivery Fee (Public)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#post-calculate-delivery-fee-public)

Calculate the restaurant cart’s delivery fee for a destination address before checkout.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/cart/calculate-fee

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| address | object |  |  |  |
| address.lat | number |  |  |  |
| address.lng | number |  |  |  |



### Request body (raw)

```json
{
  "address": {
    "lat": 25.2048,
    "lng": 55.2708
  }
}
```

## POST /restaurant/locations/:locationId/orders — Create Order from Cart (Delivery - Wallet)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#post-create-order-from-cart-delivery-wallet)

Submit a delivery cart using wallet payment and the supplied delivery details.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/orders

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| cart_id | string |  |  |  |
| order_type | string |  |  |  |
| payment_method_id | string |  |  |  |
| tip_amount | number |  |  |  |
| notes | null |  |  |  |
| delivery_data | object |  |  |  |
| delivery_data.address | object |  |  |  |
| delivery_data.address.full_address | string |  |  |  |
| delivery_data.address.lat | number |  |  |  |
| delivery_data.address.lng | number |  |  |  |
| delivery_data.contact_info | object |  |  |  |
| delivery_data.contact_info.name | string |  |  |  |
| delivery_data.contact_info.phone | string |  |  |  |



### Request body (raw)

```json
{
  "cart_id": "{{cart_id}}",
  "order_type": "delivery",
  "payment_method_id": "{{wallet_payment_method_id}}",
  "tip_amount": 0,
  "notes": null,
  "delivery_data": {
    "address": {
      "full_address": "123 Main St, City",
      "lat": 25.2048,
      "lng": 55.2708
    },
    "contact_info": {
      "name": "Delivery Customer",
      "phone": "+971500000000"
    }
  }
}
```

## POST /restaurant/locations/:locationId/orders — Create Order from Cart (Delivery - Online Gateway)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#post-create-order-from-cart-delivery-online-gateway)

Submit a delivery cart using an online payment method and a payment return reference.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/orders

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| cart_id | string |  |  |  |
| order_type | string |  |  |  |
| payment_method_id | string |  |  |  |
| payment_back_ref | string |  |  |  |
| tip_amount | number |  |  |  |
| notes | null |  |  |  |
| delivery_data | object |  |  |  |
| delivery_data.address | object |  |  |  |
| delivery_data.address.full_address | string |  |  |  |
| delivery_data.address.lat | number |  |  |  |
| delivery_data.address.lng | number |  |  |  |
| delivery_data.contact_info | object |  |  |  |
| delivery_data.contact_info.name | string |  |  |  |
| delivery_data.contact_info.phone | string |  |  |  |



### Request body (raw)

```json
{
  "cart_id": "{{cart_id}}",
  "order_type": "delivery",
  "payment_method_id": "{{online_payment_method_id}}",
  "payment_back_ref": "https://yourapp.com/payments/return",
  "tip_amount": 0,
  "notes": null,
  "delivery_data": {
    "address": {
      "full_address": "123 Main St, City",
      "lat": 25.2048,
      "lng": 55.2708
    },
    "contact_info": {
      "name": "Delivery Customer",
      "phone": "+971500000000"
    }
  }
}
```

## GET /restaurant/orders — List Orders (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#get-list-orders-admin)

Browse a restaurant location’s orders by status group and date range.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/orders?location_id={{location_id}}&status_group=completed&from_date=&to_date=&page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | query string |  |  | {{location_id}} |
| status_group | query string |  | active\|completed. completed => served, delivered, closed | completed |
| from_date | query string |  | Date-only (YYYY-MM-DD). Inclusive day range. Filters by restaurant_orders.created_at |  |
| to_date | query string |  | Date-only (YYYY-MM-DD). Inclusive day range. Filters by restaurant_orders.created_at |  |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |



## GET /restaurant/orders/:orderId — Get Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#get-get-order-2)

Read one restaurant order for order-detail or operational views.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/orders/:orderId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| orderId | path string | Yes | Dynamic path value from the request URL. |  |



## POST /restaurant/orders/:orderId/status — Update Order Status (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#post-update-order-status-admin)

Change a restaurant order’s status and optionally record a note about the change.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/orders/:orderId/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| orderId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| note | string |  |  |  |



### Request body (raw)

```json
{
  "status": "preparing",
  "note": "Start cooking"
}
```

## POST /restaurant/orders/:orderId/print — Print Order (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#post-print-order-admin)

Request the order print acknowledgement. The current handler returns the order ID and printed flag; it does not dispatch a printer job.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/orders/:orderId/print

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| orderId | path string | Yes | Dynamic path value from the request URL. |  |



## POST /public/restaurant/:slug/cart/attach — Attach Cart (Public) - requires login

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#post-attach-cart-public-requires-login)

Associate an existing restaurant cart with the signed-in customer after login.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/cart/attach

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| cart_id | string |  |  |  |



### Request body (raw)

```json
{
  "cart_id": "{{cart_id}}"
}
```

## GET /restaurant/:slug/payment-methods — List Payment Methods (Tenant Admin, slug-based)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#get-list-payment-methods-tenant-admin-slug-based)

Read the payment methods configured for the restaurant identified by its slug.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/:slug/payment-methods

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{admin_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



## PUT /restaurant/:slug/payment-methods — Upsert Payment Method (Tenant/Slug)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/orders-admin-public#put-upsert-payment-method-tenant-slug)

Create or update a restaurant payment method and its active state.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/:slug/payment-methods

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{admin_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| method_type | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "method_type": "online",
  "is_active": true
}
```

## POST /restaurant/locations/:locationId/tables — Create Table

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/tables#post-create-table)

Add a table to a restaurant location with its code, section and seating capacity.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/tables

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| code | string |  |  |  |
| name | object |  |  |  |
| name.en | string |  |  |  |
| section | string |  |  |  |
| seating_capacity | number |  |  |  |
| meta | object |  |  |  |
| meta.position | object |  |  |  |
| meta.position.x | number |  |  |  |
| meta.position.y | number |  |  |  |
| meta.position.rotation | number |  |  |  |
| meta.attributes | object |  |  |  |
| meta.attributes.shape | string |  |  |  |
| meta.attributes.tags | array<string> |  |  |  |
| active | boolean |  |  |  |



### Request body (raw)

```json
{
  "code": "A05",
  "name": {
    "en": "Table A05"
  },
  "section": "Patio",
  "seating_capacity": 4,
  "meta": {
    "position": {
      "x": 12,
      "y": 8,
      "rotation": 0
    },
    "attributes": {
      "shape": "round",
      "tags": [
        "window"
      ]
    }
  },
  "active": true
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{tableId}}",
    "location_id": "{{locationId}}",
    "code": "A05",
    "name": {
      "en": "Table A05"
    },
    "section": "Patio",
    "seating_capacity": 4,
    "meta": {
      "position": {
        "x": 12,
        "y": 8,
        "rotation": 0
      }
    },
    "active": true
  }
}
```

## GET /restaurant/locations/:locationId/tables — List Tables

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/tables#get-list-tables)

Browse a location’s tables, optionally filtering by section and seating capacity.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/tables?page=1&limit=10&section=Patio&min_seating=2&max_seating=6

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |
| section | query string |  |  | Patio |
| min_seating | query string |  |  | 2 |
| max_seating | query string |  |  | 6 |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{tableId}}",
      "code": "A05",
      "section": "Patio",
      "seating_capacity": 4,
      "qr_label_url": "https://cdn.example.com/documents/restaurant/table-qr/{{tableId}}.pdf"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

## GET /restaurant/tables/:tableId — Get Table

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/tables#get-get-table)

Read one restaurant table’s configuration.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/tables/:tableId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tableId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{tableId}}",
    "code": "A05",
    "name": {
      "en": "Table A05"
    },
    "section": "Patio",
    "seating_capacity": 4,
    "qr_label_url": "https://cdn.example.com/documents/restaurant/table-qr/{{tableId}}.pdf"
  }
}
```

## PUT /restaurant/tables/:tableId — Update Table

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/tables#put-update-table)

Change a table’s name, section, seating capacity or active state.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/tables/:tableId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tableId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| section | string |  |  |  |
| seating_capacity | number |  |  |  |
| meta | object |  |  |  |
| meta.position | object |  |  |  |
| meta.position.x | number |  |  |  |
| meta.position.y | number |  |  |  |
| meta.position.rotation | number |  |  |  |
| meta.attributes | object |  |  |  |
| meta.attributes.shape | string |  |  |  |
| meta.attributes.tags | array<string> |  |  |  |
| active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Table A05 - Window"
  },
  "section": "Patio",
  "seating_capacity": 6,
  "meta": {
    "position": {
      "x": 14,
      "y": 9,
      "rotation": 0
    },
    "attributes": {
      "shape": "round",
      "tags": [
        "window",
        "vip"
      ]
    }
  },
  "active": true
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{tableId}}",
    "name": {
      "en": "Table A05 - Window"
    },
    "section": "Patio",
    "seating_capacity": 6,
    "active": true
  }
}
```

## DELETE /restaurant/tables/:tableId — Delete Table

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/tables#delete-delete-table)

Remove a table from the restaurant location’s configuration.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/tables/:tableId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tableId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{tableId}}"
  }
}
```

## GET /restaurant/locations/:locationId/sections — List Sections

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/tables#get-list-sections)

Browse the named seating sections of a restaurant location.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/sections?page=1&limit=10&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |
| search | query string |  |  |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "name": "Patio",
      "tenant_id": "..."
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

## POST /restaurant/locations/:locationId/products — Create Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#post-create-product)

Add a menu product to a restaurant location with pricing, categories and menu assignments.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/products

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| category_ids | array<string> |  |  |  |
| name | object |  |  |  |
| name.en | string |  |  |  |
| description | object |  |  |  |
| price | number |  |  |  |
| has_variant | boolean |  |  |  |
| sku | null |  |  |  |
| ingredients | array<string> |  |  |  |
| is_active | boolean |  |  |  |
| tags | array<string> |  |  |  |
| images | array |  |  |  |
| menu_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "category_ids": [
    "{{categoryId1}}",
    "{{categoryId2}}"
  ],
  "name": {
    "en": "Cheeseburger"
  },
  "description": {},
  "price": 9.99,
  "has_variant": false,
  "sku": null,
  "ingredients": [
    "beef",
    "cheese"
  ],
  "is_active": true,
  "tags": [
    "popular"
  ],
  "images": [],
  "menu_ids": [
    "{{menuId1}}",
    "{{menuId2}}"
  ]
}
```

## PUT /restaurant/products/:id — Update Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#put-update-product)

Change a restaurant product’s catalog details, pricing, categories or menu assignments.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/products/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| category_ids | array<string> |  |  |  |
| name | object |  |  |  |
| name.en | string |  |  |  |
| description | object |  |  |  |
| price | number |  |  |  |
| has_variant | boolean |  |  |  |
| sku | null |  |  |  |
| ingredients | array<string> |  |  |  |
| is_active | boolean |  |  |  |
| tags | array<string> |  |  |  |
| images | array |  |  |  |
| menu_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "category_ids": [
    "{{categoryId1}}",
    "{{categoryId3}}"
  ],
  "name": {
    "en": "Cheeseburger Deluxe"
  },
  "description": {},
  "price": 10.99,
  "has_variant": false,
  "sku": null,
  "ingredients": [
    "beef",
    "cheese"
  ],
  "is_active": true,
  "tags": [
    "popular"
  ],
  "images": [],
  "menu_ids": [
    "{{menuId1}}",
    "{{menuId3}}"
  ]
}
```

## DELETE /restaurant/products/:id — Delete Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#delete-delete-product)

Remove a product from the restaurant catalog.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/products/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



## GET /restaurant/locations/:locationId/products — List Products by Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#get-list-products-by-location)

Browse a location’s products using category, menu, availability and search filters.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/products?category_ids={{categoryId1}},{{categoryId2}}&menu_ids={{menuId1}}&is_active=true&search={{search}}&lang=en&sort=created_at&order=desc&page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| category_ids | query string |  |  | {{categoryId1}},{{categoryId2}} |
| menu_ids | query string |  |  | {{menuId1}} |
| modifier_group_ids | query string |  |  | {{modifierGroupId}} |
| is_active | query string |  |  | true |
| search | query string |  |  | {{search}} |
| lang | query string |  |  | en |
| sort | query string |  |  | created_at |
| order | query string |  |  | desc |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |



## GET /restaurant/products/:id — Get Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#get-get-product)

Read the details of one restaurant product for its product or editing view.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/products/:id?location_id={{location_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | query string |  |  | {{location_id}} |



## POST /restaurant/products/:productId/variants — Create Variant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#post-create-variant)

Add a separately priced variant to a restaurant product, such as a size or portion.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/variants

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| description | object |  |  |  |
| price | number |  |  |  |
| sku | null |  |  |  |
| ingredients | array<string> |  |  |  |
| is_active | boolean |  |  |  |
| is_primary | boolean |  |  |  |
| images | array |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Regular"
  },
  "description": {},
  "price": 8.99,
  "sku": null,
  "ingredients": [
    "beef",
    "cheese"
  ],
  "is_active": true,
  "is_primary": true,
  "images": []
}
```

## PUT /restaurant/products/:productId/variants/:variantId — Update Variant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#put-update-variant)

Change a product variant’s name, price, ingredients or availability.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/variants/:variantId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |
| variantId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| description | object |  |  |  |
| price | number |  |  |  |
| sku | null |  |  |  |
| ingredients | array<string> |  |  |  |
| is_active | boolean |  |  |  |
| is_primary | boolean |  |  |  |
| images | array |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Large"
  },
  "description": {},
  "price": 10.99,
  "sku": null,
  "ingredients": [
    "beef",
    "cheese"
  ],
  "is_active": true,
  "is_primary": false,
  "images": []
}
```

## DELETE /restaurant/products/:productId/variants/:variantId — Delete Variant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#delete-delete-variant)

Remove a variant from its restaurant product.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/variants/:variantId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |
| variantId | path string | Yes | Dynamic path value from the request URL. |  |



## POST /restaurant/products/:productId/variants/bulk — Create Variants (Bulk)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#post-create-variants-bulk)

Add several variants to the same restaurant product in one request.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/variants/bulk

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| description | object |  |  |  |
| price | number |  |  |  |
| sku | null |  |  |  |
| ingredients | array<string> |  |  |  |
| is_active | boolean |  |  |  |
| is_primary | boolean |  |  |  |
| images | array |  |  |  |



### Request body (raw)

```json
[
  {
    "name": {
      "en": "Regular"
    },
    "description": {},
    "price": 8.99,
    "sku": null,
    "ingredients": [
      "beef",
      "cheese"
    ],
    "is_active": true,
    "is_primary": true,
    "images": []
  },
  {
    "name": {
      "en": "Large"
    },
    "description": {},
    "price": 10.99,
    "sku": null,
    "ingredients": [
      "beef",
      "cheese"
    ],
    "is_active": true,
    "is_primary": false,
    "images": []
  }
]
```

## GET /restaurant/products/:productId/variants — List Variants

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#get-list-variants)

Browse a restaurant product’s variants, with availability, sorting and pagination options.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/variants?is_active=true&sort=created_at&order=desc&page=1&limit=25

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_active | query string |  |  | true |
| sort | query string |  |  | created_at |
| order | query string |  |  | desc |
| page | query string |  |  | 1 |
| limit | query string |  |  | 25 |



## POST /restaurant/products/:productId/menus — Link Product to Menus

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#post-link-product-to-menus)

Links a product to multiple menus (idempotent).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/menus

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menu_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "menu_ids": [
    "{{menuId1}}",
    "{{menuId2}}"
  ]
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "product_id": "{{productId}}",
      "menu_id": "{{menuId1}}"
    },
    {
      "product_id": "{{productId}}",
      "menu_id": "{{menuId2}}"
    }
  ]
}
```

## POST /restaurant/products/:productId/menus/remove — Unlink Product from Menus

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/products#post-unlink-product-from-menus)

Unlinks a product from multiple menus.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/products/:productId/menus/remove

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menu_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "menu_ids": [
    "{{menuId1}}",
    "{{menuId2}}"
  ]
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "count": 2
  }
}
```

## POST /restaurant/locations/:locationId/categories — Create Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/categories#post-create-category)

Add a product category to a restaurant location, optionally beneath a parent category.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/categories

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| parent_id | null |  |  |  |
| name | object |  |  |  |
| name.en | string |  |  |  |
| description | object |  |  |  |
| is_active | boolean |  |  |  |
| sort_order | number |  |  |  |
| category_image | string |  |  |  |



### Request body (raw)

```json
{
  "parent_id": null,
  "name": {
    "en": "Burgers"
  },
  "description": {},
  "is_active": true,
  "sort_order": 0,
  "category_image": "https://example.com/cat.jpg"
}
```

## PUT /restaurant/categories/:id — Update Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/categories#put-update-category)

Change a restaurant category’s name, parent, display order, image or active state.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/categories/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| parent_id | null |  |  |  |
| name | object |  |  |  |
| name.en | string |  |  |  |
| description | object |  |  |  |
| is_active | boolean |  |  |  |
| sort_order | number |  |  |  |
| category_image | string |  |  |  |



### Request body (raw)

```json
{
  "parent_id": null,
  "name": {
    "en": "Burgers & Sandwiches"
  },
  "description": {},
  "is_active": true,
  "sort_order": 10,
  "category_image": "https://example.com/cat-new.jpg"
}
```

## GET /restaurant/locations/:locationId/categories — List Categories (with filters)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/categories#get-list-categories-with-filters)

Browse a location’s product categories using parent, availability and search filters.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/categories?root_category=true&parent_id={{parent_id}}&is_active={{is_active}}&search={{search}}&includeAvailableCounts=true&page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| root_category | query string |  | Set 'true' to fetch only root categories (ignores parent_id if true). | true |
| parent_id | query string |  | Use 'null' (string) to filter root categories (ignored if root_category=true) | {{parent_id}} |
| is_active | query string |  |  | {{is_active}} |
| search | query string |  |  | {{search}} |
| includeAvailableCounts | query string |  | Include count of currently available products per category (uses availability view) | true |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |



## GET /restaurant/categories/:id — Get Category (with count)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/categories#get-get-category-with-count)

Read a restaurant category with its product count.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/categories/:id?active_only=true

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| active_only | query string |  |  | true |



## DELETE /restaurant/categories/:id — Delete Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/categories#delete-delete-category)

Remove a category from the restaurant’s catalog organization.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/categories/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Dynamic path value from the request URL. |  |



## POST /restaurant/locations/:locationId/staff — Add Staff (create + link)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/staff#post-add-staff-create-link)

Creates an auth user (email auto-confirmed), links to location as staff with staff_role (cashier|kitchen_staff), assigns tenant role 'restaurant_staff', and enqueues a credential email containing the restaurant code and 6-digit staff PIN. Password must be exactly 6 numeric digits.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/staff

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| email | string |  |  |  |
| password | string |  |  |  |
| full_name | string |  |  |  |
| staff_role | string |  |  |  |
| avatar_url | string |  |  |  |



### Request body (raw)

```json
{
  "email": "staff@example.com",
  "password": "123456",
  "full_name": "John Doe",
  "staff_role": "cashier",
  "avatar_url": "https://example.com/avatar.jpg"
}
```

## PUT /restaurant/staff/:staffId — Update Staff (name/avatar/password)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/staff#put-update-staff-name-avatar-password)

Updates staff user metadata and/or password. Email cannot be updated. Password must be exactly 6 numeric digits.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/staff/:staffId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| staffId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| full_name | string |  |  |  |
| avatar_url | string |  |  |  |
| password | string |  |  |  |



### Request body (raw)

```json
{
  "full_name": "John D. Doe",
  "avatar_url": "https://example.com/new-avatar.jpg",
  "password": "654321"
}
```

## GET /restaurant/locations/:locationId/staff — List Staff (by Location)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/staff#get-list-staff-by-location)

Lists staff with pagination and search. Response includes page, limit, total.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/staff?search={{search}}&sort=created_at&order=desc&page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| search | query string |  | Filter by user first_name/last_name/email | {{search}} |
| sort | query string |  | Sort field: created_at\|updated_at | created_at |
| order | query string |  | asc\|desc | desc |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |



## GET /restaurant/staff/:staffId — Get Staff (by ID)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/staff#get-get-staff-by-id)

Fetches a single staff member (tenant-scoped).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/staff/:staffId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| staffId | path string | Yes | Dynamic path value from the request URL. |  |



## GET /restaurant/locations/:locationId/menus — List Menus

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#get-list-menus)

Lists menus for a location with pagination and optional search. Includes is_available computed field.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/menus?page=1&limit=20&search=&lang=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| search | query string |  |  |  |
| lang | query string |  |  | en |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{menuId}}",
      "name": {
        "en": "Lunch"
      },
      "activation_mode": "automatic",
      "manual_is_active": false,
      "is_published": true,
      "is_available": true
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1
}
```

## GET /restaurant/menus/:menuId — Get Menu

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#get-get-menu)

Get a menu by ID. Includes is_available.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{menuId}}",
    "location_id": "{{locationId}}",
    "name": {
      "en": "Lunch"
    },
    "activation_mode": "automatic",
    "manual_is_active": false,
    "is_published": true,
    "is_available": true
  }
}
```

## POST /restaurant/locations/:locationId/menus — Create Menu

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#post-create-menu)

Create a new menu. activation_mode can be 'automatic' or 'manual'.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/menus

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| image | string |  |  |  |
| activation_mode | string |  |  |  |
| manual_is_active | boolean |  |  |  |
| is_published | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Lunch"
  },
  "description": {
    "en": "Lunch Menu"
  },
  "image": "https://example.com/menu.jpg",
  "activation_mode": "automatic",
  "manual_is_active": false,
  "is_published": true
}
```

### Response 201 201 Created

Created

```json
{ "success": true, "data": { "id": "{{menuId}}", ... } }
```

## PUT /restaurant/menus/:menuId — Update Menu

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#put-update-menu)

Update menu details.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| is_published | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Lunch Special"
  },
  "is_published": true
}
```

### Response 200 200 OK

OK

```json
{ "success": true, "data": { ... } }
```

## DELETE /restaurant/menus/:menuId — Delete Menu

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#delete-delete-menu)

Soft delete a menu.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{menuId}}",
    "deleted_at": "..."
  }
}
```

## POST /restaurant/menus/:menuId/products — Link Products to Menu

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#post-link-products-to-menu)

Link multiple products to a menu. Idempotent upsert.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/products

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| product_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "product_ids": [
    "{{productId1}}",
    "{{productId2}}"
  ]
}
```

### Response 200 200 OK

OK

```json
{ "success": true, "data": [ ... ] }
```

## GET /restaurant/menus/:menuId/products — List Products in Menu

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#get-list-products-in-menu)

List products linked to a specific menu.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/products?page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |



### Response 200 200 OK

OK

```json
{ "success": true, "data": [ ... ], "page": 1, "limit": 20, "total": 5 }
```

## POST /restaurant/menus/:menuId/pause — Pause Menu

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#post-pause-menu)

Temporarily pause the menu (deactivate current time window). Requires automatic schedule mode.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/pause

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "message": "Menu paused successfully"
}
```

## POST /restaurant/menus/:menuId/resume — Resume Menu

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#post-resume-menu)

Resume the menu (reactivate current time window) if manually paused.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/resume

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "message": "Menu resumed successfully"
}
```

## POST /restaurant/menus/:menuId/state — Set Menu State

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/menus#post-set-menu-state-2)

Set a menu to automatic (schedule-driven) or manual (manual_is_active=true/false). Bumps location availability_version by trigger.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/state

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| activation_mode | string |  |  |  |
| manual_is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "activation_mode": "manual",
  "manual_is_active": true
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{menuId}}",
    "activation_mode": "manual",
    "manual_is_active": true
  }
}
```

## GET /restaurant/locations/:locationId/schedules — List Location Schedules

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#get-list-location-schedules)

Returns all schedule windows for a location (weekly recurring windows with optional date ranges).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/schedules

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{scheduleId}}",
      "location_id": "{{locationId}}",
      "day_of_week": 1,
      "open_time": "08:00:00",
      "close_time": "12:00:00",
      "start_date": null,
      "end_date": null,
      "active": true
    }
  ]
}
```

## GET /restaurant/locations/:locationId/schedule-exceptions — List Location Schedule Exceptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#get-list-location-schedule-exceptions)

Returns all schedule exceptions for a location.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/schedule-exceptions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{exceptionId}}",
      "schedule_id": "{{scheduleId}}",
      "date": "2025-12-25",
      "closed": true,
      "reason": "Holiday",
      "schedule": {
        "location_id": "{{locationId}}",
        "day_of_week": 1,
        "open_time": "08:00:00",
        "close_time": "12:00:00"
      }
    }
  ]
}
```

## POST /restaurant/locations/:locationId/schedules — Set Location Schedules (replace)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#post-set-location-schedules-replace)

Replaces all existing schedules for the location with the provided set. Triggers bump availability_version.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/schedules

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| schedules | array<object> |  |  |  |
| schedules.day_of_week | number |  |  |  |
| schedules.start_time | string |  |  |  |
| schedules.end_time | string |  |  |  |
| schedules.start_date | null |  |  |  |
| schedules.end_date | null |  |  |  |
| schedules.active | boolean |  |  |  |



### Request body (raw)

```json
{
  "schedules": [
    {
      "day_of_week": 1,
      "start_time": "08:00:00",
      "end_time": "12:00:00",
      "start_date": null,
      "end_date": null,
      "active": true
    },
    {
      "day_of_week": 1,
      "start_time": "18:00:00",
      "end_time": "22:00:00",
      "active": true
    },
    {
      "day_of_week": 2,
      "start_time": "08:00:00",
      "end_time": "12:00:00"
    }
  ]
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{scheduleId}}",
      "day_of_week": 1,
      "open_time": "08:00:00",
      "close_time": "12:00:00"
    }
  ]
}
```

## POST /restaurant/locations/:locationId/schedule-exceptions — Add Location Schedule Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#post-add-location-schedule-exception)

Adds an exception for a specific schedule window on a given date. Either provide schedule_id or the (day_of_week,start_time,end_time) signature.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/locations/:locationId/schedule-exceptions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| date | string |  |  |  |
| closed | boolean |  |  |  |
| reason | string |  |  |  |
| day_of_week | number |  |  |  |
| start_time | string |  |  |  |
| end_time | string |  |  |  |



### Request body (raw)

```json
{
  "date": "2025-12-25",
  "closed": true,
  "reason": "Christmas Day",
  "day_of_week": 1,
  "start_time": "08:00:00",
  "end_time": "12:00:00"
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{exceptionId}}",
    "schedule_id": "{{scheduleId}}",
    "date": "2025-12-25",
    "closed": true,
    "reason": "Christmas Day"
  }
}
```

## DELETE /restaurant/schedule-exceptions/:exceptionId — Delete Location Schedule Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#delete-delete-location-schedule-exception)

Deletes a location schedule exception by id.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/schedule-exceptions/:exceptionId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| exceptionId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{exceptionId}}"
  }
}
```

## GET /restaurant/menus/:menuId/schedules — List Menu Schedules

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#get-list-menu-schedules)

Returns menu schedule windows (weekly recurring, optional date range).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/schedules

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{menuScheduleId}}",
      "menu_id": "{{menuId}}",
      "day_of_week": 1,
      "start_time": "12:00:00",
      "end_time": "16:00:00",
      "active": true
    }
  ]
}
```

## GET /restaurant/menus/:menuId/schedule-exceptions — List Menu Schedule Exceptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#get-list-menu-schedule-exceptions)

Returns all schedule exceptions for a menu.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/schedule-exceptions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{exceptionId}}",
      "menu_schedule_id": "{{menuScheduleId}}",
      "date": "2025-12-25",
      "closed": true,
      "reason": "Holiday",
      "schedule": {
        "menu_id": "{{menuId}}",
        "day_of_week": 1,
        "start_time": "12:00:00",
        "end_time": "16:00:00"
      }
    }
  ]
}
```

## POST /restaurant/menus/:menuId/schedules — Set Menu Schedules (replace)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#post-set-menu-schedules-replace)

Replaces all existing schedules for the menu with the provided set. Triggers bump availability_version via menu-based trigger.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/schedules

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| schedules | array<object> |  |  |  |
| schedules.day_of_week | number |  |  |  |
| schedules.start_time | string |  |  |  |
| schedules.end_time | string |  |  |  |
| schedules.active | boolean |  |  |  |



### Request body (raw)

```json
{
  "schedules": [
    {
      "day_of_week": 1,
      "start_time": "12:00:00",
      "end_time": "16:00:00",
      "active": true
    },
    {
      "day_of_week": 1,
      "start_time": "18:00:00",
      "end_time": "22:00:00",
      "active": true
    }
  ]
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": [
    {
      "id": "{{menuScheduleId}}",
      "day_of_week": 1,
      "start_time": "12:00:00",
      "end_time": "16:00:00"
    }
  ]
}
```

## POST /restaurant/menus/:menuId/schedule-exceptions — Add Menu Schedule Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#post-add-menu-schedule-exception)

Adds an exception for a specific menu schedule window on a given date. Either provide menu_schedule_id or the (day_of_week,start_time,end_time) signature.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/menus/:menuId/schedule-exceptions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| menuId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| date | string |  |  |  |
| closed | boolean |  |  |  |
| reason | string |  |  |  |
| day_of_week | number |  |  |  |
| start_time | string |  |  |  |
| end_time | string |  |  |  |



### Request body (raw)

```json
{
  "date": "2025-12-25",
  "closed": true,
  "reason": "Christmas Lunch",
  "day_of_week": 1,
  "start_time": "12:00:00",
  "end_time": "16:00:00"
}
```

### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{menuExceptionId}}",
    "menu_schedule_id": "{{menuScheduleId}}",
    "date": "2025-12-25",
    "closed": true,
    "reason": "Christmas Lunch"
  }
}
```

## DELETE /restaurant/menu-schedule-exceptions/:exceptionId — Delete Menu Schedule Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedules#delete-delete-menu-schedule-exception)

Deletes a menu schedule exception by id.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/menu-schedule-exceptions/:exceptionId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| exceptionId | path string | Yes | Dynamic path value from the request URL. |  |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "data": {
    "id": "{{menuExceptionId}}"
  }
}
```

## POST /restaurant/sessions — Create Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/sessions#post-create-session)

Open a dining session for a restaurant table. A conflicting open session on the same table is rejected.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/sessions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| table_id | string |  |  |  |
| slug | string |  |  |  |
| started_at | string |  |  |  |
| closed_at | string |  |  |  |
| note | string |  |  |  |



### Request body (raw)

```json
{
  "table_id": "{{table_id}}",
  "slug": "{{slug}}",
  "started_at": "2024-01-01T12:00:00Z",
  "closed_at": "2024-01-01T14:00:00Z",
  "note": "Optional note about this session"
}
```

## GET /restaurant/sessions — List Sessions By User

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/sessions#get-list-sessions-by-user)

Browse a customer’s dining sessions, optionally limited to a date range or time filter.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/sessions?user_id={{user_id}}&time_filter=all&sort=desc&page=1&limit=20&from_date={{from_date}}&to_date={{to_date}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_id | query string |  | Customer user ID. If omitted, falls back to authenticated user. | {{user_id}} |
| time_filter | query string |  | Allowed: all (default), past, future. | all |
| sort | query string |  | For all: created_at asc\|desc (default desc). For past: started_at latest first. For future: started_at soonest first. | desc |
| page | query string |  | Page number (1-based). | 1 |
| limit | query string |  | Items per page (max 100). | 20 |
| from_date | query string | No | Filter sessions by started_at >= from_date (ISO datetime). Optional. | {{from_date}} |
| to_date | query string | No | Filter sessions by started_at <= to_date (ISO datetime). Optional. | {{to_date}} |



## GET /restaurant/locations/{{location_id}}/sessions — List Sessions By Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/sessions#get-list-sessions-by-location)

Browse dining sessions at a restaurant location for operational review.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/locations/{{location_id}}/sessions?time_filter=all&sort=desc&page=1&limit=20&from_date={{from_date}}&to_date={{to_date}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| time_filter | query string |  | Allowed: all (default), past, future. | all |
| sort | query string |  | For all: created_at asc\|desc (default desc). For past: started_at latest first. For future: started_at soonest first. | desc |
| page | query string |  | Page number (1-based). | 1 |
| limit | query string |  | Items per page (max 100). | 20 |
| from_date | query string | No | Filter sessions by started_at >= from_date (ISO datetime). Optional. | {{from_date}} |
| to_date | query string | No | Filter sessions by started_at <= to_date (ISO datetime). Optional. | {{to_date}} |



## PUT /restaurant/sessions/:sessionId/close — Close Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/sessions#put-close-session)

Close a dine-in session and record its closing time.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/sessions/:sessionId/close

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sessionId | path string | Yes | Dynamic path value from the request URL. |  |



## GET /restaurant/:slug/cart — Get Cart

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cart#get-get-cart)

Get the authenticated user's active cart for the specified restaurant slug (location). If no active cart exists for this user and location, a new empty cart is created and returned.

### Responses
- 200: { success: true, data: Cart }
- 401: { success: false, message: 'login_required' }
- 404: { success: false, message: 'restaurant_not_found' }

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/:slug/cart

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{token}} | Bearer {{token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | restaurant location slug |  |



## POST /restaurant/:slug/cart/items — Add Cart Item

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cart#post-add-cart-item)

Add a product variant to the authenticated user's cart for the given restaurant slug. If the cart does not exist, it is auto-created.

### Responses
- 200: { success: true, data: Cart }
- 400: { success: false, message }
- 401: { success: false, message: 'login_required' }
- 404: { success: false, message: 'restaurant_not_found' }

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/:slug/cart/items

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{token}} | Bearer {{token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | restaurant location slug |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| product_id | string | Yes | string (required) |  |
| variant_id | string | Yes | string (required) |  |
| quantity | number | Yes | number >= 1 (required) |  |
| note | null | No | string (optional) |  |
| modifiers | array | No | Array<{ modifier_option_id: string; quantity?: number }> (optional) |  |



### Request body (raw)

```json
{
  "product_id": "{{product_id}}",
  "variant_id": "{{variant_id}}",
  "quantity": 1,
  "note": null,
  "modifiers": []
}
```

## PUT /restaurant/:slug/cart/items/:itemId — Update Cart Item

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cart#put-update-cart-item)

Update a cart item's quantity and/or note. If quantity is 0, the item is removed.

### Responses
- 200: { success: true, data: Cart }
- 400: { success: false, message }
- 401: { success: false, message: 'login_required' }
- 404: { success: false, message: 'restaurant_not_found' }

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/:slug/cart/items/:itemId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{token}} | Bearer {{token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | restaurant location slug |  |
| itemId | path string | Yes | cart item id |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| quantity | number |  | ?: number >= 0 |  |
| note | null |  | ?: string |  |



### Request body (raw)

```json
{
  "quantity": 2,
  "note": null
}
```

## PUT /restaurant/:slug/cart/items/:itemId/modifiers — Update Cart Item Modifiers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cart#put-update-cart-item-modifiers)

Replace modifiers for a cart item. This operation is idempotent: it deletes existing modifiers and inserts the provided set.

### Responses
- 200: { success: true, data: Cart }
- 400: { success: false, message }
- 401: { success: false, message: 'login_required' }
- 404: { success: false, message: 'restaurant_not_found' }

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/:slug/cart/items/:itemId/modifiers

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{token}} | Bearer {{token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | restaurant location slug |  |
| itemId | path string | Yes | cart item id |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| modifiers | array<object> |  | Array<{ modifier_option_id: string; quantity?: number; price_delta_snapshot?: number }> |  |
| modifiers.modifier_option_id | string |  |  |  |
| modifiers.quantity | number |  |  |  |
| modifiers.price_delta_snapshot | number |  |  |  |



### Request body (raw)

```json
{
  "modifiers": [
    {
      "modifier_option_id": "{{modifier_option_id}}",
      "quantity": 1,
      "price_delta_snapshot": 0
    }
  ]
}
```

## DELETE /restaurant/:slug/cart — Clear Cart

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cart#delete-clear-cart)

Clear all items from the authenticated user's cart for the specified restaurant slug.

### Responses
- 200: { success: true, data: Cart }
- 401: { success: false, message: 'login_required' }
- 404: { success: false, message: 'restaurant_not_found' }

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/restaurant/:slug/cart

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{token}} | Bearer {{token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | restaurant location slug |  |



## GET /restaurant/:slug/cart/availability — Get Cart Availability

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/cart#get-get-cart-availability)

Check real-time availability of items currently in the cart. Returns a list of items with their availability status (based on product active status and schedule).

### Responses
- 200: { success: true, data: [{ cart_item_id, product_id, variant_id, is_available }, ...] }
- 401: { success: false, message: 'login_required' }
- 404: { success: false, message: 'restaurant_not_found' }

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/:slug/cart/availability

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{token}} | Bearer {{token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | restaurant location slug |  |



## GET /restaurant/delivery-integration — Get Delivery Integration Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/delivery-integration#get-get-delivery-integration-settings)

Read how the restaurant connects to its delivery service.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/delivery-integration

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## PUT /restaurant/delivery-integration — Upsert Delivery Integration Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/delivery-integration#put-upsert-delivery-integration-settings)

Save delivery integration mode, connection settings, retry behavior and default service.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/restaurant/delivery-integration

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| mode | string |  |  |  |
| status | string |  |  |  |
| external_base_url | string |  |  |  |
| external_api_key | string |  |  |  |
| webhook_secret | null |  |  |  |
| timeout_ms | number |  |  |  |
| retries | number |  |  |  |
| default_delivery_service_id | string |  |  |  |



### Request body (raw)

```json
{
  "mode": "external",
  "status": "draft",
  "external_base_url": "https://external-tenant-edge-url",
  "external_api_key": "od_xxx",
  "webhook_secret": null,
  "timeout_ms": 15000,
  "retries": 3,
  "default_delivery_service_id": "{{delivery_service_id}}"
}
```

## POST /restaurant/delivery-integration/test — Test & Activate External Delivery Integration

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/delivery-integration#post-test-and-activate-external-delivery-integration)

Check the supplied external delivery connection and activate a successful integration.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/delivery-integration/test

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| mode | string |  |  |  |
| external_base_url | string |  |  |  |
| external_api_key | string |  |  |  |
| default_delivery_service_id | string |  |  |  |
| timeout_ms | number |  |  |  |
| retries | number |  |  |  |



### Request body (raw)

```json
{
  "mode": "external",
  "external_base_url": "https://external-tenant-edge-url",
  "external_api_key": "od_xxx",
  "default_delivery_service_id": "{{delivery_service_id}}",
  "timeout_ms": 15000,
  "retries": 3
}
```

## GET /restaurant/delivery-integration/services — List External Delivery Services (via Restaurant)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/delivery-integration#get-list-external-delivery-services-via-restaurant)

Browse delivery services exposed through the restaurant’s external integration.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/delivery-integration/services?delivery_type_code=ondemand&active=true

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_type_code | query string |  |  | ondemand |
| active | query string |  |  | true |



## POST /scheduler-maintenance — Run Schedule Maintenance (Internal)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/restaurant-menu/schedule-maintenance-internal#post-run-schedule-maintenance-internal)

Regenerates restaurant availability windows. Optional body fields: `location_id` regenerates one location; otherwise `menu_id` regenerates one menu; an empty body runs full maintenance for active locations and published automatic menus. If both IDs are supplied, `location_id` takes precedence. Omit both IDs only when full maintenance is intended.

Internal backend/cron only. Send `apikey: {{supabase_secret_key}}` with the default managed Supabase secret key (`SUPABASE_SECRET_KEYS.default`). Never put this value in a frontend or mobile application. Missing, invalid, publishable, legacy anon, and legacy service-role bearer credentials return HTTP 401 before work is dispatched. `Authorization: Bearer ...` alone is not accepted. Frontend handoff: none; application clients must not call this internal endpoint.

Authentication: Internal backend or cron only. Send the managed Supabase secret in apikey. Application clients must not call this endpoint.

Request: POST https://staging-api.ondi.io/functions/v1/scheduler-maintenance

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| apikey | header string |  | Default managed Supabase secret key; backend only. | {{supabase_secret_key}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | string |  |  |  |



### Request body (raw)

```json
{
  "location_id": "{{restaurant_location_id}}"
}
```

### Response 200 200 Location regenerated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "mode": "single_location"
}
```

### Response 200 200 Menu regenerated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "mode": "single_menu"
}
```

### Response 200 200 Full maintenance completed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true
}
```

### Response 500 500 Maintenance failure

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Maintenance failed"
}
```

### Response 401 401 Missing or invalid internal secret

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Unauthorized"
}
```

## POST /storefront/products — Create Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/products#post-create-product-2)

Creates a new product with flexible variant option configuration.

The `has_variant` flag controls whether the product is modeled as:
- **Multi-variant product** (`has_variant: true`): product defines variant options; warehouse fields are optional at product level; variants handle their own warehouse linking/creation.
- **Simple single-variant product** (`has_variant: false`): backend will auto-create a single primary variant.

### Exactly one scenario must be provided
- **Scenario A (link existing):** `warehouse_item_id`
- **Scenario B (create new):** `warehouse_id` + `location_id` + `inventory_quantity` (and **no** `warehouse_item_id`)

### Rules
- Sending Scenario A and Scenario B fields together returns 400
- If `has_variant` is false and neither scenario is provided, returns 400
- Scenario B requires permission `manage:operations:tenant`

- **Required:** Valid Bearer token with tenant admin permissions

### Required Fields
- `name`: Multi-language product name object
- `description`: Multi-language product description object
- `price`: Product base price (required, >= 0). Used as fallback when variant has no price.

### Optional Fields (common)
- `category_ids`: Array of category IDs the product belongs to (product can be linked to multiple categories)
- `brand_id`: Brand ID for the product
- `variant_options`: Array of variant option type names that variants must provide values for (only meaningful when `has_variant` is true)
- `is_active`: Boolean to enable/disable the product (default: true)
- `tags`: Array of searchable tags
- `images`: Product-level image URLs used as fallback when variants have no images
- `media`: Optional rich media list. Each item: `{ "type": "image"|"video", "url": "...", "sortOrder": 0 }`. When provided, the legacy `images` array is auto-derived from image-type media sorted by sortOrder.
- `has_variant`: Boolean flag (default: false)

### The backend will automatically create a primary variant with
- `warehouse_item_id`: from Scenario A or Scenario B
- `name`: `variant_name` if provided, otherwise `name`
- `description`: `variant_description` if provided, otherwise `description`
- `images`: `variant_images` if provided, otherwise product `images`
- `attributes`: from `variant_attributes` if provided

### Variant Options System (for `has_variant: true`)
- **System Options:** colors, size, volume, model, weight, material, brand
- **Custom Options:** Tenant-specific options like 'ram', 'storage', 'warranty'
- **Validation:** All variants must provide values for selected options
- **Aggregation:** Available option values are automatically aggregated from variants

### Example Variant Options
- Electronics: ["colors", "size", "model", "ram", "storage"]
- Clothing: ["colors", "size", "material", "fit_type"]
- Food: ["flavor", "size", "packaging", "organic"]

### Features
- Multi-language support (en, ar, ku)
- Flexible variant option selection
- Automatic validation of variant attributes
- Option value aggregation for filtering
- System and custom option support

### Slug uniqueness (DB enforced)

`slug` must be unique per market (trimmed, case-insensitive). Duplicate slug returns **409 Conflict** with a localized message to choose a different slug. Variant slugs are unique per product the same way.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/products

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| description.ar | string |  |  |  |
| description.ku | string |  |  |  |
| slug | string |  |  |  |
| price | number |  |  |  |
| category_ids | array<string> |  |  |  |
| brand_id | string |  |  |  |
| variant_options | array<string> |  |  |  |
| is_active | boolean |  |  |  |
| tags | array<string> |  |  |  |
| has_variant | boolean |  |  |  |
| use_product_defaults | boolean |  |  |  |
| media | array<object> |  |  |  |
| media.type | string |  |  |  |
| media.url | string |  |  |  |
| media.sortOrder | number |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "MacBook Pro Laptop",
    "ar": "لابتوب ماك بوك برو",
    "ku": "لابتۆپی ماک بوک پرۆ"
  },
  "description": {
    "en": "High-performance laptop for professionals",
    "ar": "لابتوب عالي الأداء للمحترفين",
    "ku": "لابتۆپی بەرزی کارایی بۆ پیشەییەکان"
  },
  "slug": "macbook-pro-laptop",
  "price": 1999.99,
  "category_ids": [
    "electronics-category-id"
  ],
  "brand_id": "brand-uuid",
  "variant_options": [
    "colors",
    "size",
    "model",
    "ram"
  ],
  "is_active": true,
  "tags": [
    "laptop",
    "professional",
    "apple"
  ],
  "has_variant": true,
  "use_product_defaults": false,
  "media": [
    {
      "type": "image",
      "url": "https://cdn.example.com/products/macbook-pro/main.jpg",
      "sortOrder": 0
    },
    {
      "type": "video",
      "url": "https://cdn.example.com/products/macbook-pro/demo.mp4",
      "sortOrder": 1
    },
    {
      "type": "image",
      "url": "https://cdn.example.com/products/macbook-pro/alt-1.jpg",
      "sortOrder": 2
    }
  ]
}
```

### Response 201 Success - Product Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Product created successfully",
  "product": {
    "id": "product-uuid",
    "market_id": "market-uuid",
    "brand_id": "brand-uuid",
    "slug": "macbook-pro-laptop",
    "price": 1999.99,
    "name": {
      "en": "MacBook Pro Laptop",
      "ar": "لابتوب ماك بوك برو",
      "ku": "لابتۆپی ماک بوک پرۆ"
    },
    "description": {
      "en": "High-performance laptop for professionals",
      "ar": "لابتوب عالي الأداء للمحترفين",
      "ku": "لابتۆپی بەرزی کارایی بۆ پیشەییەکان"
    },
    "is_active": true,
    "tags": [
      "laptop",
      "professional",
      "apple"
    ],
    "has_variant": true,
    "use_product_defaults": false,
    "images": [
      "https://cdn.example.com/products/macbook-pro/main.jpg",
      "https://cdn.example.com/products/macbook-pro/alt-1.jpg"
    ],
    "media": [
      {
        "type": "image",
        "url": "https://cdn.example.com/products/macbook-pro/main.jpg",
        "sortOrder": 0
      },
      {
        "type": "video",
        "url": "https://cdn.example.com/products/macbook-pro/demo.mp4",
        "sortOrder": 1
      },
      {
        "type": "image",
        "url": "https://cdn.example.com/products/macbook-pro/alt-1.jpg",
        "sortOrder": 2
      }
    ],
    "variant_options": [
      "colors",
      "size",
      "model",
      "ram"
    ],
    "available_option_values": {},
    "categories": [
      {
        "id": "electronics-category-id",
        "name": {
          "en": "Electronics",
          "ar": "إلكترونيات",
          "ku": "ئەلیکترۆنیات"
        }
      }
    ],
    "stock_quantity": 0,
    "created_at": "2024-07-20T10:00:00Z",
    "updated_at": "2024-07-20T10:00:00Z"
  }
}
```

### Response 400 Error - Validation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation failed",
  "errors": [
    {
      "field": "name",
      "message": "Name is required"
    }
  ]
}
```

### Response 409 Error - Duplicate product slug

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This product URL slug is already used in your store. Please choose a different slug."
}
```

## GET /storefront/products — Get Products

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/products#get-get-products)

Retrieves products for the market with KPI stats, server-side sorting, additive legacy filters, and AdvancedDataGrid column filters.

- Existing params remain supported: `search`, `category_id`, `brand_id`, `is_active`, `include_variants`, `limit`, `offset`
- Numeric range filters use `{ "min": number, "max": number }`
- Date range filters use `{ "from": "YYYY-MM-DD", "to": "YYYY-MM-DD" }`
- Boolean filters use string values `"true"` or `"false"`

### Stats
- `totalProducts`: total products in market
- `activeProducts`: products where `is_active = true`
- `lowStockItems`: products where `stock_quantity < 10`
- `topSelling`: English name of the top-selling product in this market

### Notes
- `column_filters` are AND-combined with each other and with top-level filters.
- Legacy numeric string ranges like `"5000,20000"` are still accepted for backward compatibility.
- Legacy single-date `created_at` strings are still accepted and treated as one-day ranges.
- `rating` and `total_reviews` come from actual approved review data.
- `stats` are market-global and are not affected by filters, sorting, or pagination.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/products?search=t-shirt&category_id=category-uuid&brand_id=brand-uuid&is_active=true&include_variants=true&limit=20&offset=0&sort_by=created_at&sort_order=desc&column_filters=%7B%22name%22%3A%22samsung%22%2C%22price%22%3A%7B%22min%22%3A5000%2C%22max%22%3A20000%7D%2C%22has_variant%22%3A%22true%22%2C%22created_at%22%3A%7B%22from%22%3A%222026-04-01%22%2C%22to%22%3A%222026-04-30%22%7D%2C%22is_active%22%3A%22true%22%7D

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| search | query string |  | Search products by name or description in any language (en/ar/ku) | t-shirt |
| category_id | query string |  | Filter products by category ID | category-uuid |
| brand_id | query string |  | Filter products by brand ID | brand-uuid |
| is_active | query string |  | Filter by active status (true/false). Default: true | true |
| include_variants | query string |  | Include product variants in response (true/false). Default: true | true |
| limit | query string |  | Number of products to return (default: 20) | 20 |
| offset | query string |  | Number of products to skip for pagination (default: 0) | 0 |
| sort_by | query string |  | Sort field. Allowed: price, stock_quantity, rating, total_reviews, created_at | created_at |
| sort_order | query string |  | Sort order: asc or desc. Default: desc `asc` or `desc` (default: `desc`) | desc |
| column_filters | query string |  | Advanced column filters JSON. Numeric ranges use min/max. Date ranges use from/to. Boolean filters use string true/false. Supported keys: name, slug, price, stock_quantity, category_id, brand_id, has_variant, is_active, rating, created_at JSON string with supported keys `name`, `slug`, `price`, `stock_quantity`, `category_id`, `brand_id`, `has_variant`, `is_active`, `rating`, `created_at` | {"name":"samsung","slug":"sam","price":{"min":5000,"max":20000},"stock_quantity":{"min":0,"max":100},"category_id":"uuid","brand_id":"uuid","has_variant":"true","is_active":"true","rating":{"min":3,"max":5},"created_at":{"from":"2026-04-01","to":"2026-04-30"}} |



### Response 200 Success - Products with Variant Options

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Products retrieved successfully",
  "products": [
    {
      "id": "product-uuid-1",
      "market_id": "market-uuid",
      "brand_id": "brand-uuid",
      "slug": "aero-pure-3000",
      "price": 12500,
      "name": {
        "en": "Aero-Pure 3000"
      },
      "is_active": true,
      "stock_quantity": 8,
      "rating": 4.6,
      "total_reviews": 23,
      "created_at": "2026-04-01T10:00:00Z",
      "updated_at": "2026-04-02T10:00:00Z"
    }
  ],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "total": 4582
  },
  "stats": {
    "totalProducts": 4582,
    "activeProducts": 4210,
    "lowStockItems": 24,
    "topSelling": "Aero-Pure 3000"
  }
}
```

### Response 200 Success - No Products Found

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Products retrieved successfully",
  "products": [],
  "pagination": {
    "limit": 20,
    "offset": 0,
    "total": 0
  },
  "stats": {
    "totalProducts": 0,
    "activeProducts": 0,
    "lowStockItems": 0,
    "topSelling": ""
  }
}
```

## GET /storefront/products/:productId — Get Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/products#get-get-product-2)

Read a storefront product’s catalog details before displaying or editing it.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/products/:productId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



## GET /storefront/products/:productId/analytics — Get Product Analytics

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/products#get-get-product-analytics)

Returns product detail analytics for the tenant market using the storefront insights read model.

- **Required:** Valid Bearer token with tenant admin permissions

### Read Model
- Current totals/trends come from `storefront_product_insights_current`
- Monthly chart data comes from `storefront_product_insights_daily`
- Engagement events can be fed by `storefront_product_events`

### Response Metrics
- `total_sales`: Total sold quantity for the product
- `total_views`: Current product view count
- `total_likes`: Current like / wishlist count
- `add_to_basket`: Add-to-basket total
- `conversion_rate`: `(total_sales / total_views) * 100` when views exist, otherwise `0`
- `sales_trend`, `views_trend`, `likes_trend`, `basket_trend`: Percentage change vs previous 30-day window
- `monthly_sales`: Sold quantity for the last 12 calendar months in chronological order
- `month_labels`: Month labels aligned with `monthly_sales`
- `total_revenue`, `monthly_revenue`, `avg_rating`, `total_reviews`, `stats_last_updated`: extra fields currently available

### Notes
- If the new insight tables are not deployed yet, the API falls back to the previous live calculation path.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/products/:productId/analytics

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | UUID of the product |  |



### Response 200 Success - Product Analytics

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "analytics": {
    "total_sales": 1248,
    "total_views": 0,
    "total_likes": 312,
    "add_to_basket": 0,
    "conversion_rate": 0,
    "sales_trend": 12.5,
    "views_trend": 0,
    "likes_trend": 0,
    "basket_trend": 0,
    "monthly_sales": [
      65,
      85,
      72,
      95,
      88,
      110,
      102,
      125,
      118,
      135,
      128,
      148
    ],
    "month_labels": [
      "Jan",
      "Feb",
      "Mar",
      "Apr",
      "May",
      "Jun",
      "Jul",
      "Aug",
      "Sep",
      "Oct",
      "Nov",
      "Dec"
    ],
    "total_revenue": 52400,
    "monthly_revenue": [
      2700,
      3400,
      2950,
      4200,
      3980,
      4700,
      4520,
      5300,
      5010,
      5900,
      5650,
      6090
    ],
    "avg_rating": 4.6,
    "total_reviews": 23,
    "stats_last_updated": "2026-04-23T08:00:00Z"
  }
}
```

## PUT /storefront/products/:productId — Update Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/products#put-update-product-2)

Change a storefront product’s content, branding, media, pricing or inventory linkage.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/products/:productId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| slug | string |  |  |  |
| brand_id | string |  |  |  |
| variant_options | array<string> |  |  |  |
| is_active | boolean |  |  |  |
| has_variant | boolean |  |  |  |
| media | array<object> |  |  |  |
| media.type | string |  |  |  |
| media.url | string |  |  |  |
| media.sortOrder | number |  |  |  |
| warehouse_item_id | string |  |  |  |
| price | number |  |  |  |
| sku | string |  |  |  |
| variant_name | object |  |  |  |
| variant_name.en | string |  |  |  |
| variant_description | object |  |  |  |
| variant_description.en | string |  |  |  |
| variant_images | array<string> |  |  |  |
| variant_attributes | object |  |  |  |
| variant_attributes.colors | string |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Updated MacBook Pro",
    "ar": "ماك بوك برو محدث",
    "ku": "ماک بوک پرۆی نوێکراوە"
  },
  "description": {
    "en": "Latest generation MacBook Pro with enhanced performance"
  },
  "slug": "updated-macbook-pro",
  "brand_id": "brand-uuid",
  "variant_options": [
    "colors",
    "size",
    "model",
    "ram",
    "storage"
  ],
  "is_active": true,
  "has_variant": false,
  "media": [
    {
      "type": "image",
      "url": "https://cdn.example.com/products/macbook-pro/main-updated.jpg",
      "sortOrder": 0
    },
    {
      "type": "video",
      "url": "https://cdn.example.com/products/macbook-pro/promo.mp4",
      "sortOrder": 1
    }
  ],
  "warehouse_item_id": "warehouse-item-simple-macbook",
  "price": 1999.99,
  "sku": "MBP-SIMPLE-BASE",
  "variant_name": {
    "en": "MacBook Pro Base Model"
  },
  "variant_description": {
    "en": "Base configuration automatically managed as a single variant"
  },
  "variant_images": [
    "https://cdn.example.com/products/macbook-pro/variant-base.jpg"
  ],
  "variant_attributes": {
    "colors": "Space Gray"
  }
}
```

## DELETE /storefront/products/:productId — Delete Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/products#delete-delete-product-2)

Remove a product from the storefront catalog.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/products/:productId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



## POST /storefront/products/:productId/variants — Create Product Variant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/product-variants#post-create-product-variant)

Creates a new variant for a product. The variant must provide values for all variant options selected on the parent product. The attributes field must contain values for each option defined in the product's variant_options array.

- **Required:** Valid Bearer token with tenant admin permissions

### Exactly one scenario must be provided
- **Scenario A (link existing):** `warehouse_item_id`
- **Scenario B (create new):** `warehouse_id` + `location_id` + `inventory_quantity` (and **no** `warehouse_item_id`)

### Rules
- Sending Scenario A and Scenario B fields together returns 400
- If neither scenario is provided, returns 400
- Scenario B requires permission `manage:operations:tenant`

### Required Fields
- `name`: Multi-language variant name object
- `attributes`: Object containing values for each product variant option

### Optional Fields
- `price`: Variant-specific price. When omitted, product.price is used.
- `description`: Multi-language variant description
- `is_active`: Boolean to enable/disable variant (default: true)
- `is_primary`: Boolean to mark as primary variant (default: false)
- `images`: Array of image URLs
- `syncStock`: Boolean (default: true). When true, variant stock is synced from warehouse after linking.

### Variant Attributes Validation
- Must provide values for ALL variant options defined on the product
- If product has variant_options: ["colors", "size", "model", "ram"]
- Then attributes must include: {"colors": "...", "size": "...", "model": "...", "ram": "..."}
- Missing or extra attributes will cause validation errors

### Automatic Aggregation
- Variant attribute values are automatically aggregated into product's available_option_values
- This enables filtering products by available variant options

### Features
- Multi-language support (en, ar, ku)
- Strict validation of variant attributes
- Primary variant designation
- Image gallery support

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/products/:productId/variants

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| description.ar | string |  |  |  |
| description.ku | string |  |  |  |
| slug | string |  |  |  |
| price | number |  |  |  |
| sku | string |  |  |  |
| warehouse_item_id | string |  |  |  |
| is_active | boolean |  |  |  |
| is_primary | boolean |  |  |  |
| media | array<object> |  |  |  |
| media.type | string |  |  |  |
| media.url | string |  |  |  |
| media.sortOrder | number |  |  |  |
| attributes | object |  |  |  |
| attributes.colors | string |  |  |  |
| attributes.size | string |  |  |  |
| attributes.model | string |  |  |  |
| attributes.ram | string |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "MacBook Pro M4 Space Gray",
    "ar": "ماك بوك برو M4 رمادي فضائي",
    "ku": "ماک بوک پرۆ M4 ڕەنگی گریی ئاسمانی"
  },
  "description": {
    "en": "14-inch MacBook Pro with M4 chip",
    "ar": "ماك بوك برو 14 بوصة مع معالج M4",
    "ku": "ماک بوک پرۆی 14 ئینچی لەگەڵ چیپی M4"
  },
  "slug": "macbook-pro-m4-space-gray",
  "price": 1999.99,
  "sku": "MBP-M4-14-SG-24GB",
  "warehouse_item_id": "warehouse-mbp-m4-space-gray",
  "is_active": true,
  "is_primary": true,
  "media": [
    {
      "type": "image",
      "url": "mbp-space-gray-1.jpg",
      "sortOrder": 0
    },
    {
      "type": "video",
      "url": "mbp-space-gray-rotation.mp4",
      "sortOrder": 1
    },
    {
      "type": "image",
      "url": "mbp-space-gray-2.jpg",
      "sortOrder": 2
    }
  ],
  "attributes": {
    "colors": "Space Gray",
    "size": "14 inch",
    "model": "M4 14CPU 20GPU",
    "ram": "24GB"
  }
}
```

### Response 201 Success - Variant Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "variant-uuid",
    "product_id": "product-uuid",
    "warehouse_item_id": "warehouse-mbp-m4-space-gray",
    "slug": "macbook-pro-m4-space-gray",
    "name": {
      "en": "MacBook Pro M4 Space Gray",
      "ar": "ماك بوك برو M4 رمادي فضائي",
      "ku": "ماک بوک پرۆ M4 ڕەنگی گریی ئاسمانی"
    },
    "description": {
      "en": "14-inch MacBook Pro with M4 chip",
      "ar": "ماك بوك برو 14 بوصة مع معالج M4",
      "ku": "ماک بوک پرۆی 14 ئینچی لەگەڵ چیپی M4"
    },
    "price": 1999.99,
    "sku": "MBP-M4-14-SG-24GB",
    "stock_quantity": 15,
    "is_active": true,
    "is_primary": true,
    "images": [
      "mbp-space-gray-1.jpg",
      "mbp-space-gray-2.jpg"
    ],
    "media": [
      {
        "type": "image",
        "url": "mbp-space-gray-1.jpg",
        "sortOrder": 0
      },
      {
        "type": "video",
        "url": "mbp-space-gray-rotation.mp4",
        "sortOrder": 1
      },
      {
        "type": "image",
        "url": "mbp-space-gray-2.jpg",
        "sortOrder": 2
      }
    ],
    "attributes": {
      "colors": "Space Gray",
      "size": "14 inch",
      "model": "M4 14CPU 20GPU",
      "ram": "24GB"
    },
    "created_at": "2024-07-20T10:00:00Z",
    "updated_at": "2024-07-20T10:00:00Z"
  },
  "message": "Product variant created successfully"
}
```

### Response 400 Error - Attribute Validation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This product does not support variant option 'storage'. Add 'storage' to the product's variant options first, or use only: colors, size, model, ram."
}
```

### Response 400 Error - Missing Required Fields

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation failed",
  "errors": [
    {
      "field": "name",
      "message": "Name is required"
    },
    {
      "field": "price",
      "message": "Price must be a positive number"
    },
    {
      "field": "sku",
      "message": "SKU is required"
    }
  ]
}
```

## POST /storefront/products/:productId/variants/bulk — Create Product Variants Bulk

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/product-variants#post-create-product-variants-bulk)

Create multiple product variants in a single request with strict validation against the product's variant options. All variants must provide values for the exact variant options defined on the parent product.

- **Required:** Valid Bearer token with tenant admin permissions

### Each variant must provide exactly one scenario
- **Scenario A (link existing):** `warehouse_item_id`
- **Scenario B (create new):** `warehouse_id` + `location_id` + `inventory_quantity` (and **no** `warehouse_item_id`)

### Rules
- Sending Scenario A and Scenario B fields together returns 400 for that variant
- If neither scenario is provided, returns 400 for that variant
- Scenario B requires permission `manage:operations:tenant`

### Validation Requirements
- Each variant must have attributes matching ALL product variant options
- If product has variant_options: ["colors", "size", "model", "ram"]
- Then every variant must include: {"colors": "...", "size": "...", "model": "...", "ram": "..."}
- Missing or extra attributes will cause individual variant failures

### Response Format
- **201:** All variants created successfully
- **207:** Partial success (some variants created, some failed)
- **400:** Validation errors or invalid request format
- **404:** Product not found

### Automatic Features
- Stock synchronization from warehouse (if syncStock: true)
- Aggregation of variant values into product's available_option_values
- Multi-language support for names and descriptions
- Primary variant designation
- Image gallery support

### Error Handling
- Individual variant validation with detailed error reporting
- Continues processing valid variants even if some fail
- Comprehensive summary with success/failure counts
- Specific error messages for attribute validation failures

### Use Cases
- Bulk import of product configurations
- Setting up complete product families
- Migrating inventory from external systems
- Creating seasonal product variations

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/products/:productId/variants/bulk

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | The ID of the product to create variants for | your-product-id |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| description.ar | string |  |  |  |
| description.ku | string |  |  |  |
| price | number |  |  |  |
| sku | string |  |  |  |
| warehouse_item_id | string |  |  |  |
| is_active | boolean |  |  |  |
| is_primary | boolean |  |  |  |
| media | array<object> |  |  |  |
| media.type | string |  |  |  |
| media.url | string |  |  |  |
| media.sortOrder | number |  |  |  |
| attributes | object |  |  |  |
| attributes.colors | string |  |  |  |
| attributes.size | string |  |  |  |
| attributes.model | string |  |  |  |
| attributes.ram | string |  |  |  |
| syncStock | boolean |  |  |  |



### Request body (raw)

```json
[
  {
    "name": {
      "en": "MacBook Pro M4 Space Gray",
      "ar": "ماك بوك برو M4 رمادي فضائي",
      "ku": "ماک بوک پرۆ M4 ڕەنگی گریی ئاسمانی"
    },
    "description": {
      "en": "14-inch MacBook Pro with M4 chip and 24GB RAM",
      "ar": "ماك بوك برو 14 بوصة مع معالج M4 وذاكرة 24 جيجابايت",
      "ku": "ماک بوک پرۆی 14 ئینچی لەگەڵ چیپی M4 و یادگەی 24 گیگابایت"
    },
    "price": 1999.99,
    "sku": "MBP-M4-14-SG-24GB",
    "warehouse_item_id": "warehouse-mbp-m4-space-gray-24gb",
    "is_active": true,
    "is_primary": true,
    "media": [
      {
        "type": "image",
        "url": "mbp-space-gray-1.jpg",
        "sortOrder": 0
      },
      {
        "type": "video",
        "url": "mbp-space-gray-spin.mp4",
        "sortOrder": 1
      },
      {
        "type": "image",
        "url": "mbp-space-gray-2.jpg",
        "sortOrder": 2
      }
    ],
    "attributes": {
      "colors": "Space Gray",
      "size": "14 inch",
      "model": "M4 14CPU 20GPU",
      "ram": "24GB"
    },
    "syncStock": true
  },
  {
    "name": {
      "en": "MacBook Pro M4 Silver",
      "ar": "ماك بوك برو M4 فضي",
      "ku": "ماک بوک پرۆ M4 زیوەیی"
    },
    "description": {
      "en": "16-inch MacBook Pro with M4 chip and 32GB RAM",
      "ar": "ماك بوك برو 16 بوصة مع معالج M4 وذاكرة 32 جيجابايت",
      "ku": "ماک بوک پرۆی 16 ئینچی لەگەڵ چیپی M4 و یادگەی 32 گیگابایت"
    },
    "price": 2499.99,
    "sku": "MBP-M4-16-SL-32GB",
    "warehouse_item_id": "warehouse-mbp-m4-silver-32gb",
    "is_active": true,
    "is_primary": false,
    "media": [
      {
        "type": "image",
        "url": "mbp-silver-1.jpg",
        "sortOrder": 0
      },
      {
        "type": "image",
        "url": "mbp-silver-2.jpg",
        "sortOrder": 1
      }
    ],
    "attributes": {
      "colors": "Silver",
      "size": "16 inch",
      "model": "M4 Pro 12CPU 18GPU",
      "ram": "32GB"
    },
    "syncStock": true
  },
  {
    "name": {
      "en": "MacBook Pro M4 Space Black",
      "ar": "ماك بوك برو M4 أسود فضائي",
      "ku": "ماک بوک پرۆ M4 ڕەنگی ڕەشی ئاسمانی"
    },
    "description": {
      "en": "16-inch MacBook Pro with M4 Max chip and 64GB RAM",
      "ar": "ماك بوك برو 16 بوصة مع معالج M4 Max وذاكرة 64 جيجابايت",
      "ku": "ماک بوک پرۆی 16 ئینچی لەگەڵ چیپی M4 Max و یادگەی 64 گیگابایت"
    },
    "price": 3999.99,
    "sku": "MBP-M4MAX-16-SB-64GB",
    "warehouse_item_id": "warehouse-mbp-m4max-space-black-64gb",
    "is_active": true,
    "is_primary": false,
    "media": [
      {
        "type": "image",
        "url": "mbp-space-black-1.jpg",
        "sortOrder": 0
      },
      {
        "type": "image",
        "url": "mbp-space-black-2.jpg",
        "sortOrder": 1
      }
    ],
    "attributes": {
      "colors": "Space Black",
      "size": "16 inch",
      "model": "M4 Max 14CPU 32GPU",
      "ram": "64GB"
    },
    "syncStock": false
  }
]
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "All variants created successfully",
  "created_variants": [
    {
      "id": "variant-id-1",
      "product_id": "product-id",
      "warehouse_item_id": "warehouse-item-red-l",
      "name": {
        "en": "Red T-Shirt Large",
        "ar": "تيشيرت أحمر كبير"
      },
      "price": 25.99,
      "sku": "TSHIRT-RED-L",
      "stock_quantity": 50,
      "is_active": true,
      "is_primary": true,
      "created_at": "2024-01-01T00:00:00Z"
    }
  ],
  "summary": {
    "total_requested": 3,
    "successful": 3,
    "failed": 0
  }
}
```

### Response 207 Partial Success Response

Multi-Status

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "2 of 3 variants created successfully",
  "created_variants": [
    {
      "id": "variant-id-1",
      "product_id": "product-id",
      "name": {
        "en": "Red T-Shirt Large"
      },
      "price": 25.99
    }
  ],
  "errors": [
    {
      "index": 2,
      "error": "Warehouse item not found"
    }
  ],
  "summary": {
    "total_requested": 3,
    "successful": 2,
    "failed": 1
  }
}
```

### Response 400 Validation Error Response

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation errors found",
  "errors": [
    {
      "index": 0,
      "errors": [
        {
          "field": "price",
          "message": "Price is required"
        }
      ]
    }
  ]
}
```

## GET /storefront/products/:productId/variants — Get Product Variants

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/product-variants#get-get-product-variants)

Retrieves variants for a specific product with search and filtering capabilities.

### Features
- Multi-language search across variant names, descriptions, and SKUs
- Active/inactive status filtering
- Pagination support with limit and offset
- Market-scoped results

### Search & Filter Features
- **Multi-language Search:** Searches across English, Arabic, and Kurdish variant names and descriptions, plus SKU field
- **Status Filtering:** Filter by active/inactive variants
- **Pagination:** Use limit and offset for result pagination

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/products/:productId/variants?search=red&is_active=true&limit=20&offset=0

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | The ID of the product to get variants for | your-product-id |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| search | query string | No | Search variants by name, description, or SKU in any language (en/ar/ku) Search term to find variants by name, description, or SKU in any language | red |
| is_active | query string | No | Filter by active status (true/false). If not provided, returns all variants. | true |
| limit | query string | No | Number of variants to return (default: 20) | 20 |
| offset | query string | No | Number of variants to skip for pagination (default: 0) | 0 |



## PUT /storefront/products/:productId/variants/:variantId — Update Product Variant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/product-variants#put-update-product-variant)

Change the price or media of one storefront product variant.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/products/:productId/variants/:variantId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |
| variantId | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| price | number |  |  |  |
| media | array<object> |  |  |  |
| media.type | string |  |  |  |
| media.url | string |  |  |  |
| media.sortOrder | number |  |  |  |



### Request body (raw)

```json
{
  "price": 27.99,
  "media": [
    {
      "type": "image",
      "url": "https://cdn.example.com/variants/red-shirt-front-updated.jpg",
      "sortOrder": 0
    },
    {
      "type": "video",
      "url": "https://cdn.example.com/variants/red-shirt-updated-spin.mp4",
      "sortOrder": 1
    }
  ]
}
```

## DELETE /storefront/products/:productId/variants/:variantId — Delete Product Variant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/products-and-variants/product-variants#delete-delete-product-variant)

Remove a variant from its storefront product.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/products/:productId/variants/:variantId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| productId | path string | Yes | Dynamic path value from the request URL. |  |
| variantId | path string | Yes | Dynamic path value from the request URL. |  |



## GET /storefront/banners — List Banners

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/banner-management#get-list-banners)

Retrieves banners for the tenant's market with advanced filtering and pagination. Only accessible by tenant administrators for banner management.

- **Required:** Valid Bearer token with tenant admin permissions

### Search & Filter Features
- **Content Search:** Find banners by title, description, or URL text
- **Position Filtering:** Filter by specific banner positions
- **Status Filtering:** Filter by active/inactive status
- **Combination Filters:** Use multiple filters together for precise results
- **Client-Side Implementation:** All filtering is performed on fetched data for optimal performance

### Features
- Admin-only access for banner management
- Advanced search and filtering capabilities
- Position-based filtering for layout management
- Status-based filtering for workflow management
- Content-based search for quick finding
- Paginated results for large banner collections
- Complete banner details with positioning info
- Optimized performance with client-side filtering

### Performance Optimization
- **Client-Side Filtering:** All search and filtering operations are performed after fetching the data
- **Benefits:** Faster initial query execution and reduced database complexity
- **Implementation:** JavaScript-based filtering on the frontend for optimal user experience

### Response Data
- Complete banner details with content
- Position and layout information
- Active status and sort order
- Title, description, and URL data
- Creation and modification timestamps
- Pagination metadata

### Use Cases
- Banner management dashboard
- Layout and positioning workflows
- Content moderation and review
- Banner performance analysis
- Bulk banner operations
- Position-specific management
- Campaign banner organization

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/banners?page=1&limit=20&search=sale&is_active=true&position=header

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Items per page (default: 20, max: 100) | 20 |
| search | query string | No | Search banners by title, description, or URL content | sale |
| is_active | query string | No | Filter by active status (true/false) | true |
| position | query string | No | Filter by banner position (header, footer, sidebar, etc.) | header |



### Response 200 Success - Banners List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "banners": [
      {
        "id": "banner-uuid-1",
        "url": "https://example.com/summer-sale-banner.jpg",
        "title": {
          "en": "Summer Sale",
          "ar": "تخفيضات الصيف",
          "ku": "فرۆشتنی هاوین"
        },
        "description": {
          "en": "Get up to 50% off on all summer items",
          "ar": "احصل على خصم يصل إلى 50% على جميع العناصر الصيفية",
          "ku": "تا 50% داشکاندن لەسەر هەموو بابەتەکانی هاوین"
        },
        "link_url": "/categories/summer-collection",
        "position": "header",
        "is_active": true,
        "sort_order": 1,
        "created_at": "2024-05-15T10:00:00Z",
        "updated_at": "2024-05-15T10:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

### Response 200 Success - No Banners Found

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "banners": [],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 0,
      "totalPages": 0
    }
  }
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 404 Error - Market Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Market not found"
}
```

## GET /storefront/advertisements — List Advertisements

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advertisement-management#get-list-advertisements)

Retrieves advertisements for the tenant's market with advanced filtering and pagination. Only accessible by tenant administrators for advertisement management.

- **Required:** Valid Bearer token with tenant admin permissions

### Search & Filter Features
- **Content Search:** Find advertisements by title or description text
- **Placement Filtering:** Filter by specific ad placement locations
- **Status Filtering:** Filter by active/inactive status
- **Expiration Filtering:** Include or exclude expired advertisements
- **Combination Filters:** Use multiple filters together for precise results

### Features
- Admin-only access for advertisement management
- Advanced search and filtering capabilities
- Placement-based filtering for targeted management
- Status-based filtering for workflow management
- Content-based search for quick finding
- Expiration filtering for cleanup workflows
- Paginated results for large advertisement volumes
- Complete advertisement details with targeting info

### Response Data
- Complete advertisement details with content
- Placement and targeting information
- Active status and expiration dates
- Performance metrics and analytics
- Creation and modification timestamps

### Use Cases
- Advertisement management dashboard
- Campaign management workflows
- Content moderation and review
- Performance analysis and optimization
- Bulk advertisement operations
- Placement-specific management
- Expired advertisement cleanup

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/advertisements?page=1&limit=20&placement=banner&is_active=true&search=summer sale&include_expired=false

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Items per page (default: 20, max: 100) | 20 |
| placement | query string | No | Filter by ad placement location (banner, sidebar, popup, etc.) | banner |
| is_active | query string | No | Filter by active status (true/false) | true |
| search | query string | No | Search advertisements by title or description content | summer sale |
| include_expired | query string | No | Include expired advertisements (default: false) | false |



### Response 200 Success - Advertisements List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "advertisements": [
      {
        "id": "ad-uuid-1",
        "market_id": "market-uuid",
        "title": {
          "en": "Summer Sale 2024",
          "ar": "تخفيضات الصيف 2024",
          "ku": "فرۆشتنی هاوینی 2024"
        },
        "description": {
          "en": "Get up to 50% off on all summer items",
          "ar": "احصل على خصم يصل إلى 50% على جميع العناصر الصيفية",
          "ku": "تا 50% داشکاندن لەسەر هەموو بابەتەکانی هاوین"
        },
        "placement": "banner",
        "ad_type": "image",
        "content_url": "https://example.com/summer-sale-banner.jpg",
        "target_url": "/categories/summer-collection",
        "is_active": true,
        "start_date": "2024-06-01T00:00:00Z",
        "end_date": "2024-08-31T23:59:59Z",
        "priority": 10,
        "created_at": "2024-05-15T10:00:00Z",
        "updated_at": "2024-05-15T10:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

### Response 200 Success - No Advertisements Found

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "advertisements": [],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 0,
      "totalPages": 0
    }
  }
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant required"
}
```

### Response 404 Error - Market Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Market not found"
}
```

## POST /storefront/advertisements — Create Advertisement

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advertisement-management#post-create-advertisement)

Creates a new advertisement for the tenant's market. Supports multi-language content and advanced targeting options.

- **Required:** Valid Bearer token with tenant admin permissions

### Required Fields
- `type`: Advertisement type (banner, sidebar, popup, inline)
- `placement`: Where to display (home, category, product, checkout, global)
- `title`: Multi-language title object
- `content`: Multi-language content object

### Optional Fields
- `image_url`: Advertisement image URL
- `link_url`: Target URL when clicked
- `is_active`: Active status (default: true)
- `start_date`: When to start showing
- `end_date`: When to stop showing
- `targeting_rules`: Demographic targeting
- `priority`: Display priority (higher = shown first)

### Features
- Multi-language support (en, ar, ku)
- Scheduled campaigns with start/end dates
- Priority-based display ordering
- Demographic targeting rules
- Flexible placement options
- Immediate activation control

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/advertisements

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| type | string |  |  |  |
| placement | string |  |  |  |
| title | object |  |  |  |
| title.en | string |  |  |  |
| title.ar | string |  |  |  |
| title.ku | string |  |  |  |
| content | object |  |  |  |
| content.en | string |  |  |  |
| content.ar | string |  |  |  |
| content.ku | string |  |  |  |
| image_url | string |  |  |  |
| link_url | string |  |  |  |
| is_active | boolean |  |  |  |
| start_date | string |  |  |  |
| end_date | string |  |  |  |
| targeting_rules | object |  |  |  |
| targeting_rules.demographics | array<string> |  |  |  |
| targeting_rules.interests | array<string> |  |  |  |
| priority | number |  |  |  |



### Request body (raw)

```json
{
  "type": "banner",
  "placement": "home",
  "title": {
    "en": "Black Friday Sale",
    "ar": "تخفيضات الجمعة السوداء",
    "ku": "فرۆشتنی جەژنی ڕەش"
  },
  "content": {
    "en": "Massive discounts on all products",
    "ar": "خصومات هائلة على جميع المنتجات",
    "ku": "داشکاندنی گەورە لەسەر هەموو بەرهەمەکان"
  },
  "image_url": "https://example.com/black-friday-banner.jpg",
  "link_url": "/sales/black-friday",
  "is_active": true,
  "start_date": "2024-11-25T00:00:00Z",
  "end_date": "2024-11-30T23:59:59Z",
  "targeting_rules": {
    "demographics": [
      "18-35"
    ],
    "interests": [
      "fashion",
      "electronics"
    ]
  },
  "priority": 5
}
```

### Response 201 Success - Advertisement Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "advertisement": {
    "id": "new-ad-uuid",
    "market_id": "market-uuid",
    "type": "banner",
    "placement": "home",
    "title": {
      "en": "Black Friday Sale",
      "ar": "تخفيضات الجمعة السوداء",
      "ku": "فرۆشتنی جەژنی ڕەش"
    },
    "content": {
      "en": "Massive discounts on all products",
      "ar": "خصومات هائلة على جميع المنتجات",
      "ku": "داشکاندنی گەورە لەسەر هەموو بەرهەمەکان"
    },
    "image_url": "https://example.com/black-friday-banner.jpg",
    "link_url": "/sales/black-friday",
    "is_active": true,
    "start_date": "2024-11-25T00:00:00Z",
    "end_date": "2024-11-30T23:59:59Z",
    "targeting_rules": {
      "demographics": [
        "18-35"
      ],
      "interests": [
        "fashion",
        "electronics"
      ]
    },
    "priority": 5,
    "is_currently_active": false,
    "created_at": "2024-07-20T10:00:00Z",
    "updated_at": "2024-07-20T10:00:00Z"
  },
  "message": "Advertisement created successfully"
}
```

### Response 400 Error - Validation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation failed",
  "errors": [
    {
      "field": "type",
      "message": "Type is required"
    },
    {
      "field": "title",
      "message": "Title is required"
    }
  ]
}
```

## GET /storefront/advertisements/{{advertisement_id}} — Get Advertisement Details

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advertisement-management#get-get-advertisement-details)

Retrieves detailed information about a specific advertisement including analytics data.

- **Required:** Valid Bearer token with tenant admin permissions

### Response Data
- Complete advertisement details
- Analytics summary (views, clicks, CTR)
- Market information
- Current active status
- Scheduling information

### Use Cases
- Advertisement editing workflows
- Performance monitoring
- Content review and approval
- Analytics dashboard details

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/advertisements/{{advertisement_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| advertisement_id | path string | Yes | UUID of the advertisement to retrieve |  |



### Response 200 Success - Advertisement Details

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "advertisement": {
    "id": "ad-uuid",
    "market_id": "market-uuid",
    "type": "banner",
    "placement": "home",
    "title": {
      "en": "Summer Sale 2024",
      "ar": "تخفيضات الصيف 2024",
      "ku": "فرۆشتنی هاوینی 2024"
    },
    "content": {
      "en": "Get up to 50% off on all summer items",
      "ar": "احصل على خصم يصل إلى 50% على جميع العناصر الصيفية",
      "ku": "تا 50% داشکاندن لەسەر هەموو بابەتەکانی هاوین"
    },
    "image_url": "https://example.com/summer-sale-banner.jpg",
    "link_url": "/categories/summer-collection",
    "is_active": true,
    "start_date": "2024-06-01T00:00:00Z",
    "end_date": "2024-08-31T23:59:59Z",
    "targeting_rules": {},
    "priority": 10,
    "is_currently_active": true,
    "created_at": "2024-05-15T10:00:00Z",
    "updated_at": "2024-05-15T10:00:00Z",
    "market": {
      "store_name": "my-store"
    },
    "analytics": {
      "total_views": 1250,
      "total_clicks": 85,
      "click_through_rate": 6.8
    }
  }
}
```

### Response 404 Error - Advertisement Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Advertisement not found"
}
```

## PUT /storefront/advertisements/{{advertisement_id}} — Update Advertisement

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advertisement-management#put-update-advertisement)

Updates an existing advertisement. All fields are optional - only provided fields will be updated.

- **Required:** Valid Bearer token with tenant admin permissions

### Updatable Fields
- `type`: Change advertisement type
- `placement`: Change placement location
- `title`: Update multi-language titles
- `content`: Update multi-language content
- `image_url`: Change advertisement image
- `link_url`: Update target URL
- `is_active`: Enable/disable advertisement
- `start_date`: Modify start scheduling
- `end_date`: Modify end scheduling
- `targeting_rules`: Update targeting criteria
- `priority`: Change display priority

### Features
- Partial updates (only send changed fields)
- Multi-language content updates
- Schedule modifications
- Priority adjustments
- Instant activation control

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/advertisements/{{advertisement_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| advertisement_id | path string | Yes | UUID of the advertisement to update |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| title | object |  |  |  |
| title.en | string |  |  |  |
| title.ar | string |  |  |  |
| title.ku | string |  |  |  |
| is_active | boolean |  |  |  |
| priority | number |  |  |  |
| end_date | string |  |  |  |



### Request body (raw)

```json
{
  "title": {
    "en": "Updated Sale Title",
    "ar": "عنوان التخفيضات المحدث",
    "ku": "ناونیشانی فرۆشتنی نوێکراوە"
  },
  "is_active": false,
  "priority": 15,
  "end_date": "2024-12-31T23:59:59Z"
}
```

### Response 200 Success - Advertisement Updated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "advertisement": {
    "id": "ad-uuid",
    "market_id": "market-uuid",
    "type": "banner",
    "placement": "home",
    "title": {
      "en": "Updated Sale Title",
      "ar": "عنوان التخفيضات المحدث",
      "ku": "ناونیشانی فرۆشتنی نوێکراوە"
    },
    "content": {
      "en": "Get up to 50% off on all summer items",
      "ar": "احصل على خصم يصل إلى 50% على جميع العناصر الصيفية",
      "ku": "تا 50% داشکاندن لەسەر هەموو بابەتەکانی هاوین"
    },
    "image_url": "https://example.com/summer-sale-banner.jpg",
    "link_url": "/categories/summer-collection",
    "is_active": false,
    "start_date": "2024-06-01T00:00:00Z",
    "end_date": "2024-12-31T23:59:59Z",
    "targeting_rules": {},
    "priority": 15,
    "is_currently_active": false,
    "created_at": "2024-05-15T10:00:00Z",
    "updated_at": "2024-07-20T11:00:00Z"
  },
  "message": "Advertisement updated successfully"
}
```

## DELETE /storefront/advertisements/{{advertisement_id}} — Delete Advertisement

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advertisement-management#delete-delete-advertisement)

Permanently deletes an advertisement and all associated analytics data.

- **Required:** Valid Bearer token with tenant admin permissions

### Important Notes
- This action is irreversible
- All analytics data will be lost
- Advertisement will be immediately removed from all placements
- Consider deactivating instead of deleting for data retention

### Use Cases
- Cleanup of test advertisements
- Removal of inappropriate content
- Campaign end-of-life management
- Database maintenance

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/advertisements/{{advertisement_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| advertisement_id | path string | Yes | UUID of the advertisement to delete |  |



### Response 200 Success - Advertisement Deleted

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Advertisement deleted successfully"
}
```

## GET /storefront/advertisements/{{advertisement_id}}/analytics — Get Advertisement Analytics

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advertisement-management#get-get-advertisement-analytics)

Retrieves detailed analytics data for a specific advertisement with optional date range filtering.

- **Required:** Valid Bearer token with tenant admin permissions

### Analytics Metrics
- Total views and clicks
- Click-through rate (CTR)
- Daily breakdown data
- Impression counts
- Performance trends

### Features
- Date range filtering
- Daily granular data
- Calculated performance metrics
- Historical trend analysis
- Export-ready data format

### Use Cases
- Campaign performance review
- ROI analysis and reporting
- A/B testing comparisons
- Optimization insights
- Executive dashboards

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/advertisements/{{advertisement_id}}/analytics?start_date=2024-07-01&end_date=2024-07-31

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| advertisement_id | path string | Yes | UUID of the advertisement |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| start_date | query string | No | Start date for analytics data (ISO format) Start date for analytics (ISO format) | 2024-07-01 |
| end_date | query string | No | End date for analytics data (ISO format) End date for analytics (ISO format) | 2024-07-31 |



### Response 200 Success - Analytics Data

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "advertisement_id": "ad-uuid",
  "analytics": {
    "totalViews": 1250,
    "totalClicks": 85,
    "totalImpressions": 1500,
    "clickThroughRate": 6.8,
    "dailyData": [
      {
        "date": "2024-07-01",
        "views": 45,
        "clicks": 3,
        "impressions": 60
      },
      {
        "date": "2024-07-02",
        "views": 52,
        "clicks": 4,
        "impressions": 65
      },
      {
        "date": "2024-07-03",
        "views": 38,
        "clicks": 2,
        "impressions": 48
      }
    ]
  },
  "period": "2024-07-01_to_2024-07-31"
}
```

## GET /storefront/categories — List Categories

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/category-management#get-list-categories)

Retrieves categories for the authenticated tenant's market with search and filtering capabilities. By default returns hierarchical structure with parent-child relationships. Use ?flat=true for a paginated flat list.

### Features
- Market-scoped (only shows categories for current tenant)
- Multi-language support (en/ar/ku)
- Hierarchical organization with search/filter support
- Advanced search in category names and descriptions
- Active/inactive status filtering
- Pagination support for flat lists
- Ordered by sort_order field

### Search & Filter Features
- **Multi-language Search:** Searches across English, Arabic, and Kurdish category names and descriptions
- **Status Filtering:** Filter by active/inactive categories
- **Pagination:** Available for flat list view only
- **Hierarchical Filtering:** Search and status filters apply to hierarchical view as well

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/categories?flat=false&search=electronics&is_active=true&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| flat | query string | No | Set to 'true' for flat list, 'false' for hierarchical structure Set to 'true' for flat paginated list, 'false' for hierarchical structure (default: false) | false |
| search | query string | No | Search categories by name or description in any language (en/ar/ku) Search term to find categories by name or description in any language | electronics |
| is_active | query string | No | Filter by active status (true/false). If not provided, returns all categories. | true |
| page | query string | No | Page number for pagination (default: 1, only applicable when flat=true) Page number for pagination (default: 1, only for flat=true) | 1 |
| limit | query string | No | Items per page (default: 20, only applicable when flat=true) Items per page (default: 20, only for flat=true) | 20 |



### Response 200 Success - Hierarchical Categories

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "categories": [
      {
        "id": "cat-uuid-1",
        "market_id": "market-uuid",
        "parent_id": null,
        "name": {
          "en": "Electronics",
          "ar": "إلكترونيات",
          "ku": "ئەلیکترۆنیات"
        },
        "description": {
          "en": "Electronic devices and accessories",
          "ar": "الأجهزة الإلكترونية والإكسسوارات",
          "ku": "ئامێرە ئەلیکترۆنییەکان و پێداویستییەکان"
        },
        "category_image": "https://example.com/images/electronics-category.jpg",
        "sort_order": 1,
        "is_active": true,
        "created_at": "2023-12-01T10:00:00Z",
        "updated_at": "2023-12-01T10:00:00Z",
        "children": [
          {
            "id": "cat-uuid-2",
            "market_id": "market-uuid",
            "parent_id": "cat-uuid-1",
            "name": {
              "en": "Smartphones",
              "ar": "الهواتف الذكية",
              "ku": "تەلەفۆنە زیرەکەکان"
            },
            "description": {
              "en": "Mobile phones and accessories",
              "ar": "الهواتف المحمولة والإكسسوارات",
              "ku": "تەلەفۆنی دەستی و پێداویستییەکان"
            },
            "category_image": "https://example.com/images/smartphones-category.jpg",
            "sort_order": 1,
            "is_active": true,
            "created_at": "2023-12-01T10:30:00Z",
            "updated_at": "2023-12-01T10:30:00Z",
            "children": []
          }
        ]
      },
      {
        "id": "cat-uuid-3",
        "market_id": "market-uuid",
        "parent_id": null,
        "name": {
          "en": "Clothing",
          "ar": "ملابس",
          "ku": "جل و بەرگ"
        },
        "description": {
          "en": "Apparel and fashion items",
          "ar": "ملابس وأزياء",
          "ku": "جل و بەرگ و مۆدە"
        },
        "category_image": "https://example.com/images/clothing-category.jpg",
        "sort_order": 2,
        "is_active": true,
        "created_at": "2023-12-01T11:00:00Z",
        "updated_at": "2023-12-01T11:00:00Z",
        "children": []
      }
    ]
  }
}
```

### Response 200 Success - Flat Categories List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "categories": [
    {
      "id": "cat-uuid-1",
      "market_id": "market-uuid",
      "parent_id": null,
      "name": {
        "en": "Electronics",
        "ar": "إلكترونيات",
        "ku": "ئەلیکترۆنیات"
      },
      "description": {
        "en": "Electronic devices and accessories",
        "ar": "الأجهزة الإلكترونية والإكسسوارات",
        "ku": "ئامێرە ئەلیکترۆنییەکان و پێداویستییەکان"
      },
      "sort_order": 1,
      "is_active": true,
      "created_at": "2023-12-01T10:00:00Z",
      "updated_at": "2023-12-01T10:00:00Z",
      "children": [
        {
          "id": "cat-uuid-2",
          "market_id": "market-uuid",
          "parent_id": "cat-uuid-1",
          "name": {
            "en": "Smartphones",
            "ar": "الهواتف الذكية",
            "ku": "تەلەفۆنە زیرەکەکان"
          },
          "description": {
            "en": "Mobile phones and accessories",
            "ar": "الهواتف المحمولة والإكسسوارات",
            "ku": "تەلەفۆنی دەستی و پێداویستییەکان"
          },
          "sort_order": 1,
          "is_active": true,
          "created_at": "2023-12-01T10:30:00Z",
          "updated_at": "2023-12-01T10:30:00Z",
          "children": []
        }
      ]
    }
  ]
}
```

### Response 200 Success - Flat Categories List with Pagination

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "categories": [
    {
      "id": "cat-uuid-1",
      "market_id": "market-uuid",
      "parent_id": null,
      "name": {
        "en": "Electronics",
        "ar": "إلكترونيات",
        "ku": "ئەلیکترۆنیات"
      },
      "description": {
        "en": "Electronic devices and accessories",
        "ar": "الأجهزة الإلكترونية والإكسسوارات",
        "ku": "ئامێرە ئەلیکترۆنییەکان و پێداویستییەکان"
      },
      "sort_order": 1,
      "is_active": true,
      "created_at": "2023-12-01T10:00:00Z",
      "updated_at": "2023-12-01T10:00:00Z"
    },
    {
      "id": "cat-uuid-2",
      "market_id": "market-uuid",
      "parent_id": "cat-uuid-1",
      "name": {
        "en": "Smartphones",
        "ar": "الهواتف الذكية",
        "ku": "تەلەفۆنە زیرەکەکان"
      },
      "description": {
        "en": "Mobile phones and accessories",
        "ar": "الهواتف المحمولة والإكسسوارات",
        "ku": "تەلەفۆنی دەستی و پێداویستییەکان"
      },
      "sort_order": 1,
      "is_active": true,
      "created_at": "2023-12-01T10:30:00Z",
      "updated_at": "2023-12-01T10:30:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 2,
    "totalPages": 1
  }
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 404 Error - Market Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Market not found"
}
```

## GET /storefront/categories/:categoryId — Get Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/category-management#get-get-category)

Retrieves a single category by its ID for the authenticated tenant's market.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/categories/:categoryId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| categoryId | path string | Yes | UUID of the category to retrieve | cat-uuid-1 |



### Response 200 Success - Category Details

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "cat-uuid-1",
    "market_id": "market-uuid",
    "parent_id": null,
    "name": {
      "en": "Electronics",
      "ar": "إلكترونيات",
      "ku": "ئەلیکترۆنیات"
    },
    "description": {
      "en": "Electronic devices and accessories",
      "ar": "الأجهزة الإلكترونية والإكسسوارات",
      "ku": "ئامێرە ئەلیکترۆنییەکان و پێداویستییەکان"
    },
    "sort_order": 1,
    "is_active": true,
    "created_at": "2023-12-01T10:00:00Z",
    "updated_at": "2023-12-01T10:00:00Z"
  }
}
```

### Response 404 Error - Category Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Category not found"
}
```

## POST /storefront/categories — Create Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/category-management#post-create-category-2)

Creates a new category in the authenticated tenant's market. Categories can be organized hierarchically by setting a parent_id and can include visual representation with category images.

- **name** (required): Multi-language object with category names
- **description** (optional): Multi-language object with category descriptions
- **category_image** (optional): URL or path to category image for visual representation
- **parent_id** (optional): UUID of parent category for hierarchical organization
- **is_active** (optional): Boolean to set category status (default: true)
- **sort_order** (optional): Integer for display ordering (default: 0)

### Features
- Multi-language support (en/ar/ku)
- Visual category representation with images
- Hierarchical categorization
- Custom sorting order
- Active/inactive status control
- Market-scoped creation

### Image Recommendations
- Use high-quality images that represent the category
- Recommended size: 400x400 pixels or larger
- Supported formats: JPG, PNG, WebP
- Store images in accessible URLs (CDN recommended)

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/categories

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| description.ar | string |  |  |  |
| description.ku | string |  |  |  |
| category_image | string |  |  |  |
| parent_id | null |  |  |  |
| is_active | boolean |  |  |  |
| sort_order | number |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Home & Garden",
    "ar": "المنزل والحديقة",
    "ku": "ماڵ و باخچە"
  },
  "description": {
    "en": "Home improvement and garden supplies",
    "ar": "مستلزمات تحسين المنزل والحديقة",
    "ku": "پێداویستییەکانی باشترکردنی ماڵ و باخچە"
  },
  "category_image": "https://example.com/images/home-garden-category.jpg",
  "parent_id": null,
  "is_active": true,
  "sort_order": 3
}
```

### Response 201 Success - Category Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "category": {
      "id": "cat-uuid-new",
      "market_id": "market-uuid",
      "parent_id": null,
      "name": {
        "en": "Home & Garden",
        "ar": "المنزل والحديقة",
        "ku": "ماڵ و باخچە"
      },
      "description": {
        "en": "Home improvement and garden supplies",
        "ar": "مستلزمات تحسين المنزل والحديقة",
        "ku": "پێداویستییەکانی باشترکردنی ماڵ و باخچە"
      },
      "category_image": "https://example.com/images/home-garden-category.jpg",
      "sort_order": 3,
      "is_active": true,
      "created_at": "2023-12-01T12:00:00Z",
      "updated_at": "2023-12-01T12:00:00Z"
    },
    "message": "Category created successfully"
  }
}
```

### Response 201 Success - Subcategory Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "category": {
      "id": "cat-uuid-garden-tools",
      "market_id": "market-uuid",
      "parent_id": "cat-uuid-home-garden",
      "name": {
        "en": "Garden Tools",
        "ar": "أدوات الحديقة",
        "ku": "ئامرازەکانی باخچە"
      },
      "description": {
        "en": "Tools and equipment for gardening",
        "ar": "أدوات ومعدات البستنة",
        "ku": "ئامراز و کەرەستەکانی باخبانی"
      },
      "sort_order": 1,
      "is_active": true,
      "created_at": "2023-12-01T12:30:00Z",
      "updated_at": "2023-12-01T12:30:00Z"
    },
    "message": "Category created successfully"
  }
}
```

### Response 400 Error - Validation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation failed",
  "errors": [
    {
      "field": "name",
      "message": "Field is required"
    }
  ]
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant required"
}
```

## PUT /storefront/categories/:categoryId — Update Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/category-management#put-update-category-2)

Updates an existing category in the authenticated tenant's market. All fields are optional for partial updates including the category image.

- **categoryId** (required): UUID of the category to update

- **name**: Multi-language object with updated category names
- **description**: Multi-language object with updated category descriptions
- **category_image**: URL or path to updated category image
- **parent_id**: UUID of new parent category (null to make root category)
- **is_active**: Boolean to update category status
- **sort_order**: Integer for updated display ordering

### Features
- Partial updates (only provided fields are updated)
- Multi-language support
- Image updates for visual representation
- Hierarchical reorganization
- Status and ordering updates
- Market-scoped updates

### Image Update Notes
- Provide new image URL to update category image
- Set to null to remove existing image
- Same image recommendations as creation apply

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/categories/:categoryId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| categoryId | path string | Yes | UUID of the category to update | cat-uuid-1 |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| description.ar | string |  |  |  |
| description.ku | string |  |  |  |
| category_image | string |  |  |  |
| is_active | boolean |  |  |  |
| sort_order | number |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Electronics & Gadgets",
    "ar": "الإلكترونيات والأجهزة",
    "ku": "ئەلیکترۆنیات و ئامێرەکان"
  },
  "description": {
    "en": "Electronic devices, gadgets and accessories",
    "ar": "الأجهزة الإلكترونية والأدوات والإكسسوارات",
    "ku": "ئامێرە ئەلیکترۆنییەکان، ئامێرەکان و پێداویستییەکان"
  },
  "category_image": "https://example.com/images/electronics-gadgets-updated.jpg",
  "is_active": true,
  "sort_order": 1
}
```

### Response 200 Success - Category Updated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "category": {
      "id": "cat-uuid-1",
      "market_id": "market-uuid",
      "parent_id": null,
      "name": {
        "en": "Electronics & Gadgets",
        "ar": "الإلكترونيات والأجهزة",
        "ku": "ئەلیکترۆنیات و ئامێرەکان"
      },
      "description": {
        "en": "Electronic devices, gadgets and accessories",
        "ar": "الأجهزة الإلكترونية والأدوات والإكسسوارات",
        "ku": "ئامێرە ئەلیکترۆنییەکان، ئامێرەکان و پێداویستییەکان"
      },
      "sort_order": 1,
      "is_active": true,
      "created_at": "2023-12-01T10:00:00Z",
      "updated_at": "2023-12-01T13:00:00Z"
    },
    "message": "Category updated successfully"
  }
}
```

### Response 200 Success - Partial Update

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "category": {
      "id": "cat-uuid-1",
      "market_id": "market-uuid",
      "parent_id": null,
      "name": {
        "en": "Electronics & Gadgets",
        "ar": "الإلكترونيات والأجهزة",
        "ku": "ئەلیکترۆنیات و ئامێرەکان"
      },
      "description": {
        "en": "Electronic devices, gadgets and accessories",
        "ar": "الأجهزة الإلكترونية والأدوات والإكسسوارات",
        "ku": "ئامێرە ئەلیکترۆنییەکان، ئامێرەکان و پێداویستییەکان"
      },
      "sort_order": 1,
      "is_active": false,
      "created_at": "2023-12-01T10:00:00Z",
      "updated_at": "2023-12-01T13:30:00Z"
    },
    "message": "Category updated successfully"
  }
}
```

### Response 404 Error - Category Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Category not found"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant required"
}
```

## DELETE /storefront/categories/:categoryId — Delete Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/category-management#delete-delete-category-2)

Deletes a category from the authenticated tenant's market. Includes safety checks to prevent data loss.

- **categoryId** (required): UUID of the category to delete

### Safety Features
- Cannot delete category if it has child categories
- Cannot delete category if it has assigned products
- Returns specific error messages for each prevention case
- Market-scoped deletion (only within tenant's market)

### Deletion Rules
1. Category must exist and belong to the tenant's market
2. Category must not have any child categories
3. Category must not have any assigned products
4. If any rule fails, deletion is prevented with explanation

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/categories/:categoryId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| categoryId | path string | Yes | UUID of the category to delete | cat-uuid-1 |



### Response 200 Success - Category Deleted

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "message": "Category deleted successfully"
  }
}
```

### Response 400 Error - Category Has Children

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Category has children and cannot be deleted. Please delete or move child categories first."
}
```

### Response 400 Error - Category Has Products

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Category has products assigned and cannot be deleted. Please reassign or remove products first."
}
```

### Response 404 Error - Category Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Category not found"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## GET /storefront/favorites — Get User Favorites

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/favorites-and-wishlist#get-get-user-favorites)

Retrieves the authenticated user's favorite products with pagination support. Shows comprehensive product details including current pricing and availability.

### Note

Search and category filtering have been removed from this API for performance optimization (avoiding JOIN table queries). For advanced filtering, consider implementing client-side filtering after fetching the data.

- **Required:** Valid Bearer token for user identification

### Performance Optimization
- **Removed Features:** Search by product name and category filtering to avoid expensive JOIN operations
- **Recommendation:** Implement search and category filtering on the client side after fetching favorites data
- **Benefits:** Faster query execution and reduced database load

### Features
- Paginated results for large wishlist management
- Real-time product availability status
- Current pricing information
- Product images and details
- Multi-language product information
- Market-specific filtering
- Favorite date tracking
- Optimized query performance

### Response Data
- Complete favorite items with product details
- Current product pricing and availability
- Favorite creation timestamps
- Pagination metadata
- Product categories and descriptions
- Stock availability indicators

### Client-Side Filtering Suggestions
- Filter by product name using JavaScript array methods
- Group favorites by category after fetching
- Implement local search functionality
- Cache favorites data for better performance

### Use Cases
- Wishlist page rendering
- Favorite products management
- Price tracking for saved items
- Cross-market favorite comparison
- Mobile app favorites sync

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/favorites?page=1&limit=20&market_id={{market_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Items per page (default: 20, max: 100) | 20 |
| market_id | query string | No | Filter favorites by specific market (optional) | {{market_id}} |



### Response 200 Success - Favorites List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "favorites": [
    {
      "id": "favorite-uuid-1",
      "user_id": "user-uuid-456",
      "variant_id": "variant-uuid-123",
      "variant_slug": "iphone-15-pro-natural-titanium",
      "product_id": "product-uuid-abc",
      "product_slug": "iphone-15-pro",
      "has_variant": true,
      "created_at": "2023-12-01T10:00:00Z",
      "updated_at": null,
      "catalog_item": {
        "id": "variant-uuid-123",
        "slug": "iphone-15-pro-natural-titanium",
        "product_id": "product-uuid-abc",
        "name": {
          "en": "iPhone 15 Pro",
          "ar": "آيفون 15 برو",
          "ku": "ئایفۆن 15 پرۆ"
        },
        "description": {
          "en": "Latest iPhone with advanced camera system"
        },
        "price": 999.99,
        "images": [
          "https://example.com/iphone15-pro-1.jpg"
        ],
        "is_active": true
      },
      "is_available": true,
      "current_price": 999.99,
      "availability_status": "available"
    },
    {
      "id": "favorite-uuid-2",
      "user_id": "user-uuid-456",
      "variant_id": "variant-uuid-789",
      "variant_slug": "premium-cotton-tshirt-white",
      "product_id": "product-uuid-def",
      "product_slug": "premium-cotton-tshirt",
      "has_variant": false,
      "created_at": "2023-11-28T15:30:00Z",
      "updated_at": null,
      "catalog_item": {
        "id": "variant-uuid-789",
        "slug": "premium-cotton-tshirt-white",
        "product_id": "product-uuid-def",
        "name": {
          "en": "Premium Cotton T-Shirt"
        },
        "price": 89.99,
        "images": [
          "https://example.com/tshirt-1.jpg"
        ],
        "is_active": false
      },
      "is_available": false,
      "current_price": 89.99,
      "availability_status": "inactive"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 2,
    "totalPages": 1
  }
}
```

### Response 200 Success - Empty Favorites

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "favorites": [],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 0,
      "totalPages": 0
    }
  }
}
```

### Response 401 Error - Authentication Required

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Authentication required"
}
```

## POST /storefront/favorites — Add to Favorites

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/favorites-and-wishlist#post-add-to-favorites)

Adds a product to the user's favorites/wishlist. Prevents duplicate entries and provides detailed product information in response.

- **Required:** Valid Bearer token for user identification

### Features
- Duplicate prevention (product can only be favorited once)
- Product validation (ensures product exists and is accessible)
- Immediate product details in response
- Cross-market favorite support
- Favorite timestamp recording

### Business Rules
- User must be authenticated
- Product must exist and be accessible
- Cannot add the same product to favorites twice
- Product can be from any market the user has access to

### Response Data
- Created favorite item with ID
- Complete product details
- Current pricing and availability
- Market information
- Creation timestamp

### Use Cases
- Product page "Add to Wishlist" action
- Quick favoriting from product listings
- Cross-market product saving
- Price tracking setup
- Mobile app wishlist sync

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/favorites

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| variant_id | string | Yes | UUID of the product to add to favorites |  |



### Request body (raw)

```json
{
  "variant_id": "variant-uuid-123"
}
```

### Response 201 Success - Item Added to Favorites

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "favorite": {
    "id": "favorite-uuid-new",
    "user_id": "user-uuid-456",
    "variant_id": "variant-uuid-123",
    "variant_slug": "macbook-pro-14-space-black",
    "product_id": "product-uuid-abc",
    "product_slug": "macbook-pro-14",
    "has_variant": true,
    "created_at": "2023-12-01T18:00:00Z",
    "updated_at": null,
    "catalog_item": {
      "id": "variant-uuid-123",
      "slug": "macbook-pro-14-space-black",
      "product_id": "product-uuid-abc",
      "name": {
        "en": "MacBook Pro 14-inch",
        "ar": "ماك بوك برو 14 بوصة"
      },
      "description": {
        "en": "Powerful laptop with M3 Pro chip"
      },
      "price": 1999.99,
      "images": [
        "https://example.com/macbook-pro-1.jpg"
      ],
      "is_active": true
    },
    "is_available": true,
    "current_price": 1999.99,
    "availability_status": "available"
  },
  "message": "Added to favorites"
}
```

### Response 409 Error - Already in Favorites

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Product is already in your favorites"
}
```

### Response 404 Error - Product Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Product not found or not accessible"
}
```

### Response 400 Error - Validation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation failed",
  "errors": [
    {
      "field": "variant_id",
      "message": "Field is required"
    }
  ]
}
```

## DELETE /storefront/favorites/:favorite_id — Remove from Favorites

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/favorites-and-wishlist#delete-remove-from-favorites)

Removes a specific item from the user's favorites/wishlist by favorite ID.

- **Required:** Valid Bearer token for user identification

### Features
- Safe removal with ownership validation
- Immediate confirmation
- Clean database cleanup
- No cascade effects on product data

### Business Rules
- User must be authenticated
- User can only remove their own favorites
- Favorite must exist
- Removal is immediate and permanent

### Response Data
- Success confirmation message
- Removal timestamp

### Use Cases
- Wishlist management
- "Remove from Favorites" button action
- Favorites cleanup
- Wishlist organization

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/favorites/:favorite_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| favorite_id | path string | Yes | UUID of the favorite item to remove | favorite-uuid-123 |



### Response 200 Success - Item Removed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "message": "Product removed from favorites successfully",
    "removed_at": "2023-12-01T18:30:00Z"
  }
}
```

### Response 404 Error - Favorite Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Favorite item not found"
}
```

### Response 403 Error - Unauthorized Access

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized to access this favorite item"
}
```

## GET /storefront/products/:product_id/favorite-status — Check Favorite Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/favorites-and-wishlist#get-check-favorite-status)

Checks whether a specific product is in the user's favorites list. Useful for UI state management on product pages.

- **Required:** Valid Bearer token for user identification

### Features
- Quick favorite status lookup
- Favorite ID return for easy removal
- UI state management support
- Fast response for real-time updates

### Response Data
- Boolean favorite status
- Favorite ID if favorited
- Favorite creation date if favorited

### Use Cases
- Product page heart/favorite icon state
- Product listing favorite indicators
- UI component state management
- Mobile app synchronization
- Batch favorite status checking

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/products/:product_id/favorite-status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| product_id | path string | Yes | UUID of the product to check favorite status | product-uuid-123 |



### Response 200 Success - Is Favorited

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "is_favorited": true,
    "favorite_id": "favorite-uuid-456",
    "favorited_at": "2023-12-01T10:00:00Z"
  }
}
```

### Response 200 Success - Not Favorited

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "is_favorited": false,
    "favorite_id": null,
    "favorited_at": null
  }
}
```

### Response 404 Error - Product Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Product not found"
}
```

## GET /storefront/favorites/updates — Get Favorite Updates

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/favorites-and-wishlist#get-get-favorite-updates)

Retrieves favorites with availability and pricing updates. Useful for notifying users about changes to their wishlist items.

- **Required:** Valid Bearer token for user identification

### Features
- Tracks newly available products
- Monitors price changes (increases and decreases)
- Identifies out-of-stock items
- Provides update summaries
- Supports notification systems

### Update Categories
- **Newly Available:** Products that were out of stock and are now available
- **Price Changed:** Products with price modifications
- **Out of Stock:** Products that became unavailable

### Response Data
- Categorized favorite updates
- Detailed change information
- Update summaries and counts
- Product details for each update

### Use Cases
- Wishlist notification system
- Price drop alerts
- Stock availability notifications
- Email/push notification content
- Dashboard activity feeds

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/favorites/updates

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Response 200 Success - Updates Available

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "newly_available": [
      {
        "id": "favorite-uuid-1",
        "variant_id": "variant-uuid-123",
        "variant": {
          "id": "variant-uuid-123",
          "market_id": "market-uuid-abc",
          "name": {
            "en": "Gaming Laptop",
            "ar": "لابتوب الألعاب",
            "ku": "لاپتۆپی یاری"
          },
          "price": 1299.99,
          "stock_available": 5,
          "images": [
            "https://example.com/gaming-laptop.jpg"
          ]
        },
        "became_available_at": "2023-12-01T14:00:00Z"
      }
    ],
    "price_changed": [
      {
        "id": "favorite-uuid-2",
        "variant_id": "variant-uuid-456",
        "variant": {
          "id": "variant-uuid-456",
          "market_id": "market-uuid-def",
          "name": {
            "en": "Wireless Headphones",
            "ar": "سماعات لاسلكية",
            "ku": "هێدفۆنی بێ تەل"
          },
          "price": 199.99,
          "images": [
            "https://example.com/headphones.jpg"
          ]
        },
        "price_change": {
          "old_price": 249.99,
          "new_price": 199.99,
          "change_amount": -50,
          "change_percentage": -20,
          "changed_at": "2023-12-01T16:00:00Z"
        }
      }
    ],
    "out_of_stock": [
      {
        "id": "favorite-uuid-3",
        "variant_id": "variant-uuid-789",
        "variant": {
          "id": "variant-uuid-789",
          "market_id": "market-uuid-ghi",
          "name": {
            "en": "Smart Watch",
            "ar": "ساعة ذكية",
            "ku": "کاتژمێری زیرەک"
          },
          "price": 299.99,
          "stock_available": 0,
          "images": [
            "https://example.com/smartwatch.jpg"
          ]
        },
        "became_unavailable_at": "2023-12-01T12:00:00Z"
      }
    ],
    "summary": {
      "newly_available_count": 1,
      "price_changed_count": 1,
      "out_of_stock_count": 1,
      "total_updates": 3
    }
  }
}
```

### Response 200 Success - No Updates

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "newly_available": [],
    "price_changed": [],
    "out_of_stock": [],
    "summary": {
      "newly_available_count": 0,
      "price_changed_count": 0,
      "out_of_stock_count": 0,
      "total_updates": 0
    }
  }
}
```

## GET /storefront/favorites/count — Get Favorites Count

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/favorites-and-wishlist#get-get-favorites-count)

Retrieves the total count of items in the user's favorites list. Useful for UI badges and quick statistics.

- **Required:** Valid Bearer token for user identification

### Features
- Fast count retrieval
- No pagination overhead
- Perfect for UI badges
- Minimal data transfer

### Response Data
- Total favorites count
- Last update timestamp

### Use Cases
- Navigation badge numbers
- Dashboard statistics
- Quick UI updates
- Mobile app synchronization
- Performance monitoring

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/favorites/count

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Response 200 Success - Favorites Count

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "count": 15,
    "last_updated": "2023-12-01T18:00:00Z"
  }
}
```

### Response 200 Success - Zero Favorites

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "count": 0,
    "last_updated": null
  }
}
```

## GET /storefront/reviews — List Reviews (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/review-management#get-list-reviews-admin)

Retrieves reviews for admin moderation with advanced filtering and pagination. Only accessible by tenant administrators for review management.

- **Required:** Valid Bearer token with tenant admin permissions

### Search & Filter Features
- **Content Search:** Find reviews by title or comment text
- **Rating Filtering:** Single, multi-select, or range (min/max)
- **Product/Variant Filtering:** Use `product_id` and/or `variant_id`
- **Status Filtering:** Single or multiple statuses
- **Date Range:** Filter by `created_at` interval
- **Combination Filters:** Use multiple filters together for precise results

### Features
- Admin-only access for moderation
- Advanced search and filtering capabilities
- Status-based filtering for workflow management
- Content-based search for quality control
- Rating-based filtering for sentiment analysis
- Product/variant-specific review management
- Paginated results for large review volumes
- Complete review details with user information
- Product context for moderation decisions
- Moderation history tracking
- Order verification for purchase validation

### Review Statuses
- **Pending:** Newly submitted, awaiting moderation
- **Approved:** Reviewed and approved for public display
- **Rejected:** Reviewed and rejected (not shown publicly)

### Response Data
- Complete review details with ratings
- User information (anonymized if needed)
- Product variant details for context
- Order information for purchase verification
- Submission and moderation timestamps
- Admin responses and moderation history

### Use Cases
- Review moderation dashboard
- Quality control workflows
- Bulk review processing
- Content management systems
- Customer service oversight
- Product quality analysis
- Sentiment monitoring
- Spam detection workflows

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/reviews?page=1&limit=10&status=pending&search=great product&rating=5&product_id=product-uuid-123&variant_id=variant-uuid-123&lang=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Items per page (default: 10, max: 100) | 10 |
| status | query string | No | Filter by review status: pending, approved, rejected. Default when omitted: pending Filter by single moderation status (pending, approved, rejected) | pending |
| statuses | query string | No | Filter by multiple statuses (comma-separated): pending, approved, rejected Comma-separated list of statuses (takes precedence over `status`) | approved,pending |
| search | query string | No | Search reviews by title or comment content | great product |
| lang | query string | No | Language code for multilingual search on title/comment (default: context lang) Language for multilingual fields search (default: context lang) | en |
| rating | query string | No | Filter reviews by specific rating (1-5 stars) Filter by a single rating (1-5) | 5 |
| ratings | query string | No | Filter by multiple ratings (comma-separated) Comma-separated list of ratings (1-5) | 4,5 |
| min_rating | query string | No | Minimum rating (inclusive) | 3 |
| max_rating | query string | No | Maximum rating (inclusive) | 5 |
| start_date | query string | No | Start of created_at date range (ISO 8601) Start of `created_at` range (ISO 8601) | 2025-01-01T00:00:00Z |
| end_date | query string | No | End of created_at date range (ISO 8601) End of `created_at` range (ISO 8601) | 2025-12-31T23:59:59Z |
| product_id | query string | No | Filter reviews for specific product | product-uuid-123 |
| variant_id | query string | No | Filter reviews for specific variant | variant-uuid-123 |



### Response 200 Success - Reviews List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "reviews": [
    {
      "id": "review-uuid-1",
      "variant_id": "variant-uuid-123",
      "user_id": "user-uuid-456",
      "order_id": "order-uuid-789",
      "rating": 5,
      "title": {
        "en": "Excellent product!",
        "ar": "منتج ممتاز!",
        "ku": "بەرهەمێکی نایاب!"
      },
      "comment": {
        "en": "Really happy with this purchase. Great quality and fast delivery.",
        "ar": "سعيد جداً بهذا الشراء. جودة رائعة وتسليم سريع.",
        "ku": "زۆر دڵخۆشم بەم کڕینە. کوالیتی باش و گەیاندنی خێرا."
      },
      "status": "pending",
      "admin_response": null,
      "created_at": "2023-12-01T10:00:00Z",
      "updated_at": "2023-12-01T10:00:00Z",
      "user": {
        "id": "user-uuid-456",
        "full_name": "John Doe",
        "email": "john****@example.com"
      },
      "variant": {
        "id": "variant-uuid-123",
        "name": {
          "en": "iPhone 15 Pro - 256GB",
          "ar": "آيفون 15 برو - 256 جيجابايت",
          "ku": "ئایفۆن 15 پرۆ - 256 گیگابایت"
        },
        "price": 999.99,
        "storefront_products": {
          "id": "product-uuid-123",
          "name": {
            "en": "iPhone 15 Pro",
            "ar": "آيفون 15 برو",
            "ku": "ئایفۆن 15 پرۆ"
          }
        }
      },
      "order": {
        "id": "order-uuid-789",
        "status": "delivered"
      },
      "is_approved": false,
      "is_pending": true,
      "is_rejected": false
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 1,
    "totalPages": 1
  }
}
```

### Response 200 Success - Empty Results

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "reviews": [],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 0,
    "totalPages": 0
  }
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## PUT /storefront/reviews/:review_id/moderate — Moderate Review

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/review-management#put-moderate-review)

Approves or rejects a review submission. Only accessible by tenant administrators with moderation capabilities.

- **Required:** Valid Bearer token with tenant admin permissions

### Moderation Actions
- **approve:** Makes the review visible to public users
- **reject:** Hides the review from public display

### Admin Response
- Multi-language support for admin responses
- Optional field for public admin replies
- Displayed alongside approved reviews

### Features
- Audit trail with moderator identification
- Multi-language admin responses
- Status change logging
- Notification triggers (if configured)
- Reversible moderation decisions

### Business Rules
- Only pending reviews can be moderated
- Moderation action is logged with timestamp
- Moderator ID is recorded for accountability
- Status changes are permanent until re-moderated
- Admin responses are public when review is approved

### Response Data
- Updated review with new status
- Moderation timestamp and moderator info
- Admin response (if provided)

### Use Cases
- Quality control workflows
- Content moderation
- Review approval processes
- Spam and inappropriate content filtering
- Customer service dispute resolution
- Public admin responses to reviews

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/reviews/:review_id/moderate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| review_id | path string | Yes | UUID of the review to moderate | review-uuid-123 |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| action | string | Yes | Moderation action ('approve' or 'reject') |  |
| admin_response | object | No | Multi-language admin response to the review (applies to both approve and reject) |  |
| admin_response.en | string |  |  |  |
| admin_response.ar | string |  |  |  |
| admin_response.ku | string |  |  |  |



### Request body (raw)

```json
{
  "action": "approve",
  "admin_response": {
    "en": "Thank you for your detailed review!",
    "ar": "شكراً لك على مراجعتك المفصلة!",
    "ku": "سوپاس بۆ پێداچوونەوە وردەکەت!"
  }
}
```

### Response 200 Success - Review Approved

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "review": {
    "id": "review-uuid-123",
    "variant_id": "variant-uuid-456",
    "user_id": "user-uuid-789",
    "order_id": "order-uuid-101",
    "rating": 5,
    "title": {
      "en": "Excellent product!"
    },
    "comment": {
      "en": "Really happy with this purchase. Great quality and fast delivery."
    },
    "status": "approved",
    "admin_response": {
      "en": "Thank you for your detailed review!",
      "ar": "شكراً لك على مراجعتك المفصلة!"
    },
    "created_at": "2023-12-01T10:00:00Z",
    "updated_at": "2023-12-01T18:00:00Z",
    "is_approved": true,
    "is_pending": false,
    "is_rejected": false
  },
  "message": "Review approved successfully"
}
```

### Response 200 Success - Review Rejected

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "review": {
    "id": "review-uuid-456",
    "status": "rejected",
    "admin_response": {
      "en": "Review contains inappropriate language and does not provide constructive feedback."
    },
    "updated_at": "2023-12-01T18:15:00Z",
    "is_approved": false,
    "is_pending": false,
    "is_rejected": true
  },
  "message": "Review rejected successfully"
}
```

### Response 404 Error - Review Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Review not found"
}
```

### Response 400 Error - Invalid Action

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation failed",
  "errors": [
    {
      "field": "action",
      "message": "Action must be 'approve' or 'reject'"
    }
  ]
}
```

## POST /storefront/reviews/bulk-moderate — Bulk Moderate Reviews (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/review-management#post-bulk-moderate-reviews-admin)

Bulk approve or reject multiple reviews. Only reviews with status 'pending' are updated.

- **Required:** Valid Bearer token with tenant admin permissions

### Endpoints
- `POST /storefront/reviews/bulk-moderate`

### Common Fields
- `action` (required): 'approve' | 'reject'
- `admin_response` (optional): multilingual map to set admin_response directly

### Supported Filters
- `ratings`: array of integers 1-5
- `rating`: single integer 1-5
- `min_rating` / `max_rating`
- `start_date` / `end_date` (ISO 8601)
- `product_id` (preferred) or legacy `catalog_item_id`
- `variant_id`

### Notes
- Only 'pending' reviews are modified
- Selection is scoped to the current tenant's market
- Response reports matched vs updated counts

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/reviews/bulk-moderate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| action | string |  |  |  |
| review_ids | array<string> |  | (array of UUIDs): Explicit list of review IDs |  |
| filters |  |  | (object): Criteria to select reviews |  |



### Request body (raw)

```json
{
  "action": "approve",
  "review_ids": [
    "review-uuid-1",
    "review-uuid-2"
  ]
}
```

### Response 200 Success - Bulk Approve by IDs

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "action": "approve",
  "matched": 2,
  "updated": 2
}
```

### Response 200 Success - Bulk Reject by Filters

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "action": "reject",
  "matched": 12,
  "updated": 10
}
```

## POST /storefront/reviews/bulk-moderate — Bulk Moderate Reviews by Filters (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/review-management#post-bulk-moderate-reviews-by-filters-admin)

Bulk approve or reject using filter criteria. Only reviews with status 'pending' are updated.

  - `product_id` (preferred) or legacy `catalog_item_id`

Response returns matched and updated counts.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/reviews/bulk-moderate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| action | string | Yes | 'approve' \| 'reject' |  |
| admin_response | object | No | multilingual map applied to affected reviews |  |
| admin_response.en | string |  |  |  |
| filters | object |  | (required if no `review_ids`) |  |
| filters.ratings | array<number> |  | array of integers 1-5 |  |
| filters.min_rating | number |  | inclusive bounds |  |
| filters.max_rating | number |  | inclusive bounds |  |
| filters.start_date | string |  | ISO timestamps on `created_at` |  |
| filters.end_date | string |  | ISO timestamps on `created_at` |  |
| filters.product_id | string |  |  |  |
| filters.variant_id | string |  |  |  |
| filters.rating |  |  | single integer 1-5 |  |



### Request body (raw)

```json
{
  "action": "reject",
  "admin_response": {
    "en": "Fails moderation policy"
  },
  "filters": {
    "ratings": [
      1,
      2
    ],
    "min_rating": 1,
    "max_rating": 2,
    "start_date": "2025-01-01T00:00:00Z",
    "end_date": "2025-12-31T23:59:59Z",
    "product_id": "product-uuid-123",
    "variant_id": "variant-uuid-123"
  }
}
```

### Response 200 Success - Filtered Reject

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "action": "reject",
  "matched": 7,
  "updated": 7
}
```

## POST /storefront/reviews — Submit Review

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/review-management#post-submit-review)

Allows customers to submit reviews for products they have purchased and received. Client submits `variant_id` for provenance; server derives and stores `product_id` for product-level display/aggregation.

- **Required:** Valid Bearer token for customer identification

### Purchase Verification
- System validates user has a delivered order containing the variant
- Review is linked to the order for verification

### Features
- Verified purchase enforcement
- Rating validation (1-5 stars)
- Multi-language content support
- Duplicate review prevention (one review per user per product)
- Auto-moderation support (pending/approved)
- Review submission tracking

### Business Rules
- User must be authenticated
- User must have a delivered order containing the variant
- Rating must be between 1-5
- One review per product per user (variant_id is just provenance)
- Content must pass validation

### Response Data
- Created review with moderation status
- `product_id` stored for product-level aggregation
- Linked order information
- Submission timestamp
- Review ID for tracking

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/reviews

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| variant_id | string | Yes | UUID of the product variant being reviewed (provenance) |  |
| rating | number | Yes | Rating from 1-5 stars |  |
| title | string | No | Short review title (max 100 characters) |  |
| comment | string | No | Detailed review text (max 1000 characters) |  |



### Request body (raw)

```json
{
  "variant_id": "variant-uuid-123",
  "rating": 4,
  "title": "Good product overall",
  "comment": "The product works well and meets my expectations. Delivery was quick and packaging was secure. Would recommend to others."
}
```

### Response 201 Success - Review Submitted

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "review": {
    "id": "review-uuid-new",
    "variant_id": "variant-uuid-123",
    "user_id": "user-uuid-789",
    "order_id": "order-uuid-456",
    "rating": 4,
    "title": {
      "en": "Good product overall"
    },
    "comment": {
      "en": "The product works well and meets my expectations. Delivery was quick and packaging was secure. Would recommend to others."
    },
    "status": "pending",
    "admin_response": null,
    "created_at": "2023-12-01T19:00:00Z",
    "updated_at": "2023-12-01T19:00:00Z",
    "is_approved": false,
    "is_pending": true,
    "is_rejected": false
  },
  "message": "Review submitted successfully and is pending moderation"
}
```

### Response 403 Error - Cannot Review Unordered Product

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "You can only review products that you have ordered and received"
}
```

### Response 409 Error - Duplicate Review

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "You have already reviewed this product variant"
}
```

### Response 400 Error - Validation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation failed",
  "errors": [
    {
      "field": "rating",
      "message": "Rating must be between 1 and 5"
    }
  ]
}
```

## GET /storefront/products/:product_id/reviews — Get Product Reviews (Public)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/review-management#get-get-product-reviews-public)

Retrieves approved reviews for all variants of a specific product. Public endpoint that aggregates reviews from all product variants and shows only approved reviews with rating statistics.

- **Optional:** No authentication required for public access

### Features
- Public access to approved reviews only
- Aggregates reviews from all product variants
- Comprehensive rating statistics
- Rating distribution breakdown
- Purchase verification indicators
- User anonymization for privacy
- Multi-language content support

### Response Data
- Only approved reviews from all variants
- Average rating calculation across all variants
- Rating count by stars (1-5)
- Total review count
- User information (anonymized)
- Variant information for context
- Purchase verification status
- Admin responses (if any)

### Use Cases
- Product page review display
- Rating and review widgets
- Customer decision support
- SEO content for product pages
- Social proof display
- Product comparison features

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/products/:product_id/reviews?page=1&limit=10

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| product_id | path string | Yes | UUID of the product to get reviews for | product-uuid-123 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Items per page (default: 10, max: 100) | 10 |



### Response 200 Success - Product Reviews with Statistics

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "review_data": {
    "reviews": [
      {
        "id": "review-uuid-1",
        "variant_id": "variant-uuid-123",
        "user_id": "user-uuid-456",
        "order_id": "order-uuid-789",
        "rating": 5,
        "title": {
          "en": "Excellent product!"
        },
        "comment": {
          "en": "Really happy with this purchase. Great quality and fast delivery."
        },
        "status": "approved",
        "admin_response": {
          "en": "Thank you for your feedback!"
        },
        "created_at": "2023-12-01T10:00:00Z",
        "updated_at": "2023-12-01T11:00:00Z",
        "user": {
          "id": "user-uuid-456",
          "full_name": "John D.",
          "email": "john****@example.com"
        },
        "variant": {
          "id": "variant-uuid-123",
          "name": {
            "en": "iPhone 15 Pro - 256GB"
          },
          "price": 999.99,
          "storefront_products": {
            "id": "product-uuid-123",
            "name": {
              "en": "iPhone 15 Pro",
              "ar": "آيفون 15 برو"
            }
          }
        },
        "is_approved": true,
        "is_pending": false,
        "is_rejected": false
      },
      {
        "id": "review-uuid-2",
        "variant_id": "variant-uuid-456",
        "user_id": "user-uuid-789",
        "order_id": "order-uuid-101",
        "rating": 4,
        "title": {
          "en": "Good value for money"
        },
        "comment": {
          "en": "Product works as expected. Minor issues with setup but overall satisfied."
        },
        "status": "approved",
        "admin_response": null,
        "created_at": "2023-11-28T15:30:00Z",
        "updated_at": "2023-11-28T16:00:00Z",
        "user": {
          "id": "user-uuid-789",
          "full_name": "Sarah M.",
          "email": "sarah****@example.com"
        },
        "variant": {
          "id": "variant-uuid-456",
          "name": {
            "en": "iPhone 15 Pro - 512GB"
          },
          "price": 1199.99,
          "storefront_products": {
            "id": "product-uuid-123",
            "name": {
              "en": "iPhone 15 Pro",
              "ar": "آيفون 15 برو"
            }
          }
        },
        "is_approved": true,
        "is_pending": false,
        "is_rejected": false
      }
    ],
    "statistics": {
      "total_reviews": 2,
      "average_rating": 4.5,
      "rating_counts": {
        "1": 0,
        "2": 0,
        "3": 0,
        "4": 1,
        "5": 1
      }
    },
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 2,
      "totalPages": 1
    }
  }
}
```

### Response 200 Success - No Reviews

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "review_data": {
    "reviews": [],
    "statistics": {
      "total_reviews": 0,
      "average_rating": 0,
      "rating_counts": {
        "1": 0,
        "2": 0,
        "3": 0,
        "4": 0,
        "5": 0
      }
    },
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 0,
      "totalPages": 0
    }
  }
}
```

## GET /storefront/variants/:variant_id/reviews — Get Variant Reviews (Public)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/review-management#get-get-variant-reviews-public)

Retrieves approved reviews for a specific product variant. Public endpoint that shows only approved reviews with rating statistics specific to this variant.

- **Optional:** No authentication required for public access

### Features
- Public access to approved reviews only
- Variant-specific review filtering
- Comprehensive rating statistics for this variant
- Rating distribution breakdown
- Purchase verification indicators
- User anonymization for privacy
- Multi-language content support
- Admin responses display

### Response Data
- Only approved reviews for this specific variant
- Average rating calculation for this variant
- Rating count by stars (1-5) for this variant
- Total review count for this variant
- User information (anonymized)
- Variant information for context
- Purchase verification status
- Admin responses (if any)

### Use Cases
- Variant-specific review display
- Detailed product variant analysis
- Variant comparison features
- Specific variant decision support
- Variant-level rating widgets

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/variants/:variant_id/reviews?page=1&limit=10

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| variant_id | path string | Yes | UUID of the product variant to get reviews for | variant-uuid-123 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Items per page (default: 10, max: 100) | 10 |



### Response 200 Success - Variant Reviews with Statistics

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "review_data": {
    "reviews": [
      {
        "id": "review-uuid-1",
        "variant_id": "variant-uuid-123",
        "user_id": "user-uuid-456",
        "order_id": "order-uuid-789",
        "rating": 5,
        "title": {
          "en": "Excellent variant!",
          "ar": "نموذج ممتاز!"
        },
        "comment": {
          "en": "This specific variant works perfectly. The 256GB storage is exactly what I needed.",
          "ar": "هذا النموذج المحدد يعمل بشكل مثالي. مساحة التخزين 256 جيجابايت هي بالضبط ما احتجته."
        },
        "status": "approved",
        "admin_response": {
          "en": "Thank you for the detailed feedback on this variant!"
        },
        "created_at": "2023-12-01T10:00:00Z",
        "updated_at": "2023-12-01T11:00:00Z",
        "user": {
          "id": "user-uuid-456",
          "full_name": "John D.",
          "email": "john****@example.com"
        },
        "variant": {
          "id": "variant-uuid-123",
          "name": {
            "en": "iPhone 15 Pro - 256GB",
            "ar": "آيفون 15 برو - 256 جيجابايت"
          },
          "price": 999.99,
          "storefront_products": {
            "id": "product-uuid-123",
            "name": {
              "en": "iPhone 15 Pro",
              "ar": "آيفون 15 برو"
            }
          }
        },
        "is_approved": true,
        "is_pending": false,
        "is_rejected": false
      }
    ],
    "statistics": {
      "total_reviews": 1,
      "average_rating": 5,
      "rating_counts": {
        "1": 0,
        "2": 0,
        "3": 0,
        "4": 0,
        "5": 1
      }
    },
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

### Response 200 Success - No Reviews for Variant

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "review_data": {
    "reviews": [],
    "statistics": {
      "total_reviews": 0,
      "average_rating": 0,
      "rating_counts": {
        "1": 0,
        "2": 0,
        "3": 0,
        "4": 0,
        "5": 0
      }
    },
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 0,
      "totalPages": 0
    }
  }
}
```

## GET /storefront/variants/:variant_id/review-eligibility — Check Review Eligibility

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/review-management#get-check-review-eligibility)

Checks whether the current user is eligible to review the given variant.

### Rules
- Authentication required
- Must have a delivered order containing the variant
- Must not have already reviewed it

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/variants/:variant_id/review-eligibility

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| variant_id | path string | Yes | UUID of the product variant to check | variant-uuid-123 |



### Response 200 Success - Eligible

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "eligible": true,
  "reason": null,
  "order_id": "order-uuid-789"
}
```

### Response 200 Success - Already Reviewed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "eligible": false,
  "reason": "already_reviewed"
}
```

### Response 200 Success - Not Purchased

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "eligible": false,
  "reason": "not_purchased"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "eligible": false,
  "reason": "authentication_required",
  "message": "Authentication required"
}
```

## GET /storefront/products/:product_id/review-eligibility — Check Product Review Eligibility

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/review-management#get-check-product-review-eligibility)

Checks whether the current user is eligible to review the given product (any variant).

### Rules
- Authentication required
- Must have a delivered order containing any variant of the product
- Must not have already reviewed any variant of the product

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/products/:product_id/review-eligibility

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| product_id | path string | Yes | UUID of the product to check | product-uuid-123 |



### Response 200 Success - Eligible

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "eligible": true,
  "reason": null,
  "order_id": "order-uuid-789",
  "purchased_at": "2023-12-01T10:00:00Z"
}
```

### Response 200 Success - Already Reviewed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "eligible": false,
  "reason": "already_reviewed"
}
```

### Response 200 Success - Not Purchased

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "eligible": false,
  "reason": "not_purchased"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "eligible": false,
  "reason": "authentication_required",
  "message": "Authentication required"
}
```

## POST /storefront/orders/confirm — Confirm Order with Address and Payment

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#post-confirm-order-with-address-and-payment)

Creates and confirms a storefront order from the current cart by attaching the shipping address, selected delivery service, and payment method.

### Key rules
- Requires `cartId`, `marketId`, `deliveryServiceId`, and `paymentMethodTypeId`.
- Accepts either `addressId` or an `address` JSON object with lat/long/country.
- The backend re-calculates and persists the quoted delivery fee during order creation, before approval-time delivery creation.
- When `save_to_address_book` is false and `address` is provided, the address is normalized but not saved; it is embedded in the order as JSON.
- For gateway payments, `backRef` (or `back_ref`) is required.

Valid user credentials required.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/orders/confirm

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| cartId | string |  |  |  |
| marketId | string |  |  |  |
| deliveryServiceId | string |  |  |  |
| address | object |  |  |  |
| address.full_address | string |  |  |  |
| address.lat | number |  |  |  |
| address.long | number |  |  |  |
| address.country | string |  |  |  |
| address.contact_info | object |  |  |  |
| address.contact_info.phone | string |  |  |  |
| address.contact_info.email | string |  |  |  |
| save_to_address_book | boolean |  |  |  |
| paymentMethodTypeId | string |  |  |  |
| backRef | string |  |  |  |



### Request body (raw)

```json
{
  "cartId": "{{cart_id}}",
  "marketId": "{{market_id}}",
  "deliveryServiceId": "{{delivery_service_id}}",
  "address": {
    "full_address": "Customer Address",
    "lat": 33.3152,
    "long": 44.3661,
    "country": "IQ",
    "contact_info": {
      "phone": "+964700000000",
      "email": "user@example.com"
    }
  },
  "save_to_address_book": false,
  "paymentMethodTypeId": "{{payment_method_id}}",
  "backRef": "https://example.com/payment-return"
}
```

### Response 200 Success - COD Payment

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "paymentType": "COD",
  "order": {
    "id": "order-uuid-123",
    "marketId": "market-uuid-abc",
    "userId": "user-uuid-456",
    "status": "pending",
    "paymentStatus": "pending",
    "items": [
      {
        "item_id": "variant-uuid-789",
        "quantity": 2,
        "price": 29.99,
        "name": {
          "en": "Blue T-Shirt Large",
          "ar": "تيشيرت أزرق كبير",
          "ku": "کراسی شینی گەورە"
        }
      }
    ],
    "totalPrice": 59.98,
    "paymentMethod": "cod",
    "shippingAddress": {
      "id": "address-uuid-456",
      "street": "123 Main Street",
      "city": "New York",
      "state": "NY",
      "country": "USA",
      "zipCode": "10001",
      "phone": "+1234567890"
    },
    "createdAt": "2024-01-15T10:30:00Z",
    "updatedAt": "2024-01-15T10:35:00Z"
  },
  "message": "Order confirmed successfully - payment will be collected on delivery",
  "instructions": "Please have exact cash ready when the order is delivered"
}
```

### Response 200 Success - Wallet Payment

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "paymentType": "WALLET",
  "order": {
    "id": "order-uuid-123",
    "marketId": "market-uuid-abc",
    "userId": "user-uuid-456",
    "status": "approved",
    "paymentStatus": "paid",
    "items": [
      {
        "item_id": "variant-uuid-789",
        "quantity": 1,
        "price": 149.99,
        "name": {
          "en": "Wireless Headphones"
        }
      }
    ],
    "totalPrice": 149.99,
    "paymentMethod": "wallet",
    "shippingAddress": {
      "id": "address-uuid-456",
      "street": "456 Oak Avenue",
      "city": "Los Angeles",
      "state": "CA",
      "country": "USA",
      "zipCode": "90210"
    },
    "createdAt": "2024-01-15T10:30:00Z",
    "updatedAt": "2024-01-15T10:35:00Z"
  },
  "walletBalance": 850.01,
  "message": "Order confirmed and paid from wallet successfully"
}
```

### Response 200 Success - Gateway Payment

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "paymentType": "GATEWAY",
  "order": {
    "id": "order-uuid-123",
    "marketId": "market-uuid-abc",
    "userId": "user-uuid-456",
    "status": "pending",
    "paymentStatus": "pending",
    "items": [
      {
        "item_id": "variant-uuid-789",
        "quantity": 1,
        "price": 999.99,
        "name": {
          "en": "Premium Smartphone"
        }
      }
    ],
    "totalPrice": 999.99,
    "paymentMethod": "gateway",
    "shippingAddress": {
      "id": "address-uuid-456",
      "street": "789 Pine Street",
      "city": "Chicago",
      "state": "IL",
      "country": "USA",
      "zipCode": "60601"
    },
    "createdAt": "2024-01-15T10:30:00Z",
    "updatedAt": "2024-01-15T10:35:00Z"
  },
  "redirectUrl": "https://checkout.razorpay.com/v1/checkout.js",
  "paymentData": {
    "orderId": "rzp_order_AbCdEfGhIjKlMn",
    "amount": 999.99,
    "currency": "USD",
    "keyId": "rzp_test_1234567890",
    "checkoutUrl": "{\"key\":\"rzp_test_1234567890\",\"amount\":99999,\"currency\":\"USD\",\"name\":\"Ondi Store\",\"description\":\"Payment for order order-uuid-123\",\"order_id\":\"rzp_order_AbCdEfGhIjKlMn\"}"
  },
  "message": "Please proceed to payment gateway to complete the order"
}
```

### Response 404 Error - Order Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Order not found or does not belong to user"
}
```

### Response 400 Error - Order Cannot Be Modified

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Order cannot be modified - only pending orders can be confirmed"
}
```

### Response 404 Error - Address Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Shipping address not found or does not belong to user"
}
```

### Response 400 Error - Insufficient Wallet Balance

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient balance. Available: 50.00 USD",
  "error": "Insufficient wallet balance"
}
```

### Response 400 Error - Missing Required Fields

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Missing required fields",
  "required": [
    "cartId",
    "marketId",
    "deliveryServiceId",
    "addressId_or_address",
    "paymentMethodTypeId"
  ]
}
```

## GET /storefront/orders/{{order_id}}/payment-status — Get Order Payment Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#get-get-order-payment-status)

Retrieves order details with current payment status and transaction information. Only returns orders belonging to the authenticated user.

- **Required:** Valid user authentication

- **order_id** (required): UUID of the order to check

### Features
- Real-time order and payment status
- Complete order details including items and shipping
- Payment transaction history
- Market information
- User access control (own orders only)

### Order Statuses
- **pending**: Order created but payment not completed
- **approved**: Payment completed, ready for processing
- **shipped**: Order has been shipped
- **delivered**: Order delivered to customer
- **cancelled**: Order cancelled or payment failed

### Payment Statuses
- **pending**: Payment not yet completed
- **paid**: Payment successfully completed
- **failed**: Payment failed or was rejected

### Use Cases
- Check payment completion after gateway redirect
- Order status tracking for customers
- Payment confirmation pages
- Customer support queries
- Real-time order updates

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/orders/{{order_id}}/payment-status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success - Order with Payment Details

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "order": {
    "id": "order-uuid-123",
    "status": "approved",
    "paymentStatus": "paid",
    "totalPrice": 299.99,
    "paymentMethod": "gateway",
    "items": [
      {
        "item_id": "variant-uuid-789",
        "quantity": 2,
        "price": 149.99,
        "name": {
          "en": "Premium Wireless Mouse",
          "ar": "فأرة لاسلكية متميزة",
          "ku": "مشکی بێتەل باش"
        },
        "variant": {
          "color": "Black",
          "size": "Standard"
        }
      }
    ],
    "shippingAddress": {
      "id": "address-uuid-456",
      "street": "123 Main Street",
      "city": "New York",
      "state": "NY",
      "country": "USA",
      "zipCode": "10001",
      "phone": "+1234567890",
      "firstName": "John",
      "lastName": "Doe"
    },
    "createdAt": "2024-01-15T10:30:00Z",
    "updatedAt": "2024-01-15T10:45:00Z",
    "market": {
      "name": "TechMart Electronics",
      "settings": {
        "currency": "USD",
        "deliveryFee": 5.99
      }
    },
    "payments": [
      {
        "id": "payment-uuid-789",
        "status": "SUCCESS",
        "payment_method": "Credit/Debit Card",
        "amount": 299.99,
        "gateway_transaction_id": "rzp_pay_AbCdEfGhIjKlMn",
        "created_at": "2024-01-15T10:30:00Z",
        "processed_at": "2024-01-15T10:32:15Z"
      }
    ]
  }
}
```

### Response 200 Success - COD Order

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "order": {
    "id": "order-uuid-456",
    "status": "pending",
    "paymentStatus": "pending",
    "totalPrice": 79.99,
    "paymentMethod": "cod",
    "items": [
      {
        "item_id": "variant-uuid-123",
        "quantity": 1,
        "price": 79.99,
        "name": {
          "en": "Cotton T-Shirt",
          "ar": "تيشيرت قطني",
          "ku": "کراسی لۆکە"
        }
      }
    ],
    "shippingAddress": {
      "street": "456 Oak Avenue",
      "city": "Los Angeles",
      "state": "CA",
      "country": "USA",
      "zipCode": "90210"
    },
    "createdAt": "2024-01-15T09:30:00Z",
    "updatedAt": "2024-01-15T09:35:00Z",
    "market": {
      "name": "Fashion Store",
      "settings": {
        "currency": "USD"
      }
    },
    "payments": []
  }
}
```

### Response 200 Success - Wallet Order

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "order": {
    "id": "order-uuid-789",
    "status": "approved",
    "paymentStatus": "paid",
    "totalPrice": 199.99,
    "paymentMethod": "wallet",
    "items": [
      {
        "item_id": "variant-uuid-999",
        "quantity": 1,
        "price": 199.99,
        "name": {
          "en": "Bluetooth Speaker"
        }
      }
    ],
    "shippingAddress": {
      "street": "789 Pine Street",
      "city": "Chicago",
      "state": "IL",
      "country": "USA",
      "zipCode": "60601"
    },
    "createdAt": "2024-01-15T11:30:00Z",
    "updatedAt": "2024-01-15T11:35:00Z",
    "market": {
      "name": "Electronics Hub",
      "settings": {
        "currency": "USD"
      }
    },
    "payments": [
      {
        "id": "payment-uuid-999",
        "status": "SUCCESS",
        "payment_method": "Digital Wallet",
        "amount": 199.99,
        "gateway_transaction_id": "wallet_order-uuid-789_1705320900000",
        "created_at": "2024-01-15T11:30:00Z",
        "processed_at": "2024-01-15T11:30:05Z"
      }
    ]
  }
}
```

### Response 404 Error - Order Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Order not found or does not belong to user"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Authentication required"
}
```

## GET /storefront/orders — List Orders (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#get-list-orders-admin-2)

Retrieves orders for tenant admin management with pagination, additive top-level filters, advanced column filters, server-side sorting, and market KPI stats. Only accessible by tenant administrators for order oversight.

- **Required:** Valid Bearer token with tenant admin permissions

### Important
- Existing query params remain unchanged.
- `column_filters` are AND-combined with each other and with existing filters.
- `stats` are always global market totals and are never affected by filters, search, sorting, or pagination.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/orders?page=1&limit=10&status=pending&search=ord-123&date_from=2024-01-01&date_to=2024-12-31&min_amount=50&max_amount=1000

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Items per page (default: 10, max: 100) | 10 |
| status | query string | No | Filter by order status: pending, confirmed, cancelled, completed Existing exact status filter | pending |
| search | query string | No | Search orders by order number or order ID (partial match). Order number format: M{market_code}-{YY}-{MM}{DD}{seq4} Existing search by order number or order ID | M1000-25-12210047 |
| date_from | query string | No | Filter orders from this date (YYYY-MM-DD format) / `date_to` (optional): Existing created-at range filters | 2024-01-01 |
| date_to | query string |  | Filter orders until this date (YYYY-MM-DD format) | 2024-12-31 |
| min_amount | query string | No | Filter orders with minimum total amount / `max_amount` (optional): Existing final amount range filters | 50 |
| max_amount | query string |  | Filter orders with maximum total amount | 1000 |
| column_filters | query string | No | Advanced column filters JSON. Supported keys: status, paymentMethod, city, finalAmount, deliveryFee, discountAmount, totalItems, createdAt JSON string. Supported keys: `status`, `paymentMethod`, `city`, `finalAmount`, `deliveryFee`, `discountAmount`, `totalItems`, `createdAt` | {"status":"awaiting_approval","paymentMethod":"COD","city":"Baghdad","totalItems":{"min":2}} |
| sort_by | query string | No | Sort field. Allowed: createdAt, finalAmount, deliveryFee, discountAmount, totalItems, status | createdAt |
| sort_order | query string | No | Sort order: asc or desc. Defaults to desc `asc` or `desc` (default `desc`) | desc |



### Response 200 Success - Orders List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "orders": [
    {
      "id": "order-uuid-1",
      "order_number": "M1000-25-12010001",
      "status": "awaiting_approval",
      "paymentMethod": "COD",
      "paymentStatus": "pending",
      "shippingAddress": {
        "city": "Baghdad"
      },
      "deliveryFee": 5000,
      "discountAmount": 1000,
      "finalAmount": 24000,
      "totalItems": 3,
      "createdAt": "2026-04-23T09:30:00.000Z",
      "updatedAt": "2026-04-23T09:30:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 10,
    "total": 1284,
    "totalPages": 129
  },
  "stats": {
    "totalOrders": 1284,
    "pendingOrders": 42,
    "outForDelivery": 18,
    "deliveredToday": 156
  }
}
```

## GET /storefront/orders/:order_id — Get Order Details

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#get-get-order-details)

Retrieves complete details for a specific order. Includes all order items, customer information, payment details, and delivery status.

- **Required:** Valid Bearer token with tenant admin permissions

### Features
- Complete order information
- Detailed item breakdown
- Customer and shipping details
- Payment status and history
- Delivery tracking information
- Order status timeline

### Response Data
- Full order details with all items
- Customer contact information
- Complete shipping address
- Payment method and transaction details
- Order status history
- Estimated and actual delivery dates
- Individual item details with pricing

### Use Cases
- Order detail view in admin panel
- Customer service inquiries
- Order fulfillment processing
- Payment verification
- Delivery coordination

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/orders/:order_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | UUID of the order to retrieve | order-uuid-123 |



### Response 200 Success - Order Details

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "order-uuid-123",
      "order_number": "M1000-25-12010001",
      "user_id": "user-uuid-456",
      "market_id": "market-uuid-abc",
      "status": "confirmed",
      "payment_method": "cod",
      "payment_status": "pending",
      "subtotal": 1999.98,
      "tax_amount": 0,
      "shipping_cost": 15,
      "total_price": 2014.98,
      "currency": "USD",
      "customer_notes": "Please handle with care",
      "created_at": "2023-12-01T10:00:00Z",
      "updated_at": "2023-12-01T16:00:00Z",
      "customer": {
        "id": "user-uuid-456",
        "first_name": "John",
        "last_name": "Doe",
        "email": "john.doe@example.com",
        "phone": "+1234567890",
        "verified_customer": true
      },
      "shipping_address": {
        "title": "Home",
        "address_line1": "123 Main Street",
        "address_line2": "Apt 4B",
        "city": "New York",
        "state": "NY",
        "postal_code": "10001",
        "country": "US",
        "phone": "+1234567890",
        "special_instructions": "Leave at the door"
      },
      "items": [
        {
          "id": "order-item-uuid-1",
          "variant_id": "variant-uuid-123",
          "quantity": 2,
          "unit_price": 999.99,
          "total_price": 1999.98,
          "variant": {
            "color": "Space Gray",
            "storage": "256GB"
          },
          "product_name": {
            "en": "iPhone 15 Pro",
            "ar": "آيفون 15 برو",
            "ku": "ئایفۆن 15 پرۆ"
          },
          "product_images": [
            "https://example.com/iphone15-pro-1.jpg"
          ],
          "warehouse_item": {
            "id": "warehouse-item-uuid-456",
            "sku": "IPH15PRO-SG-256",
            "current_stock": 23
          }
        }
      ],
      "status_history": [
        {
          "status": "pending",
          "timestamp": "2023-12-01T10:00:00Z",
          "note": "Order placed"
        },
        {
          "status": "confirmed",
          "timestamp": "2023-12-01T16:00:00Z",
          "note": "Order confirmed by admin",
          "updated_by": "admin-user-uuid-789"
        }
      ],
      "estimated_delivery": {
        "date": "2023-12-05",
        "time_window": "10:00 AM - 2:00 PM",
        "method": "standard"
      },
      "delivery_assignment": {
        "id": "delivery-uuid-456",
        "driver_name": "Mike Johnson",
        "vehicle_info": "Toyota Camry - ABC123",
        "contact_phone": "+1555123456",
        "tracking_url": "https://tracking.example.com/delivery-uuid-456"
      }
    }
  }
}
```

## PUT /storefront/orders/:order_id/approve — Approve Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#put-approve-order)

Approves a pending order and moves it to confirmed status. Triggers inventory reservation and delivery assignment processes.

- **Required:** Valid Bearer token with tenant admin permissions

### Features
- Order status update to 'confirmed'
- Inventory reservation
- Delivery assignment integration
- Admin action logging
- Customer notification triggers

### Business Rules
- Only pending orders can be approved
- Stock availability validation
- Payment verification (for wallet payments)
- Admin permission verification

### Response Data
- Updated order with confirmed status
- Approval timestamp and admin info
- Updated delivery estimates
- Inventory reservation confirmation

### Use Cases
- Order approval workflow
- Manual order verification
- Fraud prevention processes
- Inventory management
- Customer service operations

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/orders/:order_id/approve

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | UUID of the order to approve | order-uuid-123 |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| admin_notes | string | No | Internal notes about the approval |  |
| estimated_delivery_date | string | No | Override estimated delivery date |  |



### Request body (raw)

```json
{
  "admin_notes": "Order approved and ready for fulfillment",
  "estimated_delivery_date": "2023-12-05"
}
```

### Response 200 Success - Order Approved

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "order-uuid-123",
      "order_number": "M1000-25-12010001",
      "status": "confirmed",
      "approved_at": "2023-12-01T18:00:00Z",
      "approved_by": "admin-user-uuid-789",
      "admin_notes": "Order approved and ready for fulfillment",
      "estimated_delivery": {
        "date": "2023-12-05",
        "time_window": "10:00 AM - 2:00 PM"
      },
      "updated_at": "2023-12-01T18:00:00Z"
    },
    "inventory_reserved": true,
    "delivery_assigned": {
      "delivery_id": "delivery-uuid-456",
      "driver_assigned": true,
      "tracking_number": "TRK-2023-001234"
    },
    "message": "Order approved successfully"
  }
}
```

## PUT /storefront/orders/:order_id/cancel — Cancel Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#put-cancel-order)

Cancels an order and handles inventory, payment, and delivery cleanup. Available for orders that haven't been shipped.

- **Required:** Valid Bearer token with tenant admin permissions

### Cancellation Reasons
- **customer_request:** Customer-initiated cancellation
- **out_of_stock:** Inventory unavailable
- **fraud:** Suspected fraudulent order
- **payment_failed:** Payment processing failed
- **other:** Other administrative reasons

### Features
- Order status update to 'cancelled'
- Inventory release/restoration
- Payment refund processing (for paid orders)
- Delivery cancellation
- Customer notification

### Business Rules
- Cannot cancel shipped or delivered orders
- Automatic inventory restoration
- Refund processing for wallet payments
- Delivery assignment cancellation

### Response Data
- Updated order with cancelled status
- Cancellation details and timestamp
- Refund processing status
- Inventory restoration confirmation

### Use Cases
- Customer service cancellations
- Inventory management
- Fraud prevention
- Payment processing issues
- Business policy enforcement

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/orders/:order_id/cancel

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | UUID of the order to cancel | order-uuid-123 |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| reason | string | Yes | Cancellation reason (customer_request, out_of_stock, fraud, other) |  |
| admin_notes | string | No | Internal notes about the cancellation |  |
| refund_required | boolean | No | Whether refund processing is needed |  |



### Request body (raw)

```json
{
  "reason": "customer_request",
  "admin_notes": "Customer requested cancellation due to change of mind",
  "refund_required": true
}
```

### Response 200 Success - Order Cancelled

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "order-uuid-123",
      "order_number": "M1000-25-12010001",
      "status": "cancelled",
      "cancelled_at": "2023-12-01T19:00:00Z",
      "cancelled_by": "admin-user-uuid-789",
      "cancellation_reason": "customer_request",
      "admin_notes": "Customer requested cancellation due to change of mind",
      "updated_at": "2023-12-01T19:00:00Z"
    },
    "inventory_restored": true,
    "refund_processed": {
      "required": true,
      "amount": 2014.98,
      "status": "processed",
      "transaction_id": "refund-txn-uuid-789"
    },
    "delivery_cancelled": {
      "delivery_id": "delivery-uuid-456",
      "status": "cancelled"
    },
    "message": "Order cancelled successfully"
  }
}
```

## GET /storefront/orders/history — Get Customer Order History

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#get-get-customer-order-history)

Retrieves order history for the authenticated customer with pagination and filtering options.

- **Required:** Valid Bearer token for customer identification

### Features
- Customer-scoped order access
- Chronological order listing
- Status-based filtering
- Simplified order summaries
- Delivery tracking information

### Response Data
- Order summaries with key details
- Status and delivery information
- Item count and total pricing
- Order dates and tracking
- Quick access to order details

### Use Cases
- Customer order history page
- Order tracking and status
- Reorder functionality
- Customer service self-help
- Mobile app order management

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/orders/history?page=1&limit=10&status=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated customers (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (default: 1) | 1 |
| limit | query string | No | Items per page (default: 10, max: 50) | 10 |
| status | query string | No | Filter by order status (optional) |  |



### Response 200 Success - Customer Order History

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "orders": [
      {
        "id": "order-uuid-123",
        "order_number": "M1000-25-12010001",
        "status": "delivered",
        "total_price": 2014.98,
        "currency": "USD",
        "items_count": 2,
        "created_at": "2023-11-28T10:00:00Z",
        "delivered_at": "2023-12-01T14:30:00Z",
        "market": {
          "id": "market-uuid-abc",
          "store_name": "TechMart",
          "logo_url": "https://example.com/techmart-logo.png"
        },
        "first_item": {
          "name": {
            "en": "iPhone 15 Pro",
            "ar": "آيفون 15 برو",
            "ku": "ئایفۆن 15 پرۆ"
          },
          "image": "https://example.com/iphone15-pro-1.jpg",
          "quantity": 2
        },
        "can_review": true,
        "can_reorder": true
      },
      {
        "id": "order-uuid-456",
        "order_number": "M1000-25-12010002",
        "status": "shipped",
        "total_price": 89.99,
        "currency": "USD",
        "items_count": 1,
        "created_at": "2023-12-01T15:00:00Z",
        "estimated_delivery": "2023-12-03",
        "tracking_number": "TRK-2023-001235",
        "market": {
          "id": "market-uuid-def",
          "store_name": "FashionHub",
          "logo_url": "https://example.com/fashionhub-logo.png"
        },
        "first_item": {
          "name": {
            "en": "Premium Cotton T-Shirt",
            "ar": "قميص قطني ممتاز",
            "ku": "کراسی لۆکسی قوتون"
          },
          "image": "https://example.com/tshirt-1.jpg",
          "quantity": 1
        },
        "can_review": false,
        "can_reorder": true
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 2,
      "totalPages": 1
    }
  }
}
```

## GET /storefront/order-groups/:group_id — Get Customer Order Group Details

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#get-get-customer-order-group-details)

Retrieves detailed information for a specific order group within a customer's order. This is useful for multi-warehouse fulfillment where an order is split into multiple groups.

- **Required:** Valid Bearer token for customer identification

### Features
- Customer-scoped access (users can only see their own order groups)
- Complete group details with all items
- Group-specific status and timeline
- Warehouse and delivery information
- Financial breakdown per group

### Multi-Warehouse Context
- Orders can be split into multiple groups when items come from different warehouses
- Each group has its own status, delivery timeline, and fulfillment process
- Customers can track each group independently
- Groups may have different delivery dates and tracking numbers

### Response Data
- Complete group details with items
- Group status and timeline
- Warehouse information
- Delivery details and tracking
- Financial breakdown (prices, fees, discounts)
- Group-specific items with quantities and pricing

### Use Cases
- Multi-warehouse order tracking
- Group-specific delivery updates
- Partial order fulfillment status
- Customer service for split orders
- Detailed order breakdown view

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/order-groups/:group_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated customers (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| group_id | path string | Yes | UUID of the order group to retrieve UUID of the specific order group to retrieve | group-uuid-456 |



### Response 200 Success - Order Group Details

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "orderGroup": {
    "id": "group-uuid-456",
    "orderId": "order-uuid-123",
    "warehouseId": "warehouse-uuid-789",
    "deliveryId": "delivery-uuid-321",
    "status": "shipped",
    "statusTimeline": [
      {
        "status": "pending",
        "timestamp": "2023-12-01T10:00:00Z",
        "note": "Group created from order"
      },
      {
        "status": "awaiting_approval",
        "timestamp": "2023-12-01T10:30:00Z",
        "note": "Payment confirmed, awaiting approval"
      },
      {
        "status": "approved",
        "timestamp": "2023-12-01T14:00:00Z",
        "note": "Order group approved by admin"
      },
      {
        "status": "shipped",
        "timestamp": "2023-12-02T09:00:00Z",
        "note": "Items shipped from warehouse"
      }
    ],
    "totalPrice": 1299.98,
    "deliveryFee": 15,
    "discountAmount": 50,
    "finalAmount": 1264.98,
    "items": [
      {
        "id": "order-item-uuid-1",
        "productVariantId": "variant-uuid-123",
        "quantity": 1,
        "unitPrice": 999.99,
        "totalPrice": 999.99,
        "product": {
          "name": {
            "en": "iPhone 15 Pro",
            "ar": "آيفون 15 برو",
            "ku": "ئایفۆن 15 پرۆ"
          },
          "images": [
            "https://example.com/iphone15-pro-1.jpg"
          ]
        },
        "variant": {
          "color": "Space Gray",
          "storage": "256GB",
          "sku": "IPH15PRO-SG-256"
        }
      },
      {
        "id": "order-item-uuid-2",
        "productVariantId": "variant-uuid-456",
        "quantity": 1,
        "unitPrice": 299.99,
        "totalPrice": 299.99,
        "product": {
          "name": {
            "en": "AirPods Pro",
            "ar": "إيربودز برو",
            "ku": "ئێرپۆدز پرۆ"
          },
          "images": [
            "https://example.com/airpods-pro-1.jpg"
          ]
        },
        "variant": {
          "color": "White",
          "sku": "AIRPODS-PRO-WHITE"
        }
      }
    ],
    "warehouse": {
      "id": "warehouse-uuid-789",
      "name": "Central Electronics Warehouse",
      "address": {
        "city": "New York",
        "state": "NY",
        "country": "USA"
      }
    },
    "delivery": {
      "id": "delivery-uuid-321",
      "trackingNumber": "TRK-GRP-456-001",
      "estimatedDelivery": "2023-12-04T15:00:00Z",
      "carrier": "Express Delivery Co",
      "status": "in_transit"
    },
    "createdAt": "2023-12-01T10:00:00Z",
    "updatedAt": "2023-12-02T09:00:00Z"
  }
}
```

### Response 404 Error - Order Group Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Order group not found or does not belong to this order"
}
```

### Response 404 Error - Order Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Order not found or does not belong to user"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Authentication required"
}
```

## GET /storefront/orders/:order_id/details — Get Customer Order Details

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#get-get-customer-order-details)

Retrieves detailed order information for the authenticated customer. Only shows orders belonging to the authenticated user.

- **Required:** Valid Bearer token for customer identification

### Features
- Customer-scoped access (users can only see their own orders)
- Complete order details and timeline
- Delivery tracking information
- Payment status and method
- Item details with current availability

### Response Data
- Full order details with items
- Order status timeline
- Delivery tracking and estimates
- Payment information
- Shipping address
- Item availability for reordering

### Use Cases
- Customer order detail page
- Order tracking and status updates
- Reorder functionality
- Customer service self-help
- Receipt and invoice display

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/orders/:order_id/details

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated customers (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | UUID of the order to retrieve | order-uuid-123 |



### Response 200 Success - Customer Order Details

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order": {
      "id": "order-uuid-123",
      "order_number": "M1000-25-12010001",
      "status": "delivered",
      "payment_method": "cod",
      "payment_status": "completed",
      "subtotal": 1999.98,
      "tax_amount": 0,
      "shipping_cost": 15,
      "total_price": 2014.98,
      "currency": "USD",
      "created_at": "2023-11-28T10:00:00Z",
      "delivered_at": "2023-12-01T14:30:00Z",
      "shipping_address": {
        "title": "Home",
        "address_line1": "123 Main Street",
        "city": "New York",
        "state": "NY",
        "postal_code": "10001",
        "country": "US"
      },
      "items": [
        {
          "id": "order-item-uuid-1",
          "variant_id": "variant-uuid-123",
          "quantity": 2,
          "unit_price": 999.99,
          "total_price": 1999.98,
          "variant": {
            "color": "Space Gray",
            "storage": "256GB"
          },
          "product_name": {
            "en": "iPhone 15 Pro",
            "ar": "آيفون 15 برو",
            "ku": "ئایفۆن 15 پرۆ"
          },
          "product_images": [
            "https://example.com/iphone15-pro-1.jpg"
          ],
          "current_availability": {
            "in_stock": true,
            "current_price": 949.99,
            "price_changed": true
          }
        }
      ],
      "status_timeline": [
        {
          "status": "pending",
          "timestamp": "2023-11-28T10:00:00Z",
          "description": "Order placed"
        },
        {
          "status": "confirmed",
          "timestamp": "2023-11-28T16:00:00Z",
          "description": "Order confirmed"
        },
        {
          "status": "shipped",
          "timestamp": "2023-11-30T09:00:00Z",
          "description": "Order shipped"
        },
        {
          "status": "delivered",
          "timestamp": "2023-12-01T14:30:00Z",
          "description": "Order delivered successfully"
        }
      ],
      "delivery_info": {
        "tracking_number": "TRK-2023-001234",
        "driver_name": "Mike Johnson",
        "driver_phone": "+1555123456",
        "delivered_by": "Mike Johnson",
        "delivery_notes": "Left at front door as requested"
      },
      "actions_available": {
        "can_review": true,
        "can_reorder": true,
        "can_return": false,
        "can_cancel": false
      }
    }
  }
}
```

## POST /storefront/stores/{{store_id}}/delivery/price — Calculate Delivery Price (from Cart)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#post-calculate-delivery-price-from-cart)

Calculates delivery fee for the authenticated user's current cart in a specific store (market).

- **store_id** (required): ID of the store/market

- **delivery_service_id** (required): Delivery service to price
- **dropoff.latitude** (required)
- **dropoff.longitude** (required)

### Behavior
- The backend loads the authenticated user's current cart for the store
- Inventory is planned across warehouses before pricing
- If any warehouse quote fails, the whole request fails with `success: false`
- The response still includes `data`, and `message` is localized from the first failed quote error
- A unit-range failure returns code `can_not_use_this_delivery_for_this_unit_size` plus calculated/allowed unit fields at the top level and on the failed quote

### Success Response
- **data.market**: { id, slug, store_name }
- **data.allocations**: [{ warehouse_id, units }]
- **data.quotes**: [{ warehouse_id, units, success, price, error }]
- **data.total_fee**: number

### Frontend integration

display `message`; use `code` for branching and the numeric range fields to explain why another service or cart adjustment is required.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/stores/{{store_id}}/delivery/price

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| store_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_service_id | string |  |  |  |
| dropoff | object |  |  |  |
| dropoff.latitude | number |  |  |  |
| dropoff.longitude | number |  |  |  |



### Request body (raw)

```json
{
  "delivery_service_id": "service-uuid",
  "dropoff": {
    "latitude": 36.2,
    "longitude": 44
  }
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "store-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      }
    },
    "allocations": [
      {
        "warehouse_id": "wh-1",
        "units": 2
      }
    ],
    "quotes": [
      {
        "warehouse_id": "wh-1",
        "units": 2,
        "success": true,
        "price": 5.5
      }
    ],
    "total_fee": 5.5
  }
}
```

### Response 400 Pricing Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery location outside delivery zone",
  "data": {
    "market": {
      "id": "store-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      }
    },
    "allocations": [
      {
        "warehouse_id": "wh-1",
        "units": 1
      }
    ],
    "quotes": [
      {
        "warehouse_id": "wh-1",
        "units": 1,
        "success": false,
        "price": null,
        "error": "delivery_location_outside_delivery_zone"
      }
    ],
    "total_fee": 0
  }
}
```

### Response 400 Calculated Units Outside Service Range

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum",
  "data": {
    "quotes": [
      {
        "warehouse_id": "wh-1",
        "units": 6,
        "success": false,
        "price": null,
        "error": "can_not_use_this_delivery_for_this_unit_size",
        "field": "unit_size",
        "calculated_unit_size": 6,
        "allowed_unit_min": 1,
        "allowed_unit_max": 4,
        "violation": "above_maximum"
      }
    ],
    "total_fee": 0
  }
}
```

### Response 401 Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "unauthorized"
}
```

## PUT /storefront/orders/:order_id/groups/:group_id/approve — Approve Order Group (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#put-approve-order-group-admin)

Approves a specific order group that is in 'awaiting_approval'. Updates inventory reservations for just this group and may trigger delivery creation.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/orders/:order_id/groups/:group_id/approve

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | UUID of the order | order-uuid-123 |
| group_id | path string | Yes | UUID of the group to approve | group-uuid-456 |



### Response 200 Success - Group Approved

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "order": { /* ... */ },
  "group": { "id": "group-uuid-456", "status": "approved" },
  "message": "Order group approved successfully"
}
```

## POST /storefront/orders/:order_id/groups/:group_id/delivery — Create Delivery for Order Group (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#post-create-delivery-for-order-group-admin)

Creates and links a delivery for a specific order group. Use this when market settings have auto-delivery disabled, or when delivery was not created during approval.

### Notes
- Idempotent: if the group already has a delivery_id, returns success with existing linkage.
- Creates internal/external delivery based on tenant delivery configuration.
- Internal unit-range failures return code `can_not_use_this_delivery_for_this_unit_size`, a localized message, and calculated/allowed unit fields.

### Frontend integration

display `message`; use `code` only for branching. On a unit-range failure, keep the group unlinked and show the numeric mismatch.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/orders/:order_id/groups/:group_id/delivery

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | UUID of the order | order-uuid-123 |
| group_id | path string | Yes | UUID of the order group | group-uuid-456 |



### Response 400 Calculated Units Outside Service Range

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum",
  "origin": "internal"
}
```

## PUT /storefront/orders/:order_id/groups/:group_id/status — Update Order Group Status (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#put-update-order-group-status-admin)

Updates a specific order group's status. Allowed values: pending, awaiting_approval, approved, rejected, shipped, in_transit, delivered, failed_delivery, returned, cancelled, refunded.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/orders/:order_id/groups/:group_id/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | UUID of the order | order-uuid-123 |
| group_id | path string | Yes | UUID of the order group | group-uuid-456 |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "status": "shipped"
}
```

### Response 200 Success - Group Status Updated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "order": { /* ... */ },
  "group": { "id": "group-uuid-456", "status": "shipped" },
  "message": "Order group status updated"
}
```

### Response 400 Validation Error

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation failed",
  "errors": [
    {
      "field": "status",
      "message": "Invalid status"
    }
  ]
}
```

## PUT /storefront/orders/:order_id/groups/:group_id/items — Edit Order Group Items (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#put-edit-order-group-items-admin)

Allows a tenant admin to edit the products inside a specific order group.

The `items` array is the **complete desired state** — whatever is in the array becomes the new set of items for the group:
- Items in the array but not in the group → added
- Items in the group but not in the array → removed
- Items in both → quantity updated if changed

### Warehouse validation

Every variant must belong to the same warehouse as the group. A `variant_warehouse_mismatch` error is returned if a variant comes from a different warehouse.

### Allowed group statuses

`pending`, `awaiting_approval`, `approved`. Editing is blocked once the group is shipped or further.

### Financial recalculation

Group totals and order totals are automatically recalculated after the edit.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/orders/:order_id/groups/:group_id/items

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | UUID of the order | order-uuid-123 |
| group_id | path string | Yes | UUID of the order group | group-uuid-456 |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| items | array<object> | Yes | Non-empty array of `{ variantId: string, quantity: number }` — must be positive integers. |  |
| items.variantId | string |  |  |  |
| items.quantity | number |  |  |  |



### Request body (raw)

```json
{
  "items": [
    {
      "variantId": "variant-uuid-111",
      "quantity": 2
    },
    {
      "variantId": "variant-uuid-222",
      "quantity": 1
    }
  ]
}
```

### Response 200 Success - Items Updated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "order": {
    "id": "order-uuid-123",
    "status": "awaiting_approval",
    "totalPrice": 1500,
    "finalAmount": 1515
  },
  "group": {
    "id": "group-uuid-456",
    "warehouseId": "warehouse-uuid-789",
    "status": "awaiting_approval",
    "totalPrice": 1500,
    "finalAmount": 1515,
    "items": [
      {
        "id": "item-uuid-1",
        "variantId": "variant-uuid-111",
        "quantity": 2,
        "unitPrice": 500,
        "totalPrice": 1000
      },
      {
        "id": "item-uuid-2",
        "variantId": "variant-uuid-222",
        "quantity": 1,
        "unitPrice": 500,
        "totalPrice": 500
      }
    ]
  },
  "message": "Order group items updated"
}
```

### Response 400 Error - Variant from Wrong Warehouse

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "variant_not_in_group_warehouse",
  "details": {
    "error": "variant_warehouse_mismatch",
    "variantId": "variant-uuid-999"
  }
}
```

### Response 400 Error - Group Not Editable

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "order_group_not_editable",
  "details": {
    "status": "shipped"
  }
}
```

### Response 404 Error - Order Group Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "order_group_not_found"
}
```

## DELETE /storefront/orders/:order_id/groups/:group_id — Remove Order Group (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-management#delete-remove-order-group-admin)

Removes (cancels) a specific order group from an order.

### Typical use cases
- Customer calls support to drop one warehouse shipment while keeping the rest
- A product set from one warehouse can no longer be fulfilled
- Admin/support needs to replace a product with a different variant (remove old group item, edit items to add new)

### Business rules
- The order must have **more than one active group** — you cannot remove the last active group. Use the cancel order endpoint instead.
- The group must not already be shipped, in_transit, delivered, cancelled, or refunded.
- Automatically releases inventory reservations and cancels any linked delivery for the group.
- Recalculates order financial totals after removal.

### No request body required.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/orders/:order_id/groups/:group_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | UUID of the order | order-uuid-123 |
| group_id | path string | Yes | UUID of the order group to remove | group-uuid-456 |



### Response 200 Success - Group Removed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "order": {
    "id": "order-uuid-123",
    "status": "awaiting_approval",
    "totalPrice": 500,
    "finalAmount": 515
  },
  "group": {
    "id": "group-uuid-456",
    "status": "cancelled"
  },
  "message": "order_group_removed_successfully"
}
```

### Response 400 Error - Last Active Group

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "cannot_remove_last_order_group",
  "details": "Use cancel order to cancel the entire order instead"
}
```

### Response 400 Error - Group Cannot Be Removed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "order_group_cannot_be_removed",
  "details": {
    "status": "shipped"
  }
}
```

### Response 404 Error - Order Group Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "order_group_not_found"
}
```

## POST /storefront/market/create — Create Market

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/market-creation-and-management#post-create-market)

Creates a new market/storefront for the authenticated tenant. Implements Shopify-like market creation with initial configuration.

- **store_name** (required): Unique store identifier (alphanumeric, hyphens, underscores only)
- **directory_category** (optional): Normalized directory category key. Defaults to `general`
- **tagline** (optional): Multi-language store tagline/slogan
- **settings** (optional): Market settings object
  - **auto_create_delivery** (optional, boolean): If true, approving an order group will auto-create a delivery
- **logo_url** (optional): URL to store logo image
- **theme_settings** (optional): Visual theme configuration
- **banners** (optional): Array of banner configurations for the storefront

### Banner Object Fields
- **url** (required): Image URL for the banner
- **title** (required): Multi-language banner title
- **subtitle** (optional): Multi-language banner subtitle
- **link_url** (optional): URL to navigate when banner is clicked
- **is_active** (optional): Boolean to control banner visibility
- **sort_order** (optional): Number to control banner display order

### Features
- Unique store name validation
- Multi-language support
- Theme customization
- Banner management with multi-language support
- Automatic default category creation
- Draft status by default (requires approval)

### Business Rules
- Store name must be globally unique
- `directory_category` must match one of the configured directory category keys
- Store name format: alphanumeric with hyphens/underscores
- Market created in 'draft' status
- Banner URL and title are required if banners are provided
- Default categories auto-created
- Requires system admin approval before going live

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/market/create

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| store_name | string |  |  |  |
| directory_category | string |  |  |  |
| tagline | object |  |  |  |
| tagline.en | string |  |  |  |
| tagline.ar | string |  |  |  |
| tagline.ku | string |  |  |  |
| settings | object |  |  |  |
| settings.auto_create_delivery | boolean |  |  |  |
| logo_url | string |  |  |  |
| theme_settings | object |  |  |  |
| theme_settings.primary_color | string |  |  |  |
| theme_settings.secondary_color | string |  |  |  |
| theme_settings.accent_color | string |  |  |  |
| theme_settings.theme_style | string |  |  |  |
| banners | array<object> |  |  |  |
| banners.url | string |  |  |  |
| banners.title | object |  |  |  |
| banners.title.en | string |  |  |  |
| banners.title.ar | string |  |  |  |
| banners.title.ku | string |  |  |  |
| banners.subtitle | object |  |  |  |
| banners.subtitle.en | string |  |  |  |
| banners.subtitle.ar | string |  |  |  |
| banners.subtitle.ku | string |  |  |  |
| banners.link_url | string |  |  |  |
| banners.is_active | boolean |  |  |  |
| banners.sort_order | number |  |  |  |



### Request body (raw)

```json
{
  "store_name": "my-awesome-store",
  "directory_category": "fashion_apparel",
  "tagline": {
    "en": "Quality products for everyone",
    "ar": "منتجات عالية الجودة للجميع",
    "ku": "بەرهەمی کوالیتی بۆ هەمووان"
  },
  "settings": {
    "auto_create_delivery": true
  },
  "logo_url": "https://example.com/my-store-logo.png",
  "theme_settings": {
    "primary_color": "#007AFF",
    "secondary_color": "#FFFFFF",
    "accent_color": "#FF9500",
    "theme_style": "modern"
  },
  "banners": [
    {
      "url": "https://example.com/banner1.jpg",
      "title": {
        "en": "Summer Sale",
        "ar": "تخفيضات الصيف",
        "ku": "فرۆشتنی هاوین"
      },
      "subtitle": {
        "en": "Up to 50% off",
        "ar": "خصم يصل إلى 50%",
        "ku": "تا 50% داشکاندن"
      },
      "link_url": "/categories/electronics",
      "is_active": true,
      "sort_order": 1
    },
    {
      "url": "https://example.com/banner2.jpg",
      "title": {
        "en": "New Arrivals",
        "ar": "وصل حديثاً",
        "ku": "گەیشتنی نوێ"
      },
      "subtitle": {
        "en": "Check out our latest products",
        "ar": "تحقق من أحدث منتجاتنا",
        "ku": "بەرهەمە نوێیەکانمان ببینە"
      },
      "link_url": "/products/new",
      "is_active": true,
      "sort_order": 2
    }
  ]
}
```

### Response 201 Success - Market Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid-new",
      "tenant_id": "tenant-uuid",
      "code": 1000,
      "directory_category": "fashion_apparel",
      "store_name": "my-awesome-store",
      "status": "draft",
      "publish_status": "unpublished",
      "tagline": {
        "en": "Quality products for everyone",
        "ar": "منتجات عالية الجودة للجميع",
        "ku": "بەرهەمی کوالیتی بۆ هەمووان"
      },
      "logo_url": "https://example.com/my-store-logo.png",
      "theme_settings": {
        "primary_color": "#007AFF",
        "secondary_color": "#FFFFFF",
        "accent_color": "#FF9500",
        "theme_style": "modern"
      },
      "banners": [],
      "approved_by": null,
      "approved_at": null,
      "admin_notes": null,
      "created_at": "2023-12-01T14:00:00Z",
      "updated_at": "2023-12-01T14:00:00Z"
    },
    "message": "Market created successfully. Your store is now in draft status and pending approval."
  }
}
```

### Response 409 Error - Store Name Already Exists

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Store name already exists. Please choose a different name."
}
```

### Response 400 Error - Validation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation failed",
  "errors": [
    {
      "field": "store_name",
      "message": "Store name must contain only letters, numbers, hyphens, and underscores"
    }
  ]
}
```

## GET /storefront/market — Get Market Details

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/market-creation-and-management#get-get-market-details)

Retrieves the market details for the authenticated tenant. Returns comprehensive market information including configuration, status, and metadata.

### Features
- Complete market configuration
- Status and approval information
- Theme and branding settings
- Banner configurations
- Timestamp information
- Storefront Command Center extras: `readiness`, `summary`, `recent_activity`

### Returns
- `market` — full market object (id, status, publish_status, settings, theme_settings, banners, etc.)
- `readiness` — 7 setup checklist booleans (`warehouse_created`, `store_live`, `payments_enabled`, `delivery_configured`, `products_added`, `categories_added`, `domain_connected`). `domain_connected` is optional in UI; the other 6 must all be true for 100% readiness.
- `summary` — KPI counts (`total_products` active, `total_categories`, `total_customers` (market users), `total_orders`).
- `recent_activity` — up to 5 most recent activity items, mixed types (`order` | `product` | `review`), sorted by timestamp desc. Empty `[]` when no activity.

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/market

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |



### Response 200 Success - Market Details

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "market": {
    "id": "market-uuid",
    "tenant_id": "tenant-uuid",
    "code": 1000,
    "store_name": "my-awesome-store",
    "status": "approved",
    "publish_status": "published",
    "tagline": {
      "en": "Quality products for everyone",
      "ar": "منتجات عالية الجودة للجميع",
      "ku": "بەرهەمی کوالیتی بۆ هەمووان"
    },
    "logo_url": "https://example.com/my-store-logo.png",
    "settings": {
      "currency": "USD",
      "payment_methods": [
        "cod",
        "card"
      ],
      "auto_create_delivery": true
    },
    "theme_settings": {
      "primary_color": "#007AFF",
      "secondary_color": "#FFFFFF",
      "accent_color": "#FF9500",
      "theme_style": "modern"
    },
    "banners": [],
    "approved_by": "admin-uuid",
    "approved_at": "2026-04-13T15:00:00Z",
    "admin_notes": "Approved",
    "created_by": "user-uuid",
    "updated_by": "user-uuid",
    "created_at": "2026-04-13T14:00:00Z",
    "updated_at": "2026-04-13T15:30:00Z",
    "is_live": true,
    "is_active": true
  },
  "readiness": {
    "warehouse_created": true,
    "store_live": true,
    "payments_enabled": true,
    "delivery_configured": true,
    "products_added": true,
    "categories_added": true,
    "domain_connected": false
  },
  "summary": {
    "total_products": 12,
    "total_categories": 4,
    "total_customers": 7,
    "total_orders": 25
  },
  "recent_activity": [
    {
      "type": "order",
      "title": "New order #1234",
      "subtitle": "USD 45.00",
      "timestamp": "2026-04-13T10:30:00Z"
    },
    {
      "type": "product",
      "title": "New product: Wireless Mouse",
      "subtitle": "Stock: 50",
      "timestamp": "2026-04-12T18:10:00Z"
    },
    {
      "type": "review",
      "title": "New 5★ review",
      "subtitle": "On Wireless Mouse",
      "timestamp": "2026-04-12T09:00:00Z"
    }
  ]
}
```

### Response 404 Error - Market Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Market not found for this tenant"
}
```

## GET /storefront/market/{{market_id}} — Get Market by ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/market-creation-and-management#get-get-market-by-id)

Retrieves market details by specific market ID. Useful for admin operations or when accessing market data from external systems.

- **market_id** (required): UUID of the market to retrieve

### Features
- Direct market access by ID
- Complete market configuration
- Status and approval information
- Theme and branding settings
- Banner configurations
- Timestamp information

### Returns
- Market ID and basic info
- Status (draft, pending_approval, approved, rejected, inactive)
- Publish status (published, unpublished)
- Theme settings and branding
- Banner configurations
- Approval workflow data
- Tenant information

### Use Cases
- Admin panel market management
- External system integrations
- Cross-tenant market lookups
- Public market display

### Authentication Required

Valid authentication token

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/market/{{market_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| market_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success - Market Found



```json
{
  "success": true,
  "market": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "tenant_id": "tenant-uuid",
    "code": 1000,
    "store_name": "my-awesome-store",
    "status": "approved",
    "publish_status": "published",
    "tagline": {
      "en": "Quality products for everyone",
      "ar": "منتجات عالية الجودة للجميع",
      "ku": "بەرهەمی کوالیتی بۆ هەمووان"
    },
    "logo_url": "https://example.com/my-store-logo.png",
    "theme_settings": {
      "primary_color": "#007AFF",
      "secondary_color": "#FFFFFF",
      "accent_color": "#FF9500",
      "theme_style": "modern"
    },
    "banners": [
      {
        "id": "banner-uuid-1",
        "url": "https://example.com/banner1.jpg",
        "title": {
          "en": "Summer Sale",
          "ar": "تخفيضات الصيف",
          "ku": "فرۆشتنی هاوین"
        },
        "subtitle": {
          "en": "Up to 50% off",
          "ar": "خصم يصل إلى 50%",
          "ku": "تا 50% داشکاندن"
        },
        "link_url": "/categories/electronics",
        "is_active": true,
        "sort_order": 1
      }
    ],
    "approved_by": "admin-uuid",
    "approved_at": "2023-12-01T15:00:00Z",
    "admin_notes": "Approved after review. Great store concept!",
    "created_by": "user-uuid",
    "updated_by": "user-uuid",
    "created_at": "2023-12-01T14:00:00Z",
    "updated_at": "2023-12-01T15:30:00Z",
    "is_live": true,
    "is_active": true
  }
}
```

### Response 404 Error - Market Not Found



```json
{
  "success": false,
  "message": "Market not found"
}
```

### Response 400 Error - Invalid Market ID



```json
{
  "success": false,
  "message": "Market ID required"
}
```

## PUT /storefront/market — Update Market

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/market-creation-and-management#put-update-market)

Updates the market configuration for the authenticated tenant. All fields are optional for partial updates.

- **directory_category**: Normalized directory category key
- **tagline**: Multi-language store tagline/slogan
- **settings**: Market settings object
  - **auto_create_delivery** (optional, boolean): If true, approving an order group will auto-create a delivery
- **logo_url**: Updated URL to store logo image
- **theme_settings**: Updated visual theme configuration
- **banners**: Updated banner configurations array

### Banner Object Fields
- **url** (required): Image URL for the banner
- **title** (required): Multi-language banner title
- **subtitle** (optional): Multi-language banner subtitle
- **link_url** (optional): URL to navigate when banner is clicked
- **is_active** (optional): Boolean to control banner visibility
- **sort_order** (optional): Number to control banner display order

### Features
- Partial updates (only provided fields updated)
- Multi-language content updates
- Theme customization
- Banner management with multi-language support
- Preserves existing data

### Business Rules
- Cannot update store_name (requires admin approval)
- `directory_category` must match one of the configured directory category keys
- Cannot change status directly (use workflow endpoints)
- Banner URL and title are required if banners are provided
- Updates reset approval if market was previously approved
- Changes require re-approval for going live

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/market

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| directory_category | string |  |  |  |
| tagline | object |  |  |  |
| tagline.en | string |  |  |  |
| tagline.ar | string |  |  |  |
| tagline.ku | string |  |  |  |
| settings | object |  |  |  |
| settings.auto_create_delivery | boolean |  |  |  |
| logo_url | string |  |  |  |
| theme_settings | object |  |  |  |
| theme_settings.primary_color | string |  |  |  |
| theme_settings.secondary_color | string |  |  |  |
| theme_settings.accent_color | string |  |  |  |
| theme_settings.theme_style | string |  |  |  |
| banners | array<object> |  |  |  |
| banners.url | string |  |  |  |
| banners.title | object |  |  |  |
| banners.title.en | string |  |  |  |
| banners.title.ar | string |  |  |  |
| banners.title.ku | string |  |  |  |
| banners.subtitle | object |  |  |  |
| banners.subtitle.en | string |  |  |  |
| banners.subtitle.ar | string |  |  |  |
| banners.subtitle.ku | string |  |  |  |
| banners.link_url | string |  |  |  |
| banners.is_active | boolean |  |  |  |
| banners.sort_order | number |  |  |  |



### Request body (raw)

```json
{
  "directory_category": "home_furniture",
  "tagline": {
    "en": "Premium quality, exceptional service",
    "ar": "جودة ممتازة، خدمة استثنائية",
    "ku": "کوالیتی باڵا، خزمەتگوزاری تایبەت"
  },
  "settings": {
    "auto_create_delivery": true
  },
  "logo_url": "https://example.com/updated-logo.png",
  "theme_settings": {
    "primary_color": "#34C759",
    "secondary_color": "#FFFFFF",
    "accent_color": "#30D158",
    "theme_style": "modern"
  },
  "banners": [
    {
      "url": "https://example.com/updated-banner.jpg",
      "title": {
        "en": "Winter Collection",
        "ar": "مجموعة الشتاء",
        "ku": "کۆکراوەی زستان"
      },
      "subtitle": {
        "en": "Warm and cozy items",
        "ar": "عناصر دافئة ومريحة",
        "ku": "بەرگی گەرم و ئارام"
      },
      "link_url": "/categories/winter",
      "is_active": true,
      "sort_order": 1
    }
  ]
}
```

### Response 200 Success - Market Updated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "code": 1000,
      "directory_category": "home_furniture",
      "store_name": "my-awesome-store",
      "status": "approved",
      "publish_status": "published",
      "tagline": {
        "en": "Premium quality, exceptional service",
        "ar": "جودة ممتازة، خدمة استثنائية",
        "ku": "کوالیتی باڵا، خزمەتگوزاری تایبەت"
      },
      "logo_url": "https://example.com/updated-logo.png",
      "theme_settings": {
        "primary_color": "#34C759",
        "secondary_color": "#FFFFFF",
        "accent_color": "#30D158",
        "theme_style": "modern"
      },
      "banners": [],
      "updated_at": "2023-12-01T16:00:00Z"
    },
    "message": "Market updated successfully"
  }
}
```

## POST /storefront/market/submit-for-approval — Submit Market for Approval

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/market-creation-and-management#post-submit-market-for-approval)

Submits the market for system admin approval. Changes status from 'draft' to 'pending_approval'.

- **notes** (optional): Message to admin reviewers explaining the submission

### Features
- Status transition to pending_approval
- Optional notes for admin review
- Validates market completeness
- Triggers admin notification

### Business Rules
- Market must be in 'draft' status
- Market must have basic information completed
- Market must have at least one product category
- Cannot resubmit if already pending or approved

### Workflow
1. Validates market readiness
2. Changes status to 'pending_approval'
3. Notifies system administrators
4. Market enters approval queue

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/market/submit-for-approval

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "notes": "Ready for review. All products are properly categorized and store information is complete."
}
```

### Response 200 Success - Submitted for Approval

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "status": "pending_approval",
      "updated_at": "2023-12-01T16:30:00Z"
    },
    "message": "Market submitted for approval successfully. You will be notified once reviewed."
  }
}
```

### Response 400 Error - Invalid Status

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Market is not in draft status and cannot be submitted for approval"
}
```

## POST /storefront/market/publish — Toggle Market Publishing

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/market-creation-and-management#post-toggle-market-publishing)

Toggles the publishing status of an approved market. Controls whether the market is publicly accessible.

- **is_published** (required): Boolean - true to publish, false to unpublish

### Features
- Publish/unpublish approved markets
- Instant public visibility control
- Maintains approval status
- Reversible action

### Business Rules
- Market must be in 'approved' status
- Only approved markets can be published
- Published markets are publicly accessible
- Unpublished markets are hidden from public

### Values
- **true**: Makes approved market publicly accessible
- **false**: Hides market from public while keeping approval

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/market/publish

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_published | boolean |  |  |  |



### Request body (raw)

```json
{
  "is_published": true
}
```

### Response 200 Success - Market Published

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "code": 1000,
      "store_name": "my-awesome-store",
      "status": "approved",
      "publish_status": "published",
      "tagline": {
        "en": "Quality products for everyone",
        "ar": "منتجات عالية الجودة للجميع",
        "ku": "بەرهەمی کوالیتی بۆ هەمووان"
      },
      "logo_url": "https://example.com/my-store-logo.png",
      "theme_settings": {
        "primary_color": "#007AFF",
        "secondary_color": "#FFFFFF",
        "accent_color": "#FF9500",
        "theme_style": "modern"
      },
      "banners": [],
      "approved_by": "admin-uuid",
      "approved_at": "2023-12-01T15:00:00Z",
      "admin_notes": "Approved after review",
      "created_by": "user-uuid",
      "updated_by": "user-uuid",
      "created_at": "2023-12-01T14:00:00Z",
      "updated_at": "2023-12-01T17:00:00Z",
      "is_live": true,
      "is_active": true
    },
    "message": "Market published successfully. Your store is now live!"
  }
}
```

### Response 200 Success - Market Unpublished

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "code": 1000,
      "store_name": "my-awesome-store",
      "status": "approved",
      "publish_status": "unpublished",
      "tagline": {
        "en": "Quality products for everyone",
        "ar": "منتجات عالية الجودة للجميع",
        "ku": "بەرهەمی کوالیتی بۆ هەمووان"
      },
      "logo_url": "https://example.com/my-store-logo.png",
      "theme_settings": {
        "primary_color": "#007AFF",
        "secondary_color": "#FFFFFF",
        "accent_color": "#FF9500",
        "theme_style": "modern"
      },
      "banners": [],
      "approved_by": "admin-uuid",
      "approved_at": "2023-12-01T15:00:00Z",
      "admin_notes": "Approved after review",
      "created_by": "user-uuid",
      "updated_by": "user-uuid",
      "created_at": "2023-12-01T14:00:00Z",
      "updated_at": "2023-12-01T17:30:00Z",
      "is_live": false,
      "is_active": true
    },
    "message": "Market unpublished successfully. Your store is now hidden from public."
  }
}
```

### Response 400 Error - Market Not Approved

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Market must be approved before it can be published"
}
```

## GET /storefront/market/check-name — Check Store Name Availability

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/market-creation-and-management#get-check-store-name-availability)

Checks if a store name is available for use. Validates both availability and format.

- **store_name** (required): Store name to check for availability

### Features
- Real-time availability checking
- Format validation
- Global uniqueness verification
- Instant feedback for UI

### Validation Rules
- Must be 2-100 characters long
- Alphanumeric characters only
- Hyphens and underscores allowed
- No spaces or special characters
- Globally unique across all markets

### Use Cases
- Form validation during market creation
- Real-time feedback in store setup
- Preventing duplicate store names

### Authentication Required

Tenant-level access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/market/check-name?store_name=my-new-store

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for authenticated requests | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| store_name | query string |  | Store name to check for availability | my-new-store |



### Response 200 Success - Available

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "store_name": "my-unique-store",
    "available": true,
    "message": "Store name is available"
  }
}
```

### Response 200 Success - Not Available

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "store_name": "existing-store",
    "available": false,
    "message": "Store name is already taken"
  }
}
```

### Response 400 Error - Invalid Format

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Store name must contain only letters, numbers, hyphens, and underscores"
}
```

## GET /storefront/stores/:store_id/cart — Get Cart

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/shopping-cart-and-checkout#get-get-cart-2)

Retrieves the authenticated user's shopping cart for the store (market), with items, pricing, and totals.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/stores/:store_id/cart

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT (required). Cart is keyed by authenticated user. | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| store_id | path string | Yes |  | {{store_id}} |



### Response 200 Success - Cart with Items

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "cart": {
      "id": "cart-uuid-123",
      "market_id": "{{store_id}}",
      "enriched_items": [
        {
          "variant_id": "variant-uuid-1",
          "quantity": 2,
          "variant_slug": "iphone-15-pro-space-gray-256gb",
          "product_slug": "iphone-15-pro",
          "has_variant": true,
          "variant": {
            "id": "variant-uuid-1",
            "slug": "iphone-15-pro-space-gray-256gb",
            "name": {
              "en": "iPhone 15 Pro - 256GB"
            },
            "price": 999.99,
            "images": [
              "https://example.com/iphone15-pro-1.jpg"
            ],
            "stock_quantity": 25,
            "attributes": {
              "color": "Space Gray",
              "storage": "256GB"
            },
            "product": {
              "id": "product-uuid-1",
              "name": {
                "en": "iPhone 15 Pro"
              },
              "market_id": "market-uuid",
              "slug": "iphone-15-pro",
              "has_variant": true
            }
          }
        }
      ],
      "total_items": 2,
      "updated_at": "2023-12-01T15:30:00Z"
    }
  }
}
```

### Response 200 Success - Empty Cart

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "cart": null
}
```

## POST /storefront/stores/:store_id/cart/items — Add to Cart

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/shopping-cart-and-checkout#post-add-to-cart)

Adds a product variant to the authenticated user's shopping cart.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/stores/:store_id/cart/items

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| store_id | path string | Yes |  | {{store_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| variant_id | string |  |  |  |
| quantity | number |  |  |  |



### Request body (raw)

```json
{
  "variant_id": "variant-uuid-123",
  "quantity": 1
}
```

### Response 200 Success - Item Added

OK

```json
{
  "success": true,
  "message": "Item added to cart"
}
```

### Response 400 Error - Insufficient Stock

Bad Request

```json
{
  "success": false,
  "message": "Insufficient stock available"
}
```

### Response 404 Error - Product Not Found

Not Found

```json
{
  "success": false,
  "message": "Product not found or not available"
}
```

## PUT /storefront/stores/:store_id/cart/items — Update Cart Item

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/shopping-cart-and-checkout#put-update-cart-item-2)

Updates the quantity of an item in the shopping cart. To remove an item, set quantity to 0.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/stores/:store_id/cart/items

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| store_id | path string | Yes |  | {{store_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| variant_id | string |  |  |  |
| quantity | number |  |  |  |



### Request body (raw)

```json
{
  "variant_id": "variant-uuid-123",
  "quantity": 3
}
```

### Response 200 Success - Quantity Updated

OK

```json
{
  "success": true,
  "message": "Cart updated"
}
```

## PUT /storefront/stores/:store_id/cart/items — Remove from Cart

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/shopping-cart-and-checkout#put-remove-from-cart)

Removes an item from the cart by setting its quantity to 0.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/stores/:store_id/cart/items

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| store_id | path string | Yes |  | {{store_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| variant_id | string |  |  |  |
| quantity | number |  |  |  |



### Request body (raw)

```json
{
  "variant_id": "variant-uuid-123",
  "quantity": 0
}
```

### Response 200 Success - Item Removed

OK

```json
{
  "success": true,
  "message": "Cart updated"
}
```

## POST /storefront/stores/{{store_id}}/checkout — Checkout - Create Order from Cart

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/shopping-cart-and-checkout#post-checkout-create-order-from-cart)

Creates a pending order from the authenticated user's cart. No request body required. Payment method and shipping address will be handled in a separate payment initiation API.

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/stores/{{store_id}}/checkout

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| X-API-Key | header string |  | Alternative authentication method | {{api_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| store_id | path string | Yes | Variable used inside the request path. |  |



## GET /storefront/analytics — Dashboard Analytics

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advanced-features/analytics-and-reporting#get-dashboard-analytics)

Retrieves comprehensive dashboard analytics for the storefront including sales metrics, customer insights, and performance indicators.

### Authentication Required

Tenant admin access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/analytics?period=30d

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for tenant admin users | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| period | query string |  | Time period (7d, 30d, 90d, 1y) | 30d |



### Response 200 Success - Dashboard Analytics

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "period": "30d",
    "sales": {
      "total_revenue": 125430.5,
      "total_orders": 247,
      "average_order_value": 507.85,
      "growth_rate": 12.5
    },
    "customers": {
      "total_customers": 189,
      "new_customers": 45,
      "returning_customers": 144,
      "retention_rate": 76.2
    },
    "top_products": [
      {
        "id": "product-uuid-1",
        "name": {
          "en": "iPhone 15 Pro"
        },
        "units_sold": 45,
        "revenue": 44999.55
      }
    ]
  }
}
```

## GET /storefront/payment-methods/available — Get Available Payment Methods

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advanced-features/payment-methods-management#get-get-available-payment-methods)

Retrieves all available payment methods that can be configured for the storefront. For the COD record, the localized display name is Cash while the canonical name, gateway code, type, and behavior remain unchanged.

### Authentication Required

Tenant admin access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/payment-methods/available

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for tenant admin users | Bearer {{access_token}} |



### Response 200 Success - Available Payment Methods

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "available_methods": [
      {
        "id": "cod",
        "name": {
          "en": "Cash",
          "ar": "نقدًا",
          "fa": "نقدی",
          "ku": "نەقدی"
        },
        "is_configured": true,
        "is_enabled": true,
        "setup_required": false
      },
      {
        "id": "wallet",
        "name": {
          "en": "Digital Wallet",
          "ar": "المحفظة الرقمية",
          "ku": "جزدانی دیجیتاڵ"
        },
        "is_configured": true,
        "is_enabled": true,
        "setup_required": false
      }
    ]
  }
}
```

## GET /storefront/theme — Get Theme Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advanced-features/theme-customization#get-get-theme-settings)

Retrieves current theme and appearance settings for the storefront.

### Authentication Required

Tenant admin access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/theme

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for tenant admin users | Bearer {{access_token}} |



### Response 200 Success - Theme Settings

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "theme_settings": {
      "primary_color": "#007AFF",
      "secondary_color": "#FFFFFF",
      "accent_color": "#FF9500",
      "theme_style": "modern"
    },
    "branding": {
      "logo_url": "https://example.com/logo.png",
      "store_name": "My Awesome Store"
    }
  }
}
```

## PUT /storefront/theme — Update Theme Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advanced-features/theme-customization#put-update-theme-settings)

Updates theme and appearance settings for the storefront. Supports partial updates.

### Authentication Required

Tenant admin access

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/theme

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for tenant admin users | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| theme_settings | object |  |  |  |
| theme_settings.primary_color | string |  |  |  |
| theme_settings.secondary_color | string |  |  |  |
| theme_settings.accent_color | string |  |  |  |
| theme_settings.theme_style | string |  |  |  |



### Request body (raw)

```json
{
  "theme_settings": {
    "primary_color": "#34C759",
    "secondary_color": "#FFFFFF",
    "accent_color": "#30D158",
    "theme_style": "modern"
  }
}
```

### Response 200 Success - Theme Updated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "theme_settings": {
      "primary_color": "#34C759",
      "secondary_color": "#FFFFFF",
      "accent_color": "#30D158",
      "theme_style": "modern"
    },
    "updated_at": "2023-12-01T19:00:00Z",
    "message": "Theme settings updated successfully"
  }
}
```

## POST /storefront/stock/refresh — Refresh Catalog Stock

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/advanced-features/stock-management#post-refresh-catalog-stock)

Manually refresh stock quantities for all catalog items in the tenant's storefront. This updates the stock_quantity field for all catalog items based on current warehouse inventory levels.

### Authentication Required

Tenant admin access

### Features
- Updates stock quantities from warehouse inventory
- Processes all catalog items for the tenant
- Returns summary of updated items
- Maintains real-time stock accuracy

### Use Cases
- Manual stock synchronization
- Troubleshooting stock discrepancies
- Batch stock updates after inventory changes
- Administrative stock management

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/stock/refresh

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token for tenant admin users | Bearer {{access_token}} |



### Response 200 Success - Stock Refreshed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "message": "Stock quantities refreshed successfully",
    "updated_variants_count": 25,
    "total_products_affected": 15,
    "updated_at": "2023-12-01T18:30:00Z"
  }
}
```

### Response 404 Error - No Market Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Market not found for tenant"
}
```

## GET /storefront/admin/order-status-rules — Get All Status Rules

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-status-rules-management#get-get-all-status-rules)

Retrieves all active order status aggregation rules along with available status values and aggregation types.

- **Required:** Valid tenant admin authentication

### Features
- Returns rules sorted by priority (highest first)
- Includes available order status enum values
- Provides aggregation types (ALL, ANY)
- Shows rule descriptions and business logic

### Use Cases
- Configure admin interface for status rules
- Understand current business logic
- Troubleshoot status aggregation issues

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/admin/order-status-rules

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin (required) | Bearer {{admin_access_token}} |



### Response 200 Success - Status Rules List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "rules": [
    {
      "id": "rule-uuid-1",
      "status": "cancelled",
      "priority": 1,
      "aggregationType": "ALL",
      "targetStatus": "cancelled",
      "isActive": true,
      "description": "When all groups have status 'cancelled', set order status to 'cancelled'",
      "createdAt": "2024-01-20T10:00:00Z",
      "updatedAt": "2024-01-20T10:00:00Z"
    },
    {
      "id": "rule-uuid-2",
      "status": "delivered",
      "priority": 2,
      "aggregationType": "ALL",
      "targetStatus": "delivered",
      "isActive": true,
      "description": "When all groups have status 'delivered', set order status to 'delivered'"
    },
    {
      "id": "rule-uuid-3",
      "status": "shipped",
      "priority": 12,
      "aggregationType": "ANY",
      "targetStatus": "shipped",
      "isActive": true,
      "description": "When any group have status 'shipped', set order status to 'shipped'"
    },
    {
      "id": "rule-uuid-4",
      "status": "pending",
      "priority": 99,
      "aggregationType": "ANY",
      "targetStatus": "pending",
      "isActive": true,
      "description": "When any group have status 'pending', set order status to 'pending'"
    }
  ],
  "availableStatuses": [
    "pending",
    "awaiting_approval",
    "approved",
    "rejected",
    "shipped",
    "in_transit",
    "delivered",
    "failed_delivery",
    "returned",
    "cancelled",
    "refunded"
  ],
  "aggregationTypes": [
    "ALL",
    "ANY"
  ]
}
```

## POST /storefront/admin/order-status-rules — Create Status Rule

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-status-rules-management#post-create-status-rule)

Creates a new order status aggregation rule.

- **Required:** Valid tenant admin authentication

- **status** (required): The group status to check for (must be valid enum value)
- **priority** (required): Rule priority (lower number = higher priority, 1 is highest)
- **aggregationType** (required): Either 'ALL' or 'ANY'
  - **ALL**: All groups must have this status
  - **ANY**: At least one group must have this status
- **targetStatus** (required): The resulting order status (must be valid enum value)
- **isActive** (optional): Whether the rule is active (default: true)

### Business Logic
- Rules are processed in priority order (1, 2, 3...)
- First matching rule wins and stops processing
- Status values are validated against the order status enum
- Priority conflicts are allowed but not recommended

### Examples
- Priority 1, ALL cancelled → cancelled (highest priority terminal state)
- Priority 10, ANY shipped → shipped (progressive state)
- Priority 99, ANY pending → pending (default fallback)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/admin/order-status-rules

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin (required) | Bearer {{admin_access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| priority | number |  |  |  |
| aggregationType | string |  |  |  |
| targetStatus | string |  |  |  |
| isActive | boolean |  |  |  |



### Request body (raw)

```json
{
  "status": "processing",
  "priority": 15,
  "aggregationType": "ANY",
  "targetStatus": "processing",
  "isActive": true
}
```

### Response 201 Success - Rule Created

Created

```json
{
  "success": true,
  "rule": {
    "id": "new-rule-uuid",
    "status": "processing",
    "priority": 15,
    "aggregationType": "ANY",
    "targetStatus": "processing",
    "isActive": true,
    "description": "When any group have status 'processing', set order status to 'processing'",
    "createdAt": "2024-01-20T11:00:00Z",
    "updatedAt": "2024-01-20T11:00:00Z"
  },
  "message": "Status rule created successfully"
}
```

### Response 400 Error - Invalid Status

Bad Request

```json
{
  "success": false,
  "message": "Invalid status: invalid_status. Available statuses are: pending, awaiting_approval, approved, rejected, shipped, in_transit, delivered, failed_delivery, returned, cancelled, refunded"
}
```

## PUT /storefront/admin/order-status-rules/{{rule_id}} — Update Status Rule

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-status-rules-management#put-update-status-rule)

Updates an existing order status aggregation rule.

- **Required:** Valid tenant admin authentication

- **rule_id** (required): UUID of the rule to update

- **status**: Update the group status to check
- **priority**: Update the rule priority
- **aggregationType**: Update aggregation type (ALL/ANY)
- **targetStatus**: Update the resulting order status
- **isActive**: Enable/disable the rule

### Partial Updates
- Only include fields you want to update
- Omitted fields remain unchanged
- Status values are validated if provided

### Use Cases
- Change rule priority for reordering
- Temporarily disable rules without deletion
- Modify business logic for specific statuses
- Update aggregation behavior

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/admin/order-status-rules/{{rule_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin (required) | Bearer {{admin_access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| rule_id | path string | Yes | Variable used inside the request path. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| priority | number |  |  |  |
| isActive | boolean |  |  |  |



### Request body (raw)

```json
{
  "priority": 8,
  "isActive": false
}
```

### Response 200 Success - Rule Updated

OK

```json
{
  "success": true,
  "rule": {
    "id": "rule-uuid",
    "status": "processing",
    "priority": 8,
    "aggregationType": "ANY",
    "targetStatus": "processing",
    "isActive": false,
    "description": "When any group have status 'processing', set order status to 'processing'",
    "updatedAt": "2024-01-20T11:30:00Z"
  },
  "message": "Status rule updated successfully"
}
```

## DELETE /storefront/admin/order-status-rules/{{rule_id}} — Delete Status Rule

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-status-rules-management#delete-delete-status-rule)

Permanently deletes an order status aggregation rule.

- **Required:** Valid tenant admin authentication

- **rule_id** (required): UUID of the rule to delete

### Warning
- This action is permanent and cannot be undone
- Consider deactivating rules instead of deleting
- Deleting rules may affect order status aggregation
- Ensure you have backups of important rules

### Alternative
- Use UPDATE with `isActive: false` to disable instead of delete
- This preserves rule history and allows easy reactivation

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/admin/order-status-rules/{{rule_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin (required) | Bearer {{admin_access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| rule_id | path string | Yes | Variable used inside the request path. |  |



### Response 200 Success - Rule Deleted

OK

```json
{
  "success": true,
  "message": "Status rule deleted successfully"
}
```

## POST /storefront/admin/order-status-rules/reorder — Reorder Status Rules

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-status-rules-management#post-reorder-status-rules)

Reorders status aggregation rules by priority in batch.

- **Required:** Valid tenant admin authentication

- **ruleIds** (required): Array of rule UUIDs in desired priority order

### Reordering Logic
- First rule in array gets priority 10
- Second rule gets priority 20
- Third rule gets priority 30, etc.
- This provides spacing for future insertions

### Use Cases
- Drag-and-drop reordering in admin interface
- Bulk priority updates
- Business logic reorganization
- Testing different rule precedence

### Best Practices
- Terminal states (cancelled, delivered) should have high priority (low numbers)
- Progressive states (shipped, approved) should have medium priority
- Default states (pending) should have low priority (high numbers)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/admin/order-status-rules/reorder

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin (required) | Bearer {{admin_access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| ruleIds | array<string> |  |  |  |



### Request body (raw)

```json
{
  "ruleIds": [
    "rule-uuid-1",
    "rule-uuid-2",
    "rule-uuid-3",
    "rule-uuid-4"
  ]
}
```

### Response 200 Success - Rules Reordered

OK

```json
{
  "success": true,
  "rules": [
    {
      "id": "rule-uuid-1",
      "status": "cancelled",
      "priority": 10,
      "aggregationType": "ALL",
      "targetStatus": "cancelled"
    },
    {
      "id": "rule-uuid-2",
      "status": "delivered",
      "priority": 20,
      "aggregationType": "ALL",
      "targetStatus": "delivered"
    },
    {
      "id": "rule-uuid-3",
      "status": "shipped",
      "priority": 30,
      "aggregationType": "ANY",
      "targetStatus": "shipped"
    },
    {
      "id": "rule-uuid-4",
      "status": "pending",
      "priority": 40,
      "aggregationType": "ANY",
      "targetStatus": "pending"
    }
  ],
  "message": "Status rules reordered successfully"
}
```

## POST /storefront/admin/order-status-rules/reset-defaults — Reset to Default Rules

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-status-rules-management#post-reset-to-default-rules)

Resets all status aggregation rules to the default configuration.

- **Required:** Valid tenant admin authentication

### Warning
- This action deactivates all existing rules
- Creates a new set of default rules
- Custom business logic will be lost
- This action cannot be undone

### Default Rules Created
1. ALL cancelled → cancelled (Priority 1)
2. ALL delivered → delivered (Priority 2)
3. ALL rejected → rejected (Priority 3)
4. ALL refunded → refunded (Priority 4)
5. ALL returned → returned (Priority 5)
6. ANY failed_delivery → failed_delivery (Priority 10)
7. ANY in_transit → shipped (Priority 11)
8. ANY shipped → shipped (Priority 12)
9. ANY approved → approved (Priority 13)
10. ANY awaiting_approval → awaiting_approval (Priority 14)
11. ANY pending → pending (Priority 99)

### Use Cases
- Emergency reset after configuration errors
- Starting fresh with proven business logic
- Restoring after failed customizations
- Setting up new tenants

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/admin/order-status-rules/reset-defaults

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin (required) | Bearer {{admin_access_token}} |



### Response 200 Success - Rules Reset

OK

```json
{
  "success": true,
  "rules": [
    {
      "id": "new-rule-uuid-1",
      "status": "cancelled",
      "priority": 1,
      "aggregationType": "ALL",
      "targetStatus": "cancelled",
      "isActive": true
    },
    {
      "id": "new-rule-uuid-2",
      "status": "delivered",
      "priority": 2,
      "aggregationType": "ALL",
      "targetStatus": "delivered",
      "isActive": true
    }
  ],
  "message": "Status rules reset to defaults successfully"
}
```

## POST /storefront/admin/order-status-rules/test — Test Status Aggregation

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/order-status-rules-management#post-test-status-aggregation)

Tests status aggregation logic with a given set of group statuses without affecting actual orders.

- **Required:** Valid tenant admin authentication

- **groupStatuses** (required): Array of group status values to test

### Testing Logic
- Simulates the same aggregation logic used in production
- Processes rules in priority order
- Returns the first matching rule result
- Shows which rules would match for analysis

### Use Cases
- Test rule changes before applying to production
- Validate business logic correctness
- Troubleshoot unexpected status aggregations
- Training and documentation
- Quality assurance testing

### Example Scenarios
- Test: ['shipped', 'pending'] → Result: 'shipped' (ANY shipped rule)
- Test: ['delivered', 'delivered'] → Result: 'delivered' (ALL delivered rule)
- Test: ['cancelled', 'cancelled'] → Result: 'cancelled' (ALL cancelled rule)
- Test: ['pending', 'pending'] → Result: 'pending' (fallback rule)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/admin/order-status-rules/test

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin (required) | Bearer {{admin_access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| groupStatuses | array<string> |  |  |  |



### Request body (raw)

```json
{
  "groupStatuses": [
    "shipped",
    "pending",
    "approved"
  ]
}
```

### Response 200 Success - Aggregation Test Result

OK

```json
{
  "success": true,
  "input": {
    "groupStatuses": [
      "shipped",
      "pending",
      "approved"
    ],
    "totalGroups": 3
  },
  "result": {
    "aggregatedStatus": "shipped",
    "appliedRules": [
      {
        "id": "rule-uuid-shipped",
        "status": "shipped",
        "priority": 12,
        "aggregationType": "ANY",
        "targetStatus": "shipped",
        "description": "When any group have status 'shipped', set order status to 'shipped'",
        "matched": true,
        "reason": "1 out of 3 groups have status 'shipped'"
      },
      {
        "id": "rule-uuid-approved",
        "status": "approved",
        "priority": 13,
        "aggregationType": "ANY",
        "targetStatus": "approved",
        "description": "When any group have status 'approved', set order status to 'approved'",
        "matched": true,
        "reason": "1 out of 3 groups have status 'approved'"
      },
      {
        "id": "rule-uuid-pending",
        "status": "pending",
        "priority": 99,
        "aggregationType": "ANY",
        "targetStatus": "pending",
        "description": "When any group have status 'pending', set order status to 'pending'",
        "matched": true,
        "reason": "1 out of 3 groups have status 'pending'"
      }
    ]
  }
}
```

### Response 200 Test - All Delivered Scenario

OK

```json
{
  "success": true,
  "input": {
    "groupStatuses": [
      "delivered",
      "delivered",
      "delivered"
    ],
    "totalGroups": 3
  },
  "result": {
    "aggregatedStatus": "delivered",
    "appliedRules": [
      {
        "status": "delivered",
        "priority": 2,
        "aggregationType": "ALL",
        "targetStatus": "delivered",
        "matched": true,
        "reason": "All 3 groups have status 'delivered'"
      }
    ]
  }
}
```

## GET /storefront/variant-option-types — List Variant Option Types

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/variant-option-types-management#get-list-variant-option-types)

Retrieves available variant option types including both system-wide (colors, size, volume, etc.) and tenant-specific custom options. These option types are used when creating products to define what variant attributes are required.

- **Required:** Valid Bearer token with tenant access

### System Option Types
- **colors**: Color variations (Red, Blue, Black, etc.)
- **size**: Size variations (S, M, L, XL, 14 inch, etc.)
- **volume**: Volume/capacity variations (500ml, 1L, etc.)
- **model**: Model variations (iPhone 15 Pro, MacBook Air, etc.)
- **weight**: Weight variations (1kg, 2.5kg, etc.)
- **material**: Material variations (Cotton, Leather, etc.)
- **brand**: Brand variations (Nike, Adidas, etc.)

### Custom Option Types
- Tenant-specific options like 'ram', 'storage', 'warranty', etc.
- Created by tenant admins for specific business needs
- Only visible to the creating tenant

### Features
- Multi-language support (en, ar, ku)
- System and custom option filtering
- Tenant-scoped access control
- Active status filtering
- Alphabetical ordering

### Use Cases
- Product creation interface (selecting variant options)
- Variant management workflows
- Product catalog configuration
- Custom option type discovery
- Multi-language option type display

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/variant-option-types?include_system=true&include_custom=true&tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated requests (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| include_system | query string | No | Include system-wide option types (default: true) Include system-wide option types like colors, size, volume (default: true) | true |
| include_custom | query string | No | Include tenant-specific custom option types (default: true) | true |
| tenant_id | query string | No | Tenant ID for custom options (optional, defaults to user's tenant) Specific tenant ID for custom options (defaults to user's tenant) | {{tenant_id}} |



### Response 200 Success - Mixed Option Types

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": [
    {
      "id": "system-colors-uuid",
      "name": "colors",
      "display_name": {
        "en": "Colors",
        "ar": "الألوان",
        "ku": "ڕەنگەکان"
      },
      "is_system": true,
      "tenant_id": null,
      "is_active": true,
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    },
    {
      "id": "system-size-uuid",
      "name": "size",
      "display_name": {
        "en": "Size",
        "ar": "الحجم",
        "ku": "قەبارە"
      },
      "is_system": true,
      "tenant_id": null,
      "is_active": true,
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    },
    {
      "id": "custom-ram-uuid",
      "name": "ram",
      "display_name": {
        "en": "RAM Memory",
        "ar": "ذاكرة الوصول العشوائي",
        "ku": "یادگەی ڕام"
      },
      "is_system": false,
      "tenant_id": "tenant-uuid",
      "is_active": true,
      "created_at": "2024-07-20T10:00:00Z",
      "updated_at": "2024-07-20T10:00:00Z"
    }
  ],
  "message": "Variant option types retrieved successfully"
}
```

### Response 200 Success - System Options Only

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": [
    {
      "id": "system-colors-uuid",
      "name": "colors",
      "display_name": {
        "en": "Colors",
        "ar": "الألوان",
        "ku": "ڕەنگەکان"
      },
      "is_system": true,
      "tenant_id": null,
      "is_active": true,
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    },
    {
      "id": "system-size-uuid",
      "name": "size",
      "display_name": {
        "en": "Size",
        "ar": "الحجم",
        "ku": "قەبارە"
      },
      "is_system": true,
      "tenant_id": null,
      "is_active": true,
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    }
  ],
  "message": "Variant option types retrieved successfully"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Unauthorized"
}
```

## POST /storefront/variant-option-types — Create Custom Variant Option Type

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/variant-option-types-management#post-create-custom-variant-option-type)

Creates a new custom variant option type for the tenant. Custom option types allow businesses to define specific variant attributes beyond the standard system options.

- **Required:** Valid Bearer token with tenant access
- **Permission:** Only tenant members can create custom options for their tenant

### Required Fields
- `name`: Unique identifier for the option type (lowercase, alphanumeric + underscore)
- `display_name`: Multi-language display names object
- `display_name.en`: English display name (required)
- `tenant_id`: Tenant ID (optional, defaults to user's tenant)

### Optional Fields
- `display_name.ar`: Arabic display name
- `display_name.ku`: Kurdish display name

### Naming Rules
- Must be lowercase
- Only alphanumeric characters and underscores allowed
- Must be unique within tenant and not conflict with system options
- Cannot use names like: colors, size, volume, model, weight, material, brand

### Features
- Tenant-scoped option types
- Multi-language support
- Name validation and conflict checking
- Automatic activation
- Unique constraint enforcement

### Use Cases
- Electronics: 'ram', 'storage', 'processor', 'graphics_card'
- Clothing: 'fabric', 'fit_type', 'collar_style', 'sleeve_length'
- Food: 'flavor', 'spice_level', 'dietary_type', 'packaging'
- Books: 'format', 'language', 'edition', 'binding_type'

### Examples
- Tech products: storage, ram, processor, warranty
- Fashion: fabric, fit, collar, sleeve
- Food: flavor, spice_level, organic, packaging
- Books: format, language, edition

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/variant-option-types

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated requests (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| display_name | object |  |  |  |
| display_name.en | string |  |  |  |
| display_name.ar | string |  |  |  |
| display_name.ku | string |  |  |  |
| tenant_id | string |  |  |  |



### Request body (raw)

```json
{
  "name": "storage",
  "display_name": {
    "en": "Storage Capacity",
    "ar": "سعة التخزين",
    "ku": "قەبارەی هەڵگرتن"
  },
  "tenant_id": "{{tenant_id}}"
}
```

### Response 201 Success - Custom Option Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "custom-storage-uuid",
    "name": "storage",
    "display_name": {
      "en": "Storage Capacity",
      "ar": "سعة التخزين",
      "ku": "قەبارەی هەڵگرتن"
    },
    "is_system": false,
    "tenant_id": "tenant-uuid",
    "is_active": true,
    "created_at": "2024-07-20T10:00:00Z",
    "updated_at": "2024-07-20T10:00:00Z"
  },
  "message": "Variant option type created successfully"
}
```

### Response 400 Error - Validation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Missing required fields: name, display_name.en"
}
```

### Response 400 Error - Invalid Name Format

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Name must be lowercase alphanumeric with underscores only"
}
```

### Response 409 Error - Name Conflicts with System Option

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Option type name conflicts with system option"
}
```

### Response 409 Error - Name Already Exists

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Option type name already exists for this tenant"
}
```

### Response 403 Error - Access Denied

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Access denied. Can only create options for your own tenant."
}
```

## GET /storefront/variant-option-types/{{option_type_id}} — Get Variant Option Type

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/variant-option-types-management#get-get-variant-option-type)

Retrieves a specific variant option type by its ID. Returns both system and custom option types if the user has access.

- **Required:** Valid Bearer token with tenant access

### Access Control
- System option types: Accessible by all authenticated users
- Custom option types: Only accessible by the creating tenant's members

### Response Data
- Complete option type details
- Multi-language display names
- System/custom classification
- Tenant association (for custom types)
- Active status and timestamps

### Use Cases
- Option type detail views
- Edit form pre-population
- Validation of option type existence
- Display name localization
- Permission checking workflows

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/variant-option-types/{{option_type_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated requests (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| option_type_id | path string | Yes | UUID of the variant option type to retrieve |  |



### Response 200 Success - System Option Type

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "system-colors-uuid",
    "name": "colors",
    "display_name": {
      "en": "Colors",
      "ar": "الألوان",
      "ku": "ڕەنگەکان"
    },
    "is_system": true,
    "tenant_id": null,
    "is_active": true,
    "created_at": "2024-01-01T00:00:00Z",
    "updated_at": "2024-01-01T00:00:00Z"
  },
  "message": "Variant option type retrieved successfully"
}
```

### Response 200 Success - Custom Option Type

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "custom-storage-uuid",
    "name": "storage",
    "display_name": {
      "en": "Storage Capacity",
      "ar": "سعة التخزين",
      "ku": "قەبارەی هەڵگرتن"
    },
    "is_system": false,
    "tenant_id": "tenant-uuid",
    "is_active": true,
    "created_at": "2024-07-20T10:00:00Z",
    "updated_at": "2024-07-20T10:00:00Z"
  },
  "message": "Variant option type retrieved successfully"
}
```

### Response 404 Error - Option Type Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Variant option type not found"
}
```

## PUT /storefront/variant-option-types/{{option_type_id}} — Update Variant Option Type

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/variant-option-types-management#put-update-variant-option-type)

Updates an existing custom variant option type. Only display names and active status can be modified. System option types cannot be updated.

- **Required:** Valid Bearer token with tenant access
- **Permission:** Only tenant members can update their own custom options

### Updatable Fields (all optional)
- `display_name`: Multi-language display names object
- `display_name.en`: English display name
- `display_name.ar`: Arabic display name
- `display_name.ku`: Kurdish display name
- `is_active`: Boolean to enable/disable the option type

### Restrictions
- Cannot update system option types (colors, size, volume, etc.)
- Cannot change the name field (immutable after creation)
- Can only update option types owned by your tenant
- Partial updates supported (only send fields to change)

### Features
- Partial update support
- Multi-language display name updates
- Active status control
- Ownership validation
- System option protection

### Use Cases
- Updating display names for localization
- Enabling/disabling custom option types
- Correcting display name typos
- Seasonal option type management
- Workflow status control

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/variant-option-types/{{option_type_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated requests (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| option_type_id | path string | Yes | UUID of the variant option type to update |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| display_name | object |  |  |  |
| display_name.en | string |  |  |  |
| display_name.ar | string |  |  |  |
| display_name.ku | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "display_name": {
    "en": "Storage Memory",
    "ar": "ذاكرة التخزين",
    "ku": "یادگەی هەڵگرتن"
  },
  "is_active": true
}
```

### Response 200 Success - Option Type Updated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "custom-storage-uuid",
    "name": "storage",
    "display_name": {
      "en": "Storage Memory",
      "ar": "ذاكرة التخزين",
      "ku": "یادگەی هەڵگرتن"
    },
    "is_system": false,
    "tenant_id": "tenant-uuid",
    "is_active": true,
    "created_at": "2024-07-20T10:00:00Z",
    "updated_at": "2024-07-20T11:00:00Z"
  },
  "message": "Variant option type updated successfully"
}
```

### Response 403 Error - Cannot Modify System Options

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Cannot modify system option types"
}
```

### Response 403 Error - Access Denied

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Access denied. Can only modify options for your own tenant."
}
```

### Response 404 Error - Option Type Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Variant option type not found"
}
```

## DELETE /storefront/variant-option-types/{{option_type_id}} — Delete Variant Option Type

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/variant-option-types-management#delete-delete-variant-option-type)

Deletes a custom variant option type. This is a soft delete that sets the option type as inactive. System option types cannot be deleted. Includes safety checks to prevent data loss.

- **Required:** Valid Bearer token with tenant access
- **Permission:** Only tenant members can delete their own custom options

### Safety Features
- Cannot delete system option types (colors, size, volume, etc.)
- Cannot delete option types being used by products
- Soft delete (sets is_active to false instead of permanent deletion)
- Usage validation before deletion
- Ownership verification

### Deletion Rules
1. Option type must exist and belong to the user's tenant
2. Option type must not be a system option
3. Option type must not be in use by any products
4. If any rule fails, deletion is prevented with explanation

### Important Notes
- This is a soft delete (option type becomes inactive)
- Products using this option type will prevent deletion
- Remove option type from all products before deletion
- Consider deactivating instead of deleting for data retention

### Use Cases
- Cleanup of unused custom option types
- Removal of incorrectly created options
- Business model changes
- Option type consolidation
- Tenant cleanup workflows

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/variant-option-types/{{option_type_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated requests (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| option_type_id | path string | Yes | UUID of the variant option type to delete |  |



### Response 200 Success - Option Type Deleted

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Variant option type deleted successfully"
}
```

### Response 403 Error - Cannot Delete System Options

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Cannot delete system option types"
}
```

### Response 409 Error - Option Type In Use

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Cannot delete option type that is being used by products"
}
```

### Response 403 Error - Access Denied

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Access denied. Can only delete options for your own tenant."
}
```

### Response 404 Error - Option Type Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Variant option type not found"
}
```

## GET /storefront/delivery-config — Get Delivery Config

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/storefront-delivery-configuration#get-get-delivery-config)

Fetch current tenant delivery configuration. Response: { success, config: { mode, status, external_base_url, timeout_ms, retries, last_tested_at, test_result, webhook_secret, webhook_url } }

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/delivery-config

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "config": {
    "mode": "external",
    "status": "active",
    "external_base_url": "https://example.com/functions/v1",
    "timeout_ms": 15000,
    "retries": 2,
    "last_tested_at": "2025-10-31T12:00:00.000Z",
    "test_result": {},
    "webhook_secret": "{{storefront_webhook_secret}}",
    "webhook_url": "{{base_url}}/storefront/hooks/delivery-status"
  }
}
```

## PUT /storefront/delivery-config — Update Delivery Config (Internal)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/storefront-delivery-configuration#put-update-delivery-config-internal)

Switches to internal mode and saves with status=inactive. Response: { success, message, config } with webhook fields null.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/delivery-config

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| mode | string |  |  |  |
| timeout_ms | number |  |  |  |
| retries | number |  |  |  |



### Request body (raw)

```json
{
  "mode": "internal",
  "timeout_ms": 15000,
  "retries": 2
}
```

### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "delivery_integration_updated_successfully",
  "config": {
    "mode": "internal",
    "status": "inactive",
    "external_base_url": null,
    "timeout_ms": 15000,
    "retries": 2,
    "last_tested_at": null,
    "test_result": null,
    "webhook_secret": null,
    "webhook_url": null
  }
}
```

## POST /storefront/delivery-config/test — Test & Activate External Delivery Config

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/storefront-delivery-configuration#post-test-and-activate-external-delivery-config)

Tests external using request body only. On success: saves and activates, returns { success, status, test_result, config } (with webhook_secret and webhook_url). Errors: external_health_check_error (400), external_health_check_timeout (408).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/delivery-config/test

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| mode | string |  |  |  |
| external_base_url | string |  |  |  |
| external_api_key | string |  |  |  |



### Request body (raw)

```json
{
  "mode": "external",
  "external_base_url": "{{external_base_url}}",
  "external_api_key": "{{external_api_key}}"
}
```

### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "status": "active",
  "test_result": {},
  "config": {
    "mode": "external",
    "status": "active",
    "external_base_url": "{{external_base_url}}",
    "timeout_ms": null,
    "retries": null,
    "last_tested_at": null,
    "test_result": null,
    "webhook_secret": "{{storefront_webhook_secret}}",
    "webhook_url": "{{base_url}}/storefront/hooks/delivery-status"
  }
}
```

## PUT /storefront/delivery-config — Disable External (Back to Internal)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/storefront-delivery-configuration#put-disable-external-back-to-internal)

Disables external and switches configuration back to internal with status=inactive.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/delivery-config

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| mode | string |  |  |  |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "mode": "external",
  "status": "disabled"
}
```

### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "delivery_integration_updated_successfully",
  "config": {
    "mode": "internal",
    "status": "inactive",
    "external_base_url": null,
    "timeout_ms": null,
    "retries": null,
    "last_tested_at": null,
    "test_result": null,
    "webhook_secret": null,
    "webhook_url": null
  }
}
```

## POST /storefront/hooks/delivery-status — Inbound Delivery Status Webhook (Test Receiver)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/storefront-delivery-configuration#post-inbound-delivery-status-webhook-test-receiver)

Webhook endpoint to receive delivery status updates. Provide the generated webhook secret in X-API-Key.

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/hooks/delivery-status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-API-Key | header string |  |  | {{storefront_webhook_secret}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | string |  |  |  |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "delivery_id": "EXT-DEL-123",
  "status": "in_transit"
}
```

## GET /storefront/stores/{{market_id}}/delivery-services — List Delivery Services

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/storefront-delivery-configuration#get-list-delivery-services-3)

List delivery services for the storefront tenant. Only works if delivery integration is enabled (status='active'). Supports both internal and external modes:
- Internal mode: Queries delivery_services table directly
- External mode: Proxies request to external delivery module API

### Response

{ success: true, deliveryServices: [...], page: number, limit: number, total: number }

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/stores/{{market_id}}/delivery-services?delivery_type_code=on_demand&active=true&search=&page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| market_id | path string | Yes | Variable used inside the request path. |  |
| store_id | path string |  | (market_id): Market ID to determine tenant |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_type_code | query string | No | Filter by delivery type code (e.g., 'on_demand') | on_demand |
| active | query string | No | Filter by active status (default: true) | true |
| search | query string | No | Search by name or code |  |
| page | query string | No | Page number (default: 1) | 1 |
| limit | query string | No | Items per page (default: 10) | 10 |



### Response 200 200 OK - Internal Mode

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "deliveryServices": [
    {
      "id": "service-uuid",
      "name": "Standard Delivery",
      "code": "standard",
      "description": "Standard delivery service",
      "service_level": "standard",
      "default_price": 5,
      "estimated_delivery_time_min": 30,
      "estimated_delivery_time_max": 60,
      "service_options": {
        "signature_required": false,
        "insurance_available": true
      },
      "unit_min": 1,
      "unit_max": 10,
      "active": true,
      "delivery_type": {
        "id": "type-uuid",
        "code": "on_demand",
        "name": "On Demand"
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 200 200 OK - External Mode

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "deliveryServices": [
    {
      "id": "external-service-id",
      "name": "Express Delivery",
      "code": "express",
      "description": "Express delivery from external provider",
      "service_level": "express",
      "default_price": 10,
      "estimated_delivery_time_min": 15,
      "estimated_delivery_time_max": 30,
      "service_options": {
        "signature_required": true,
        "insurance_available": true
      },
      "unit_min": 1,
      "unit_max": 5,
      "active": true,
      "delivery_type": {
        "id": "type-uuid",
        "code": "on_demand",
        "name": "On Demand"
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 400 400 Bad Request - Market ID Required

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "market_id_required"
}
```

### Response 404 404 Not Found - Market Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "market_not_found"
}
```

### Response 400 400 Bad Request - Integration Not Enabled

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "delivery_integration_not_enabled"
}
```

## GET /storefront/brands — List Brands

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/brand-management#get-list-brands)

List brands for the tenant's market with pagination, AdvancedDataGrid sorting/filtering, per-brand products_count, and market-wide stats.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/brands?page=1&limit=20&is_active=true&search=&sort_by=products_count&sort_order=desc&column_filters=%7B%22name%22%3A%22adi%22%2C%22is_active%22%3A%22true%22%2C%22created_at%22%3A%222025-01-01..2025-12-31%22%7D

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) | 1 |
| limit | query string |  | Items per page (default: 20) | 20 |
| is_active | query string | No | Filter by active state (optional) | true |
| search | query string | No | Search by name/description (optional) |  |
| sort_by | query string |  | Sort by one of: name, slug, is_active, products_count, created_at | products_count |
| sort_order | query string |  | Sort order: asc or desc | desc |
| column_filters | query string |  | JSON-encoded column filters for AdvancedDataGrid. Supported keys: name, is_active, created_at | {"name":"adi","is_active":"true","created_at":"2025-01-01..2025-12-31"} |



### Response 200 Success Response



```json
{
  "success": true,
  "brands": [
    {
      "id": "24b931dc-0000-0000-0000-000000000001",
      "market_id": "market-uuid",
      "name": {
        "en": "adidas"
      },
      "slug": "adidas",
      "description": {},
      "logo_url": "https://example.com/logo.png",
      "is_active": true,
      "products_count": 452,
      "created_at": "2026-04-01T10:00:00Z",
      "updated_at": "2026-04-05T10:00:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1284,
    "totalPages": 65
  },
  "stats": {
    "totalBrands": 1284,
    "activeBrands": 1202,
    "topBrand": {
      "name": "Lumina Global",
      "productsCount": 452
    },
    "recentlyAdded": [
      {
        "name": "BrandA",
        "logo_url": "https://example.com/a.png"
      },
      {
        "name": "BrandB",
        "logo_url": "https://example.com/b.png"
      },
      {
        "name": "BrandC",
        "logo_url": "https://example.com/c.png"
      }
    ]
  }
}
```

## POST /storefront/brands — Create Brand

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/brand-management#post-create-brand)

Create a new brand in the tenant's market. Required: name (multilingual), slug.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/brands

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| slug | string |  |  |  |
| description | object |  |  |  |
| logo_url | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": ""
  },
  "slug": "",
  "description": {},
  "logo_url": "",
  "is_active": true
}
```

## GET /storefront/brands/:brand_id — Get Brand

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/brand-management#get-get-brand)

Fetch a brand by ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/brands/:brand_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| brand_id | path string | Yes |  | brand-uuid |



## PUT /storefront/brands/:brand_id — Update Brand

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/brand-management#put-update-brand)

Update brand fields (all optional).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/storefront/brands/:brand_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| brand_id | path string | Yes |  | brand-uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| slug | string |  |  |  |
| description | object |  |  |  |
| logo_url | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": ""
  },
  "slug": "",
  "description": {},
  "logo_url": "",
  "is_active": true
}
```

## DELETE /storefront/brands/:brand_id — Delete Brand

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/brand-management#delete-delete-brand)

Delete a brand by ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/brands/:brand_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| brand_id | path string | Yes |  | brand-uuid |



## GET /storefront/custom-domain — Get Current Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/custom-domain-management#get-get-current-custom-domain)

Returns the active or pending custom domain for the authenticated tenant storefront.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/storefront/custom-domain

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success



```json
{
  "success": true,
  "message": "market_custom_domain_retrieved_successfully",
  "data": {
    "custom_domain": {
      "id": "domain-uuid",
      "market_id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "custom_domain": "shop.example.com",
      "status": "pending_dns",
      "dns_records": [
        {
          "type": "CNAME",
          "name": "shop",
          "value": "cname.vercel-dns.com",
          "purpose": "verification"
        }
      ],
      "dns_verified_at": null,
      "created_at": "2026-03-27T12:00:00Z",
      "updated_at": "2026-03-27T12:00:00Z"
    }
  }
}
```

## POST /storefront/custom-domain/connect — Connect Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/custom-domain-management#post-connect-custom-domain-2)

Creates a new custom domain connection for the authenticated tenant market and returns the DNS records the frontend should show to the user.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/custom-domain/connect

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| custom_domain | string |  |  |  |



### Request body (raw)

```json
{
  "custom_domain": "shop.example.com"
}
```

### Response 201 Success



```json
{
  "success": true,
  "message": "market_custom_domain_connected_successfully",
  "data": {
    "custom_domain": {
      "id": "domain-uuid",
      "market_id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "custom_domain": "shop.example.com",
      "status": "pending_dns",
      "dns_records": [
        {
          "type": "CNAME",
          "name": "shop",
          "value": "cname.vercel-dns.com",
          "purpose": "verification"
        }
      ],
      "dns_verified_at": null,
      "created_at": "2026-03-27T12:00:00Z",
      "updated_at": "2026-03-27T12:00:00Z"
    }
  }
}
```

### Response 409 Error - Domain Already In Use



```json
{
  "success": false,
  "message": "custom_domain_already_in_use",
  "data": null
}
```

## POST /storefront/custom-domain/verify — Verify Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/custom-domain-management#post-verify-custom-domain-2)

Checks the current tenant domain against Vercel and activates it once DNS verification is complete.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/storefront/custom-domain/verify

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success - Verified



```json
{
  "success": true,
  "message": "market_custom_domain_verified_successfully",
  "data": {
    "verified": true,
    "custom_domain": {
      "id": "domain-uuid",
      "custom_domain": "shop.example.com",
      "status": "active",
      "dns_verified_at": "2026-03-27T12:30:00Z"
    }
  }
}
```

### Response 200 Success - Still Pending



```json
{
  "success": true,
  "message": "market_custom_domain_dns_pending",
  "data": {
    "verified": false,
    "custom_domain": {
      "id": "domain-uuid",
      "custom_domain": "shop.example.com",
      "status": "pending_dns",
      "dns_verified_at": null
    }
  }
}
```

## DELETE /storefront/custom-domain — Disconnect Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/storefront-commerce/custom-domain-management#delete-disconnect-custom-domain-2)

Disconnects the current tenant custom domain and archives the connection record.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/storefront/custom-domain

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success



```json
{
  "success": true,
  "message": "market_custom_domain_disconnected_successfully",
  "data": {
    "custom_domain": {
      "id": "domain-uuid",
      "custom_domain": "shop.example.com",
      "status": "archived",
      "disconnected_at": "2026-03-27T12:45:00Z"
    }
  }
}
```

## POST /express-shipping/customer/register — Register New Customer

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/customer/customer-registration#post-register-new-customer)

Register a new customer for express shipping services. If the customer already exists (based on phone number), returns existing customer data and sends a new OTP if not verified.

- Requires valid Bearer token OR X-API-Key
- Bearer token: `Authorization: Bearer {{access_token}}`
- API Key: `X-API-Key: {{api_key}}` (alternative authentication method)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/customer/register?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages (en, ar, ku) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| full_name | string | Yes | Customer's full name |  |
| email | string | Yes | Valid email address |  |
| phone | string | Yes | Phone number in international format (e.g., +919876543210) |  |
| delivery_address | object | Yes | Comprehensive address object |  |
| delivery_address.title | string |  |  |  |
| delivery_address.latitude | number |  |  |  |
| delivery_address.longitude | number |  |  |  |
| delivery_address.full_address | string | Yes | Complete address text |  |
| delivery_address.address_line1 | string | No | Primary address line |  |
| delivery_address.address_line2 | string | No | Secondary address line |  |
| delivery_address.street | string | No | Street name |  |
| delivery_address.building | string | No | Building name/number |  |
| delivery_address.floor | string | No | Floor number |  |
| delivery_address.city | string | No | City name |  |
| delivery_address.state | string | No | State/Province |  |
| delivery_address.country | string | No | Country name |  |
| delivery_address.postal_code | string | No | ZIP/Postal code |  |
| delivery_address.notes | string | No | Additional delivery notes |  |
| tenant_id | string |  |  |  |
| delivery_address.lat |  | Yes | Latitude coordinates (-90 to 90) |  |
| delivery_address.lng |  | Yes | Longitude coordinates (-180 to 180) |  |



### Request body (raw)

```json
{
  "full_name": "John Doe",
  "email": "john.doe@example.com",
  "phone": "+919876543210",
  "delivery_address": {
    "title": "Home",
    "latitude": 19.076,
    "longitude": 72.8777,
    "full_address": "123 Main Street, Andheri West, Mumbai, Maharashtra 400058, India",
    "address_line1": "123 Main Street",
    "address_line2": "Apt 4B",
    "street": "Main Street",
    "building": "Sunrise Apartments",
    "floor": "4th Floor",
    "city": "Mumbai",
    "state": "Maharashtra",
    "country": "India",
    "postal_code": "400058",
    "notes": "Near the temple, blue building with security guard"
  },
  "tenant_id": "{{tenant_id}}"
}
```

### Response 200 Success - New Customer

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "customer_id": "GAL1704067200000",
    "is_new_customer": true,
    "is_verified": false,
    "phone": "+919876543210",
    "full_name": "John Doe",
    "email": "john.doe@example.com"
  },
  "message": "Customer registered successfully. OTP sent to your phone."
}
```

### Response 200 Success - Existing Verified Customer

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "customer_id": "GAL1704067200000",
    "is_new_customer": false,
    "is_verified": true,
    "phone": "+919876543210",
    "full_name": "John Doe",
    "email": "john.doe@example.com"
  },
  "message": "Customer already exists and is verified."
}
```

### Response 400 Error - Invalid Address

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "delivery_address must contain lat, lng, and full_address"
}
```

### Response 400 Error - Missing Required Fields (400 Bad Request)



```json
{
  "success": false,
  "message": "Missing required fields: full_name, email"
}
```

### Response 400 Error - Invalid Coordinates (400 Bad Request)



```json
{
  "success": false,
  "message": "Invalid coordinates. Latitude must be between -90 and 90, longitude between -180 and 180"
}
```

## POST /express-shipping/customer/verify-otp — Verify OTP

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/customer/otp-verification#post-verify-otp)

Verify the OTP code sent to customer's phone number. Maximum 5 attempts allowed before code expires.

- Requires valid Bearer token OR X-API-Key
- Bearer token: `Authorization: Bearer {{access_token}}`
- API Key: `X-API-Key: {{api_key}}` (alternative authentication method)

### Response

- Note: `access_token` must be sent as `X-EXPRESS-KEY` for order APIs.
- Token TTL: ~5 minutes. Renew by re-verifying if expired.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/customer/verify-otp?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages (en, ar, ku) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| phone | string | Yes | Phone number in international format |  |
| code | string | Yes | 6-digit OTP code received via SMS |  |



### Request body (raw)

```json
{
  "phone": "+919876543210",
  "code": "123456"
}
```

### Response 200 Success - OTP Verified

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "customer_id": "GAL1704067200000",
    "full_name": "John Doe",
    "email": "john.doe@example.com",
    "phone": "+919876543210",
    "is_verified": true,
    "verified_at": "2024-01-01T12:00:00Z"
  },
  "message": "OTP verified successfully. Customer is now verified."
}
```

### Response 400 Error - Invalid Code

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid verification code",
  "attempts_remaining": 3
}
```

### Response 400 Error - Code Expired

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Verification code has expired. Please request a new OTP."
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "data": {
    "user_id": "UUID",
    "customer_id": "GAL1704067200000",
    "full_name": "John Doe",
    "email": "john.doe@example.com",
    "phone": "+919876543210",
    "address_id": "UUID",
    "delivery_address": { /* ... */ },
    "access_token": "<short-lived-jwt>"
  },
  "message": "OTP verified successfully. Customer is now verified."
}
```

### Response 400 Error - Too Many Attempts (400 Bad Request)



```json
{
  "success": false,
  "message": "Too many failed attempts. Please request a new OTP."
}
```

## POST /express-shipping/customer/resend-otp — Resend OTP

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/customer/otp-verification#post-resend-otp)

Resend OTP code. One of `phone` or `customer_id` is required. If `customer_id` is provided, the API resolves the customer's phone and sends the OTP.

### Response

Errors include rate limiting, already verified, and customer not found.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/customer/resend-otp?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages (en, ar, ku) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| phone | string |  | International phone number |  |
| customer_id | string |  | Customer id (e.g., GAL1704067200000) |  |



### Request body (raw)

```json
{
  "phone": "+919876543210",
  "customer_id": ""
}
```

### Response 200 Success - OTP Sent

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "OTP sent successfully to your phone."
}
```

### Response 429 Error - Rate Limited

Too Many Requests

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Please wait before requesting another OTP",
  "wait_time": 45
}
```

### Response 400 Error - Already Verified

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Customer is already verified"
}
```

## GET /express-shipping/customer/GAL1704067200000 — Get Customer Details

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/customer/customer-management#get-get-customer-details)

Retrieve customer details by customer ID. Returns comprehensive customer information including delivery address.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/customer/GAL1704067200000?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| customer_id | path string | Yes | Customer identifier (e.g., GAL1704067200000) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Preferred language for response messages (en, ar, ku) | en |



### Response 200 Success - Customer Details

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "customer_id": "GAL1704067200000",
    "full_name": "John Doe",
    "email": "john.doe@example.com",
    "phone": "+919876543210",
    "delivery_address": {
      "lat": 19.076,
      "lng": 72.8777,
      "full_address": "123 Main Street, Andheri West, Mumbai, Maharashtra 400058, India",
      "address_line1": "123 Main Street",
      "address_line2": "Apt 4B",
      "street": "Main Street",
      "building": "Sunrise Apartments",
      "floor": "4th Floor",
      "city": "Mumbai",
      "state": "Maharashtra",
      "country": "India",
      "postal_code": "400058",
      "notes": "Near the temple, blue building with security guard"
    },
    "is_verified": true,
    "status": "verified",
    "verified_at": "2024-01-01T12:00:00Z",
    "created_at": "2024-01-01T10:00:00Z"
  }
}
```

### Response 404 Error - Customer Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Customer not found"
}
```

### Response 400 Error - Missing Tenant ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Missing required parameter: tenant_id"
}
```

## GET /express-shipping/health — Health Check

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/customer/system#get-health-check)

Check the health status of the Express Shipping API service and get available endpoints.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/health?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Preferred language for response messages (en, ar, ku) | en |



### Response 200 Success - API Health

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Express Shipping API is running",
  "timestamp": "2024-01-01T12:00:00Z",
  "endpoints": [
    "POST /express-shipping/customer/register - Register customer with comprehensive address",
    "POST /express-shipping/customer/verify-otp - Verify OTP sent to phone",
    "POST /express-shipping/customer/resend-otp - Resend OTP to phone",
    "GET /express-shipping/customer/{customer_id}?tenant_id=xxx - Get customer details"
  ]
}
```

## GET /express-shipping/customer/config — Get Customer Portal Configuration

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/customer/configuration#get-get-customer-portal-configuration)

Get only the configuration JSON for the tenant's express shipping service.

- Requires valid Bearer token OR X-API-Key

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/customer/config?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Tenant identifier (required if not provided by auth context) | {{tenant_id}} |
| language | query string | No | Preferred language for response messages (en, ar, ku) Response language (en, ar, ku) | en |



### Response 200 Success - Configuration Only

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Configuration retrieved successfully",
  "configuration": {
    "brand_settings": {
      "brand": {
        "name": {
          "ar": "Amazon",
          "en": "Amazon",
          "ku": "Amazon"
        },
        "slug": "",
        "base_url": "https://portal.brand.com",
        "direction": "ltr",
        "default_locale": "en",
        "supported_locales": [
          "en",
          "ar",
          "kr"
        ]
      },
      "theme": {
        "colors": {
          "card": "hsl(0 0% 100%)",
          "ring": "hsl(222.2 47.4% 11.2%)",
          "input": "hsl(214.3 31.8% 91.4%)",
          "muted": "hsl(210 40% 96.1%)",
          "accent": "hsl(210 40% 96.1%)",
          "border": "hsl(214.3 31.8% 91.4%)",
          "chart-1": "hsl(209 100% 60%)",
          "chart-2": "hsl(203 100% 50%)",
          "chart-3": "hsl(266 100% 60%)",
          "chart-4": "hsl(126 100% 60%)",
          "chart-5": "hsl(116 100% 60%)",
          "popover": "hsl(0 0% 100%)",
          "primary": "#BC1212",
          "sidebar": "hsl(0 0% 100%)",
          "secondary": "hsl(210 40% 96.1%)",
          "background": "hsl(0 0% 100%)",
          "foreground": "hsl(0 0% 10%)",
          "destructive": "hsl(0 84.2% 60.2%)",
          "sidebar-ring": "hsl(222.2 47.4% 11.2%)",
          "sidebar-accent": "hsl(210 40% 96.1%)",
          "sidebar-border": "hsl(214.3 31.8% 91.4%)",
          "card-foreground": "hsl(0 0% 10%)",
          "sidebar-primary": "hsl(222.2 47.4% 11.2%)",
          "muted-foreground": "hsl(215.4 16.3% 46.9%)",
          "accent-foreground": "hsl(222.2 47.4% 11.2%)",
          "popover-foreground": "hsl(0 0% 10%)",
          "primary-foreground": "hsl(210 40% 98%)",
          "sidebar-foreground": "hsl(0 0% 10%)",
          "secondary-foreground": "hsl(222.2 47.4% 11.2%)",
          "destructive-foreground": "hsl(210 40% 98%)",
          "sidebar-accent-foreground": "hsl(222.2 47.4% 11.2%)",
          "sidebar-primary-foreground": "hsl(210 40% 98%)"
        }
      },
      "assets": {
        "logos": {
          "favicon": "https://ksssjpqtoynogyuezjxx.supabase.co/storage/v1/object/public/tenants/a1b2c3d4-e5f6-4321-8901-123456789abc/brand/painting-mountain-lake-with-mountain-background.jpg/1756629024617-1a3c19e5-37b5-4879-a20e-7822186e141e",
          "logo_alt": "Brand logo",
          "footer_dark": "",
          "header_dark": "",
          "footer_light": "https://ksssjpqtoynogyuezjxx.supabase.co/storage/v1/object/public/tenants/a1b2c3d4-e5f6-4321-8901-123456789abc/brand/Ondi%20logo%201024%20by%201024%20(1).jpg/1756707864850-1a3c19e5-37b5-4879-a20e-7822186e141e",
          "header_light": "https://ksssjpqtoynogyuezjxx.supabase.co/storage/v1/object/public/tenants/a1b2c3d4-e5f6-4321-8901-123456789abc/brand/Ondi%20logo%201024%20by%201024%20(1).jpg/1756707864850-1a3c19e5-37b5-4879-a20e-7822186e141e"
        }
      },
      "footer": {
        "legal": {
          "terms_url": "",
          "use_global": true,
          "cookie_policy_url": "",
          "privacy_policy_url": ""
        },
        "style": "columns",
        "address": "full address",
        "columns": [
          {
            "links": [
              {
                "href": "/about",
                "label": "About",
                "external": false
              },
              {
                "href": "/careers",
                "label": "Careers",
                "external": false
              }
            ],
            "title": "Company"
          },
          {
            "links": [
              {
                "href": "/help",
                "label": "Help Center",
                "external": false
              },
              {
                "href": "/contact",
                "label": "Contact",
                "external": false
              }
            ],
            "title": "Resources"
          }
        ],
        "social_links": {
          "github": "",
          "tiktok": "",
          "discord": "",
          "twitter": "",
          "youtube": "",
          "facebook": "",
          "linkedin": "",
          "telegram": "",
          "whatsapp": "",
          "instagram": ""
        },
        "copyright_text": "© 2 {{brand_name}}. All rights reserved.",
        "right_side_text": ""
      },
      "contact": {
        "email": "support@brand.com",
        "phone": "",
        "address": "full address",
        "support_hours": "Mon–Fri 9:00–17:00"
      },
      "contact_form": {
        "destination": {
          "cc": [
            "arun@gmail.com"
          ],
          "to": [
            "arun@builderking.io"
          ],
          "bcc": [
            "dfdasf@gmail.com"
          ],
          "type": "email",
          "webhook_url": ""
        }
      }
    }
  },
  "configured": true
}
```

## GET /express-shipping/config — Get Tenant Configuration

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/configuration/tenant-configuration#get-get-tenant-configuration)

## API: Get Tenant Configuration
### Endpoint

`GET /express-shipping/config`

### Purpose

Retrieve current express shipping configuration for the authenticated tenant. Returns pricing, service settings, and operational parameters.

- Bearer token: `Authorization: Bearer {{access_token}}` (required)
- API Key: `X-API-Key: {{api_key}}` (alternative authentication method)

### Business Logic
1. Validate tenant authentication and permissions
2. Retrieve current express shipping configuration
3. Return comprehensive configuration settings
4. Include service status and pricing information

### Error Responses

### 401 - Unauthorized

### 403 - Forbidden

### 500 - Internal Server Error

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/config?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token authentication (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | (string, optional): Preferred language for response messages (en, ar, ku) | en |



### Response 200 Success Response - Configuration

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "tenant_id": "tenant_12345",
    "service_enabled": true,
    "configuration": {
      "pricing": {
        "base_price_per_kg": 10,
        "minimum_price": 5,
        "currency": "USD",
        "price_calculation_method": "weight_based",
        "free_shipping_threshold": 100
      },
      "service_settings": {
        "max_package_weight_kg": 30,
        "max_package_dimensions_cm": {
          "length": 120,
          "width": 80,
          "height": 80
        },
        "delivery_time_days": {
          "express": "3-5",
          "economy": "7-14"
        },
        "supported_countries": [
          "US",
          "CA",
          "GB",
          "DE",
          "FR"
        ],
        "restricted_items": [
          "batteries",
          "liquids",
          "fragile_electronics"
        ]
      },
      "operational": {
        "auto_confirm_orders": false,
        "require_customer_verification": true,
        "send_sms_notifications": true,
        "send_email_notifications": true,
        "tracking_updates_frequency": "real_time"
      },
      "integration": {
        "api_keys": {
          "tracking_provider": "configured",
          "sms_provider": "configured",
          "email_provider": "configured"
        },
        "webhook_endpoints": {
          "status_updates": "https://your-domain.com/webhooks/express-shipping",
          "delivery_confirmations": "https://your-domain.com/webhooks/deliveries"
        }
      }
    },
    "last_updated": "2024-01-27T10:00:00Z",
    "created_at": "2024-01-01T00:00:00Z"
  }
}
```

### Response 404 Error - Configuration Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Express shipping configuration not found for this tenant. Please contact support to enable the service."
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Authentication required. Please provide a valid access token."
}
```

### Response 403 403 - Forbidden



```json
{
  "success": false,
  "message": "Access denied. Insufficient permissions to view configuration."
}
```

### Response 500 500 - Internal Server Error



```json
{
  "success": false,
  "message": "Failed to retrieve configuration. Please try again."
}
```

## PUT /express-shipping/config — Update Tenant Configuration

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/configuration/tenant-configuration#put-update-tenant-configuration)

## API: Update Tenant Configuration
### Endpoint

`PUT /express-shipping/config`

### Purpose

Update express shipping configuration for the authenticated tenant. Allows modification of pricing, service settings, and operational parameters.

- Bearer token: `Authorization: Bearer {{access_token}}` (required)
- API Key: `X-API-Key: {{api_key}}` (alternative authentication method)
- Requires admin permissions for the tenant

### Business Logic
1. Validate tenant authentication and admin permissions
2. Validate configuration parameters and constraints
3. Update configuration settings (partial update supported)
4. Validate pricing and service settings for consistency
5. Return updated configuration

### Error Responses

### 400 - Invalid Configuration

### 403 - Insufficient Permissions

### 404 - Configuration Not Found

### 500 - Update Failed

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/express-shipping/config?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token authentication (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Preferred language for response messages (en, ar, ku) (string, optional): Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| configuration | object | Yes | (object, required): Configuration object to update |  |
| configuration.pricing | object | No | (object, optional): Pricing configuration |  |
| configuration.pricing.base_price_per_kg | number |  | (number): Base price per kilogram |  |
| configuration.pricing.minimum_price | number |  | (number): Minimum shipping price |  |
| configuration.pricing.currency | string |  | (string): Currency code (USD, EUR, etc.) |  |
| configuration.pricing.price_calculation_method | string |  | (string): 'weight_based' or 'fixed_rate' |  |
| configuration.pricing.free_shipping_threshold | number |  | (number): Order value for free shipping |  |
| configuration.service_settings | object | No | (object, optional): Service limitation settings |  |
| configuration.service_settings.max_package_weight_kg | number |  | (number): Maximum package weight in kg |  |
| configuration.service_settings.max_package_dimensions_cm | object |  | (object): Maximum dimensions |  |
| configuration.service_settings.max_package_dimensions_cm.length | number |  | (number): Maximum length in cm |  |
| configuration.service_settings.max_package_dimensions_cm.width | number |  | (number): Maximum width in cm |  |
| configuration.service_settings.max_package_dimensions_cm.height | number |  | (number): Maximum height in cm |  |
| configuration.service_settings.delivery_time_days | object |  | (object): Estimated delivery times |  |
| configuration.service_settings.delivery_time_days.express | string |  | (string): Express delivery time range |  |
| configuration.service_settings.delivery_time_days.economy | string |  | (string): Economy delivery time range |  |
| configuration.service_settings.supported_countries | array<string> |  | (array): List of supported country codes |  |
| configuration.service_settings.restricted_items | array<string> |  | (array): List of restricted item categories |  |
| configuration.operational | object | No | (object, optional): Operational settings |  |
| configuration.operational.auto_confirm_orders | boolean |  | (boolean): Auto-confirm orders without review |  |
| configuration.operational.require_customer_verification | boolean |  | (boolean): Require customer phone verification |  |
| configuration.operational.send_sms_notifications | boolean |  | (boolean): Enable SMS notifications |  |
| configuration.operational.send_email_notifications | boolean |  | (boolean): Enable email notifications |  |
| configuration.operational.tracking_updates_frequency | string |  | (string): 'real_time', 'hourly', 'daily' |  |
| configuration.integration | object | No | (object, optional): Integration settings |  |
| configuration.integration.webhook_endpoints | object |  | (object): Webhook URLs |  |
| configuration.integration.webhook_endpoints.status_updates | string |  | (string): URL for status update webhooks |  |
| configuration.integration.webhook_endpoints.delivery_confirmations | string |  | (string): URL for delivery confirmation webhooks |  |



### Request body (raw)

```json
{
  "configuration": {
    "pricing": {
      "base_price_per_kg": 12,
      "minimum_price": 6,
      "currency": "USD",
      "price_calculation_method": "weight_based",
      "free_shipping_threshold": 150
    },
    "service_settings": {
      "max_package_weight_kg": 25,
      "max_package_dimensions_cm": {
        "length": 100,
        "width": 70,
        "height": 70
      },
      "delivery_time_days": {
        "express": "2-4",
        "economy": "5-10"
      },
      "supported_countries": [
        "US",
        "CA",
        "GB",
        "DE",
        "FR",
        "IT"
      ],
      "restricted_items": [
        "batteries",
        "liquids",
        "fragile_electronics",
        "hazardous_materials"
      ]
    },
    "operational": {
      "auto_confirm_orders": true,
      "require_customer_verification": true,
      "send_sms_notifications": true,
      "send_email_notifications": true,
      "tracking_updates_frequency": "hourly"
    },
    "integration": {
      "webhook_endpoints": {
        "status_updates": "https://your-domain.com/webhooks/express-shipping-v2",
        "delivery_confirmations": "https://your-domain.com/webhooks/deliveries-v2"
      }
    }
  }
}
```

### Response 200 Success Response - Configuration Updated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "tenant_id": "tenant_12345",
    "service_enabled": true,
    "configuration": {
      "pricing": {
        "base_price_per_kg": 12,
        "minimum_price": 6,
        "currency": "USD",
        "price_calculation_method": "weight_based",
        "free_shipping_threshold": 150
      },
      "service_settings": {
        "max_package_weight_kg": 25,
        "max_package_dimensions_cm": {
          "length": 100,
          "width": 70,
          "height": 70
        },
        "delivery_time_days": {
          "express": "2-4",
          "economy": "5-10"
        },
        "supported_countries": [
          "US",
          "CA",
          "GB",
          "DE",
          "FR",
          "IT"
        ],
        "restricted_items": [
          "batteries",
          "liquids",
          "fragile_electronics",
          "hazardous_materials"
        ]
      },
      "operational": {
        "auto_confirm_orders": true,
        "require_customer_verification": true,
        "send_sms_notifications": true,
        "send_email_notifications": true,
        "tracking_updates_frequency": "hourly"
      }
    },
    "last_updated": "2024-01-27T15:30:00Z",
    "created_at": "2024-01-01T00:00:00Z"
  },
  "message": "Configuration updated successfully"
}
```

### Response 400 Error - Invalid Configuration

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid configuration parameters",
  "details": {
    "errors": [
      "base_price_per_kg must be greater than 0",
      "minimum_price must be greater than 0"
    ]
  }
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions. Admin access required to update configuration."
}
```

### Response 200 Success Response (200 OK)



```json
{
  "success": true,
  "data": {
    "tenant_id": "tenant_12345",
    "service_enabled": true,
    "configuration": {
      "pricing": {
        "base_price_per_kg": 12,
        "minimum_price": 6,
        "currency": "USD",
        "price_calculation_method": "weight_based",
        "free_shipping_threshold": 150
      },
      "service_settings": {
        "max_package_weight_kg": 25,
        "max_package_dimensions_cm": {
          "length": 100,
          "width": 70,
          "height": 70
        },
        "delivery_time_days": {
          "express": "2-4",
          "economy": "5-10"
        },
        "supported_countries": [
          "US",
          "CA",
          "GB",
          "DE",
          "FR",
          "IT"
        ],
        "restricted_items": [
          "batteries",
          "liquids",
          "fragile_electronics",
          "hazardous_materials"
        ]
      },
      "operational": {
        "auto_confirm_orders": true,
        "require_customer_verification": true,
        "send_sms_notifications": true,
        "send_email_notifications": true,
        "tracking_updates_frequency": "hourly"
      },
      "integration": {
        "api_keys": {
          "tracking_provider": "configured",
          "sms_provider": "configured",
          "email_provider": "configured"
        },
        "webhook_endpoints": {
          "status_updates": "https://your-domain.com/webhooks/express-shipping-v2",
          "delivery_confirmations": "https://your-domain.com/webhooks/deliveries-v2"
        }
      }
    },
    "last_updated": "2024-01-27T15:30:00Z",
    "created_at": "2024-01-01T00:00:00Z"
  },
  "message": "Configuration updated successfully"
}
```

### Response 400 400 - Invalid Configuration



```json
{
  "success": false,
  "message": "Invalid configuration parameters",
  "details": {
    "errors": [
      "base_price_per_kg must be greater than 0",
      "max_package_weight_kg cannot exceed 50kg",
      "webhook_endpoints.status_updates must be a valid URL"
    ]
  }
}
```

### Response 404 404 - Configuration Not Found



```json
{
  "success": false,
  "message": "Express shipping configuration not found. Please contact support to initialize the service."
}
```

### Response 500 500 - Update Failed



```json
{
  "success": false,
  "message": "Failed to update configuration. Please try again."
}
```

## GET /express-shipping/services — List Express Shipping Services

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/configuration/shipping-services#get-list-express-shipping-services)

## API: List Express Shipping Services
### Endpoint

`GET /express-shipping/services`

### Purpose

Retrieve list of available express shipping services and delivery options. Returns service details, pricing information, and availability.

- Bearer token: `Authorization: Bearer {{access_token}}` (required)
- API Key: `X-API-Key: {{api_key}}` (alternative authentication method)

### Business Logic
1. Validate tenant authentication
2. Retrieve available shipping services based on filters
3. Calculate pricing based on current configuration
4. Return service options with delivery estimates

### Error Responses

### 400 - Invalid Country Code

### 401 - Unauthorized

### 404 - Services Not Available

### 500 - Internal Server Error

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/services?language=en&active_only=true

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token authentication (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | (string, optional): Preferred language for response messages (en, ar, ku) | en |
| active_only | query string | No | (boolean, optional): Return only active services (default: true) | true |
| country | query string | No | (string, optional): Filter services by destination country code (ISO 2-letter) |  |



### Response 200 Success Response - Services List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "services": [
      {
        "service_id": "express_international",
        "name": "Express International",
        "description": "Fast international shipping for urgent deliveries",
        "type": "express",
        "delivery_time": "3-5 business days",
        "is_active": true,
        "supported_countries": [
          "US",
          "CA",
          "GB",
          "DE",
          "FR",
          "IT"
        ],
        "pricing": {
          "base_price": 15,
          "price_per_kg": 12,
          "minimum_price": 6,
          "currency": "USD",
          "free_shipping_threshold": 150
        },
        "limits": {
          "max_weight_kg": 25,
          "max_dimensions_cm": {
            "length": 100,
            "width": 70,
            "height": 70
          }
        },
        "features": [
          "tracking_included",
          "insurance_available",
          "signature_confirmation",
          "real_time_updates"
        ]
      },
      {
        "service_id": "economy_international",
        "name": "Economy International",
        "description": "Cost-effective international shipping for non-urgent deliveries",
        "type": "economy",
        "delivery_time": "7-14 business days",
        "is_active": true,
        "supported_countries": [
          "US",
          "CA",
          "GB",
          "DE",
          "FR",
          "IT",
          "ES",
          "NL"
        ],
        "pricing": {
          "base_price": 8,
          "price_per_kg": 6,
          "minimum_price": 4,
          "currency": "USD",
          "free_shipping_threshold": 100
        },
        "limits": {
          "max_weight_kg": 30,
          "max_dimensions_cm": {
            "length": 120,
            "width": 80,
            "height": 80
          }
        },
        "features": [
          "tracking_included",
          "basic_insurance"
        ]
      }
    ],
    "total_services": 2,
    "currency": "USD",
    "last_updated": "2024-01-27T10:00:00Z"
  }
}
```

### Response 200 Success Response - Empty Services

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "services": [],
    "total_services": 0,
    "currency": "USD",
    "message": "No active services available for the specified criteria"
  }
}
```

### Response 404 Error - Services Not Available

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Express shipping services are not available for this tenant. Please contact support."
}
```

### Response 400 400 - Invalid Country Code



```json
{
  "success": false,
  "message": "Invalid country code. Please use ISO 2-letter country codes (e.g., US, GB, DE)."
}
```

### Response 401 401 - Unauthorized



```json
{
  "success": false,
  "message": "Authentication required. Please provide a valid access token."
}
```

### Response 500 500 - Internal Server Error



```json
{
  "success": false,
  "message": "Failed to retrieve shipping services. Please try again."
}
```

## GET /express-shipping/ — Health Check

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/configuration/system#get-health-check-2)

## API: Health Check
### Endpoint

`GET /express-shipping/` or `GET /express-shipping/health`

### Purpose

Check API health status and get list of available endpoints. No authentication required.

None required (optional Bearer token for tenant-specific info)

### Business Logic
1. Return current service status and timestamp
2. Include authentication status if credentials provided
3. List all available API endpoints with descriptions
4. Include service version and build information

### Response (No Authentication)

### Note

This endpoint has no error responses - it always returns 200 OK with service status.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/?language=en

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Preferred language for response messages (en, ar, ku) (string, optional): Preferred language for response messages | en |



### Response 200 Success Response - Authenticated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Express Shipping API is running",
  "timestamp": "2024-01-27T15:30:00Z",
  "version": "1.0.0",
  "build": "2024.01.27.001",
  "tenant_id": "tenant_12345",
  "endpoints": [
    "POST /express-shipping/customer/register - Register customer with comprehensive address",
    "POST /express-shipping/customer/verify-otp - Verify OTP sent to phone",
    "POST /express-shipping/customer/resend-otp - Resend OTP to phone",
    "GET /express-shipping/customer/{customer_id} - Get customer details",
    "POST /express-shipping/order/extract - Extract data from invoice document",
    "POST /express-shipping/order/confirm - Confirm and create order after review",
    "GET /express-shipping/order/track/{tracking_number} - Track package by tracking number",
    "GET /express-shipping/order/list?customer_id=xxx - List customer packages",
    "GET /express-shipping/config - Get tenant express shipping configuration",
    "PUT /express-shipping/config - Update tenant express shipping configuration",
    "GET /express-shipping/services - List available express shipping services"
  ],
  "service_status": {
    "database": "connected",
    "ai_service": "available",
    "sms_service": "available",
    "email_service": "available",
    "storage": "available"
  }
}
```

### Response 200 Success Response - No Authentication

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Express Shipping API is running",
  "timestamp": "2024-01-27T15:30:00Z",
  "version": "1.0.0",
  "tenant_id": "not-authenticated",
  "endpoints": [
    "POST /express-shipping/customer/register - Register customer with comprehensive address",
    "POST /express-shipping/customer/verify-otp - Verify OTP sent to phone",
    "POST /express-shipping/customer/resend-otp - Resend OTP to phone",
    "GET /express-shipping/customer/{customer_id} - Get customer details",
    "POST /express-shipping/order/extract - Extract data from invoice document",
    "POST /express-shipping/order/confirm - Confirm and create order after review",
    "GET /express-shipping/order/track/{tracking_number} - Track package by tracking number",
    "GET /express-shipping/order/list?customer_id=xxx - List customer packages",
    "GET /express-shipping/config - Get tenant express shipping configuration",
    "PUT /express-shipping/config - Update tenant express shipping configuration",
    "GET /express-shipping/services - List available express shipping services"
  ]
}
```

### Response 200 Success Response (200 OK)



```json
{
  "success": true,
  "message": "Express Shipping API is running",
  "timestamp": "2024-01-27T15:30:00Z",
  "version": "1.0.0",
  "build": "2024.01.27.001",
  "tenant_id": "authenticated_tenant_id",
  "endpoints": [
    "POST /express-shipping/customer/register - Register customer with comprehensive address",
    "POST /express-shipping/customer/verify-otp - Verify OTP sent to phone",
    "POST /express-shipping/customer/resend-otp - Resend OTP to phone",
    "GET /express-shipping/customer/{customer_id} - Get customer details",
    "POST /express-shipping/order/extract - Extract data from invoice document",
    "POST /express-shipping/order/confirm - Confirm and create order after review",
    "GET /express-shipping/order/track/{tracking_number} - Track package by tracking number",
    "GET /express-shipping/order/list?customer_id=xxx - List customer packages",
    "GET /express-shipping/config - Get tenant express shipping configuration",
    "PUT /express-shipping/config - Update tenant express shipping configuration",
    "GET /express-shipping/services - List available express shipping services"
  ],
  "service_status": {
    "database": "connected",
    "ai_service": "available",
    "sms_service": "available",
    "email_service": "available",
    "storage": "available"
  }
}
```

## POST /express-shipping/order/extract — Extract Invoice Data

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/order/invoice-processing#post-extract-invoice-data)

## API: Extract Invoice Data
### Endpoint

`POST /express-shipping/order/extract`

- Use `X-EXPRESS-KEY: {{express_access_token}}` from OTP verification response.
- Do not send `customer_id`; it is read from token claims.

### Form Fields
- `invoice_file` (file, required)

### Business Logic
1. Validate tenant has active express shipping service
2. Validate file format and size
3. Process document using AI extraction service
4. Extract order details, items, and pricing information
5. Return structured data for customer review

### Error Responses

### 400 - Missing Required Fields

### 400 - Invalid File Format

### 400 - File Too Large

### 404 - Customer Not Found

### 500 - Extraction Failed

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/order/extract?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-EXPRESS-KEY | header string | Yes | Short-lived JWT from OTP verification (required) | {{express_access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages (en, ar, ku) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| invoice_file | form file |  | Invoice file (PDF, JPEG, PNG) | /path/to/file |



### Request body (formdata)

```json
{
  "invoice_file": "/path/to/file"
}
```

### Response 200 Success Response - Data Extracted

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order_number": "TREND-ORD-98765",
    "store_name": "Trendyol",
    "total_amount": 125.5,
    "currency": "USD",
    "order_date": "2024-01-15",
    "items": [
      {
        "name": "Wireless Bluetooth Headphones",
        "quantity": 1,
        "unit_price": 75,
        "total_price": 75
      },
      {
        "name": "Phone Case",
        "quantity": 2,
        "unit_price": 25.25,
        "total_price": 50.5
      }
    ],
    "tax_amount": 22.59,
    "customer_name": "John Doe",
    "customer_email": "john@example.com",
    "customer_address": "123 Main St, City",
    "invoice_number": "INV-2024-001",
    "invoice_file_url": "express/invoice/GAL1704067200000/1704067300000-invoice.pdf"
  },
  "message": "Invoice data extracted successfully. Please review and confirm."
}
```

### Response 400 Error - Invalid File Format

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unsupported file type. Supported types: pdf, image, jpeg, jpg, png"
}
```

### Response 404 Error - Customer Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Customer not found with ID: INVALID123"
}
```

### Response 400 400 - Missing Required Fields



```json
{
  "success": false,
  "message": "Missing required fields: customer_id, invoice_file"
}
```

### Response 400 400 - File Too Large



```json
{
  "success": false,
  "message": "File size exceeds maximum limit of 10MB"
}
```

### Response 404 404 - Customer Not Found



```json
{
  "success": false,
  "message": "Customer not found with ID: GAL1704067200000"
}
```

### Response 500 500 - Extraction Failed



```json
{
  "success": false,
  "message": "Failed to extract data from document. Please try again or contact support."
}
```

## POST /express-shipping/order/confirm — Confirm Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/order/order-management#post-confirm-order)

## API: Confirm Order
### Endpoint

`POST /express-shipping/order/confirm`

### Purpose

Create a package after customer reviews and confirms extracted invoice data. This is Step 2 of the order creation workflow.

- Bearer token: `Authorization: Bearer {{access_token}}` (required)
- API Key: `X-API-Key: {{api_key}}` (alternative authentication method)

### Content-Type

`application/json`

### Form Fields
- `customer_id` (string, required): Galina customer ID
- `confirmed_data` (string, required): JSON string with corrected/confirmed extracted data
  - `order_number` (string, required): Order number from store
  - `store_name` (string, required): Store/marketplace name
  - `total_amount` (number, required): Total order amount
  - `currency` (string, required): Currency code (USD, EUR, etc.)
  - `order_date` (string, optional): Order date (YYYY-MM-DD)
  - `items` (array, required): Array of order items
    - `name` (string, required): Item name
    - `quantity` (number, required): Quantity ordered
    - `unit_price` (number, optional): Price per unit
    - `total_price` (number, optional): Total price for quantity
  - `tax_amount` (number, optional): Tax amount
  - `customer_name` (string, optional): Customer name from invoice
  - `customer_email` (string, optional): Customer email from invoice
  - `customer_address` (string, optional): Customer address from invoice
  - `invoice_number` (string, optional): Invoice number
- `invoice_file` (file, required): Original invoice file (PDF, JPEG, PNG)

### Business Logic
1. Validate tenant has active express shipping service
2. Verify customer exists and is verified
3. Calculate shipping price based on weight and configuration
4. Create package record with confirmed data
5. Generate tracking number
6. Create delivery, inventory, and receiving order records
7. Store invoice file for reference

### Error Responses

### 400 - Missing Required Fields

### 400 - Invalid Order Data

### 404 - Customer Not Found

### 409 - Order Already Exists

### 500 - Order Creation Failed

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/order/confirm?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-EXPRESS-KEY | header string | Yes | Short-lived JWT from OTP verification (required) | {{express_access_token}} |
| Content-Type | header string |  | JSON request body | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages (en, ar, ku) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_number | string |  |  |  |
| store_name | string |  |  |  |
| total_amount | number |  |  |  |
| currency | string |  |  |  |
| order_date | string |  |  |  |
| items | array<object> |  |  |  |
| items.name | string |  |  |  |
| items.quantity | number |  |  |  |
| items.unit_price | number |  |  |  |
| items.total_price | number |  |  |  |
| tax_amount | number |  |  |  |
| customer_name | string |  |  |  |
| customer_email | string |  |  |  |
| customer_address | string |  |  |  |
| invoice_number | string |  |  |  |
| invoice_file_url | string |  |  |  |



### Request body (raw)

```json
{
  "order_number": "TREND-ORD-98765",
  "store_name": "Trendyol",
  "total_amount": 125.5,
  "currency": "USD",
  "order_date": "2024-01-15",
  "items": [
    {
      "name": "Wireless Bluetooth Headphones",
      "quantity": 1,
      "unit_price": 75,
      "total_price": 75
    },
    {
      "name": "Phone Case",
      "quantity": 2,
      "unit_price": 25.25,
      "total_price": 50.5
    }
  ],
  "tax_amount": 22.59,
  "customer_name": "John Doe",
  "customer_email": "john@example.com",
  "customer_address": "123 Main St, City",
  "invoice_number": "INV-2024-001",
  "invoice_file_url": "express/invoice/GAL1704067200000/1704067300000-invoice.pdf"
}
```

### Response 201 Success Response - Order Confirmed

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "package": {
      "tracking_number": "GL1706371234567",
      "order_id": "TREND-ORD-98765",
      "status": "claimed",
      "payment_status": "pending",
      "declared_value": 125.5,
      "currency": "USD",
      "store_name": "Trendyol",
      "quantity": 3,
      "weight": 350,
      "delivery_fee": 15,
      "claimed_at": "2024-01-27T10:00:00Z",
      "created_at": "2024-01-27T10:00:00Z"
    },
    "delivery": {
      "delivery_id": "DEL1706371234567",
      "status": "draft",
      "delivery_type": "express",
      "estimated_delivery": "2024-02-03"
    },
    "customer": {
      "customer_id": "GAL1704067200000",
      "full_name": "John Doe",
      "phone": "+919876543210",
      "delivery_address": {
        "full_address": "123 Main Street, Mumbai, India"
      }
    }
  },
  "message": "Order confirmed successfully. Package created and ready for shipping."
}
```

### Response 409 Error - Order Already Exists

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Order with number TREND-ORD-98765 already exists"
}
```

### Response 404 Error - Customer Not Verified

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Customer not found or not verified"
}
```

### Response 400 400 - Missing Required Fields



```json
{
  "success": false,
  "message": "Missing required fields: customer_id, confirmed_data"
}
```

### Response 400 400 - Invalid Order Data



```json
{
  "success": false,
  "message": "Invalid order data: total_amount must be greater than 0"
}
```

### Response 500 500 - Order Creation Failed



```json
{
  "success": false,
  "message": "Failed to create order. Please try again."
}
```

## GET /express-shipping/order/track/GL1706371234567 — Track Package by Tracking Number

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/order/package-tracking#get-track-package-by-tracking-number)

## API: Track Package by Tracking Number
### Endpoint

`GET /express-shipping/order/track/{tracking_number}`

### Purpose

Retrieve detailed package information by tracking number. Returns package status, payment information, and delivery details.

- Bearer token: `Authorization: Bearer {{access_token}}` (required)
- API Key: `X-API-Key: {{api_key}}` (alternative authentication method)

### Business Logic
1. Validate tenant has active express shipping service
2. Locate package using the provided tracking number
3. Verify access permissions (tenant-scoped)
4. Return comprehensive package information

### Package Status Values
- `claimed` - Package claimed by customer, pending pickup
- `received` - Package received at facility
- `in_transit` - Package in transit to destination
- `in_transit_international` - Package in international transit
- `in_warehouse` - Package at local warehouse
- `out_for_delivery` - Package out for delivery
- `delivered` - Package delivered to customer
- `exception` - Delivery exception occurred

### Payment Status Values
- `pending` - Payment not yet made
- `paid` - Payment completed
- `failed` - Payment failed
- `refunded` - Payment refunded

### Error Responses

### 400 - Missing Tracking Number

### 404 - Package Not Found

### 403 - Access Denied

### 500 - Internal Server Error

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/order/track/GL1706371234567?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| X-EXPRESS-KEY | header string | Yes | Short-lived JWT from OTP verification (required) | {{express_access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tracking_number | path string | Yes | (string, required): Package tracking number (format: GL followed by timestamp, e.g., GL1706371234567) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Preferred language for response messages (en, ar, ku) | en |



### Response 200 Success Response - Package Details

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "tracking_number": "GL1706371234567",
    "order_id": "TREND-ORD-98765",
    "status": "in_transit",
    "payment_status": "paid",
    "declared_value": 125.5,
    "currency": "USD",
    "store_name": "Trendyol",
    "quantity": 3,
    "weight": 350,
    "delivery_fee": 15,
    "claimed_at": "2024-01-27T10:00:00Z",
    "created_at": "2024-01-27T10:00:00Z",
    "last_updated": "2024-01-28T14:30:00Z"
  }
}
```

### Response 404 Error - Package Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Package with tracking number GL9999999999999 not found"
}
```

### Response 400 Error - Missing Tracking Number

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Missing required parameter: tracking_number"
}
```

### Response 404 404 - Package Not Found



```json
{
  "success": false,
  "message": "Package with tracking number GL1706371234567 not found"
}
```

### Response 403 403 - Access Denied



```json
{
  "success": false,
  "message": "Access denied. Package does not belong to your account."
}
```

### Response 500 500 - Internal Server Error



```json
{
  "success": false,
  "message": "Failed to retrieve package details. Please try again."
}
```

## GET /express-shipping/order/track-by-order/TREND-ORD-98765 — Track Package by Order ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/order/package-tracking#get-track-package-by-order-id)

## API: Track Package by Order ID
### Endpoint

`GET /express-shipping/order/track-by-order/{order_id}`

### Purpose

Retrieve minimal package tracking information by order ID. Returns current status, status history, payment status, claimed_at, and store_name.

- Bearer token: `Authorization: Bearer {{access_token}}` (required)

### Error Responses

### 400 - Missing Order ID

### 404 - Package Not Found

### 403 - Express Shipping Not Configured

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/order/track-by-order/TREND-ORD-98765?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token authentication (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | (string, required): Store order identifier (e.g., TREND-ORD-98765) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages (en, ar, ku) | en |



### Response 200 Success - Status by Order ID

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "status": "claimed",
    "status_history": [],
    "payment_status": "pending",
    "claimed_at": "2024-01-15T10:30:00Z",
    "store_name": "Trendyol"
  }
}
```

### Response 404 Error - Package Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Package with order_id TREND-ORD-99999 not found"
}
```

### Response 400 Error - Missing Order ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Missing required parameter: order_id"
}
```

### Response 403 403 - Express Shipping Not Configured



```json
{
  "success": false,
  "message": "Express shipping service is not configured for this tenant"
}
```

## GET /express-shipping/order/customer/GAL1704067200000 — List Customer Packages

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/order/package-tracking#get-list-customer-packages)

## API: List Customer Packages
### Endpoint

`GET /express-shipping/order/customer/{customer_id}`

### Purpose

Retrieve a list of all packages for a specific customer. Supports pagination and filtering.

- Bearer token: `Authorization: Bearer {{access_token}}` (required)
- API Key: `X-API-Key: {{api_key}}` (alternative authentication method)

### Business Logic
1. Validate tenant has active express shipping service
2. Verify customer exists and access permissions
3. Apply pagination and filtering
4. Return list of customer packages with summary information

### Error Responses

### 400 - Missing Customer ID

### 400 - Invalid Pagination

### 404 - Customer Not Found

### 500 - Internal Server Error

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/order/customer/GAL1704067200000?language=en&limit=20&offset=0

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token authentication (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| customer_id | path string | Yes | (string, required): Customer identifier in the URL path |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| customer_id | query string | Yes | Customer identifier (required) | GAL1704067200000 |
| language | query string | No | (string, optional): Preferred language for response messages (en, ar, ku) | en |
| limit | query string | No | (integer, optional): Maximum number of packages to return (default: 50, max: 100) | 20 |
| offset | query string | No | (integer, optional): Number of packages to skip for pagination (default: 0) | 0 |
| status | query string | No | Filter by package status (optional) (string, optional): Filter by package status (claimed, received, in_transit, delivered, etc.) |  |



### Response 200 Success Response - Package List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "packages": [
      {
        "tracking_number": "GL1706371234567",
        "order_id": "TREND-ORD-98765",
        "status": "in_transit",
        "payment_status": "paid",
        "declared_value": 125.5,
        "currency": "USD",
        "store_name": "Trendyol",
        "claimed_at": "2024-01-27T10:00:00Z",
        "created_at": "2024-01-27T10:00:00Z"
      },
      {
        "tracking_number": "GL1706371234568",
        "order_id": "AMZN-ORD-12345",
        "status": "delivered",
        "payment_status": "paid",
        "declared_value": 89.99,
        "currency": "USD",
        "store_name": "Amazon",
        "claimed_at": "2024-01-25T08:30:00Z",
        "created_at": "2024-01-25T08:30:00Z"
      }
    ],
    "total": 2,
    "limit": 20,
    "offset": 0,
    "has_more": false
  }
}
```

### Response 200 Success Response - Empty List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "packages": [],
    "total": 0,
    "limit": 20,
    "offset": 0,
    "has_more": false
  }
}
```

### Response 400 Error - Missing Customer ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Missing required parameter: customer_id"
}
```

### Response 400 400 - Invalid Pagination



```json
{
  "success": false,
  "message": "Invalid pagination parameters. Limit must be between 1 and 100."
}
```

### Response 404 404 - Customer Not Found



```json
{
  "success": false,
  "message": "Customer not found with ID: GAL1704067200000"
}
```

### Response 500 500 - Internal Server Error



```json
{
  "success": false,
  "message": "Failed to retrieve customer packages. Please try again."
}
```

## GET /express-shipping/order/list — List All Packages with Pagination and Search

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/order/package-tracking#get-list-all-packages-with-pagination-and-search)

## API: List All Packages with Pagination and Search
### Endpoint

`GET /express-shipping/order/list`

### Purpose

Retrieve all packages for a tenant with comprehensive filtering, search, and pagination capabilities. Ideal for admin dashboards and package management interfaces.

- Bearer token: `Authorization: Bearer {{access_token}}` (required)
- API Key: `X-API-Key: {{api_key}}` (alternative authentication method)

### Business Logic
1. Validate tenant has active express shipping service
2. Apply all filters and search criteria
3. Execute paginated query with proper sorting (newest first)
4. Include customer information for each package
5. Return comprehensive package list with pagination metadata

### Error Responses

### 400 - Invalid Pagination

### 403 - Express Shipping Not Configured

### 404 - Customer Not Found (when filtering by customer_id)

### 500 - Internal Server Error

### Usage Examples

1. **List all packages (first page):**
   `GET /express-shipping/order/list-all`

2. **Search by tracking number:**
   `GET /express-shipping/order/list-all?search=GL123`

3. **Filter by customer:**
   `GET /express-shipping/order/list-all?customer_id=GAL1704067200000`

4. **Filter by status:**
   `GET /express-shipping/order/list-all?status=claimed`

5. **Combine filters with pagination:**
   `GET /express-shipping/order/list-all?status=claimed&search=TREND&page=2&limit=10`

6. **Filter by payment status:**
   `GET /express-shipping/order/list-all?payment_status=pending&limit=50`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/order/list?page=1&limit=20&search=GL123&customer_id=GAL1704067200000&status=claimed&payment_status=pending&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token authentication (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | (integer, optional): Page number for pagination (default: 1) | 1 |
| limit | query string | No | (integer, optional): Maximum number of packages per page (default: 20, max: 100) | 20 |
| search | query string | No | Search by tracking number or order ID (optional) (string, optional): Search by tracking number or order ID (case-insensitive partial match) | GL123 |
| customer_id | query string | No | Filter by specific customer ID (optional) (string, optional): Filter by specific customer ID | GAL1704067200000 |
| status | query string | No | Filter by package status (optional) (string, optional): Filter by package status (claimed, received, in_transit, delivered, etc.) | claimed |
| payment_status | query string | No | Filter by payment status (optional) (string, optional): Filter by payment status (pending, paid, failed, refunded) | pending |
| language | query string | No | (string, optional): Preferred language for response messages (en, ar, ku) | en |



### Response 200 Success Response - All Packages List

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "packages": [
      {
        "tracking_number": "GL1706371234567",
        "order_id": "TREND-ORD-98765",
        "status": "claimed",
        "payment_status": "pending",
        "declared_value": 125.5,
        "currency": "USD",
        "store_name": "Trendyol",
        "weight": 1500,
        "quantity": 3,
        "claimed_at": "2024-01-27T10:00:00Z",
        "created_at": "2024-01-27T10:00:00Z",
        "updated_at": "2024-01-28T14:30:00Z",
        "customer": {
          "customer_id": "GAL1704067200000",
          "name": "John Doe",
          "email": "john@example.com",
          "phone": "+964751234567"
        }
      },
      {
        "tracking_number": "GL1706371234568",
        "order_id": "AMZN-ORD-12345",
        "status": "delivered",
        "payment_status": "paid",
        "declared_value": 89.99,
        "currency": "USD",
        "store_name": "Amazon",
        "weight": 800,
        "quantity": 1,
        "claimed_at": "2024-01-25T08:30:00Z",
        "created_at": "2024-01-25T08:30:00Z",
        "updated_at": "2024-01-26T12:15:00Z",
        "customer": {
          "customer_id": "GAL1704067200001",
          "name": "Jane Smith",
          "email": "jane@example.com",
          "phone": "+964751234568"
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 150,
      "total_pages": 8
    }
  }
}
```

### Response 200 Success Response - Search Results

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "packages": [
      {
        "tracking_number": "GL1234567890123",
        "order_id": "TREND-ORD-12345",
        "status": "in_transit",
        "payment_status": "paid",
        "declared_value": 75.25,
        "currency": "USD",
        "store_name": "Trendyol",
        "weight": 1200,
        "quantity": 2,
        "claimed_at": "2024-01-26T15:45:00Z",
        "created_at": "2024-01-26T15:45:00Z",
        "updated_at": "2024-01-27T09:20:00Z",
        "customer": {
          "customer_id": "GAL1704067200002",
          "name": "Ahmed Al-Mahmoud",
          "email": "ahmed@example.com",
          "phone": "+964751234569"
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 1,
      "total_pages": 1
    }
  }
}
```

### Response 200 Success Response - Empty Results

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "packages": [],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 0,
      "total_pages": 0
    }
  }
}
```

### Response 400 Error - Invalid Pagination

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid pagination parameters. Page must be >= 1 and limit must be between 1 and 100."
}
```

### Response 403 Error - Express Shipping Not Configured

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Express shipping service is not configured for this tenant"
}
```

### Response 404 404 - Customer Not Found (when filtering by customer_id)



```json
{
  "success": false,
  "message": "Customer not found with ID: GAL1704067200000"
}
```

### Response 500 500 - Internal Server Error



```json
{
  "success": false,
  "message": "Failed to retrieve packages. Please try again."
}
```

## GET /express-shipping/order/{{package_id}} — Get Package by ID (Admin)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/order/admin-operations#get-get-package-by-id-admin)

## API: Get Package by Database ID (Admin)
### Endpoint

`GET /express-shipping/order/{id}`

### Purpose

Tenant admins fetch full package details by primary ID, including user, delivery, inventory, invoice, fee breakdown, and assets.

Requires tenant admin or system admin.

### Success (200)

Returns a rich, user-friendly structure.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/order/{{package_id}}?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Tenant admin token | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| package_id | path string | Yes | Variable used inside the request path. |  |
| id | path string | Yes | (uuid, required): Package primary key |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language (en, ar, ku) | en |



### Response 200 OK - Package Details (Admin)



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "0024508a-4029-4e4c-a27e-d49dce13153a",
    "identifiers": {
      "tracking_number": "GL1757318461585",
      "cargo_tracking_code": "AZO593623623",
      "order_id": "GLA1AACB7890"
    },
    "status": {
      "current": "received",
      "history": [
        {
          "status": "received",
          "timestamp": "2025-09-08T08:03:37.078663+00:00"
        }
      ],
      "payment_status": "pending"
    },
    "timestamps": {
      "created_at": "2025-09-08T08:01:03.800289+00:00",
      "updated_at": "2025-09-08T08:03:37.078663+00:00",
      "claimed_at": "2025-09-08T08:01:03.767Z",
      "received_at": "2025-09-08T08:03:36.96+00",
      "delivered_at": null,
      "paid_at": null
    },
    "package": {
      "weight": 1500,
      "quantity": 3,
      "declared_value": 125.5,
      "currency": "IQD",
      "store_name": "Trendyol",
      "hs_code": null,
      "package_type": "general_goods",
      "cargo_info": [
        "1521235ATT35",
        "124125515"
      ]
    },
    "customer": {
      "id": "51061ded-83f5-4fb9-82e6-41f16f507c6e",
      "name": "John Doe",
      "email": "john@example.com",
      "phone": "+964751234567"
    },
    "delivery": {
      "id": "72fceec2-66bd-4234-a0c2-be7942da6eb2",
      "code": "DEL-0001",
      "barcode": "BR-0001",
      "status": "pending",
      "address": {
        "lat": 36.1877,
        "lng": 44.0107,
        "full_address": "123 Main Street..."
      },
      "instructions": null
    },
    "inventory_item": {
      "id": "7399a559-306d-4ffa-8d1b-bcae64edcef2",
      "sku": "GL...-1",
      "name": "Wireless Bluetooth Headphones"
    },
    "invoice": {
      "file_url": "express/invoice/GAL1704067200000/1704067300000-invoice.pdf"
    },
    "fees": {
      "fee_breakdown": {
        "domestic_fee": 20,
        "international_fee": 10.31
      },
      "delivery_fee": 30.31,
      "payment_method": null,
      "payment_transaction_id": null
    },
    "assets": {
      "shipping_label_url": null,
      "delivery_confirmation": null
    }
  }
}
```

### Response 404 Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "express_shipping_package_not_found"
}
```

## GET /express-shipping/pricing — List Pricing Tiers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/pricing#get-list-pricing-tiers)

List all pricing tiers for the tenant's configured express shipping service ordered by weight_limit.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/express-shipping/pricing?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Tenant admin token | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Optional when auth context provides tenant | {{tenant_id}} |



### Response 200 Success - Pricing List



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
[
  {
    "idx": 0,
    "id": "044532f1-9f96-416e-93cd-416ba9ccea66",
    "tenant_id": "{{tenant_id}}",
    "express_shipping_service_id": "af343f65-885d-4d6a-82ec-d8521b72672a",
    "weight_limit": "4.500",
    "base_price": "10.31",
    "is_active": true,
    "description": "Up to 4.5 kg",
    "created_at": "2025-08-22T05:25:47.155Z",
    "updated_at": "2025-08-22T05:25:47.155Z"
  }
]
```

## POST /express-shipping/pricing — Create Pricing Tier

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/pricing#post-create-pricing-tier)

Create a pricing tier for the tenant's express shipping service.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/pricing?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Optional when auth context provides tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| weight_limit | number |  |  |  |
| base_price | number |  |  |  |
| is_active | boolean |  |  |  |
| description | string |  |  |  |



### Request body (raw)

```json
{
  "weight_limit": 4.5,
  "base_price": 10.31,
  "is_active": true,
  "description": "Up to 4.5 kg"
}
```

### Response 201 Created



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "{{new_id}}",
    "tenant_id": "{{tenant_id}}",
    "express_shipping_service_id": "{{service_id}}",
    "weight_limit": "4.500",
    "base_price": "10.31",
    "is_active": true,
    "description": "Up to 4.5 kg"
  }
}
```

### Response 409 Conflict - Duplicate Active Tier



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Duplicate active weight tier for service"
}
```

## PUT /express-shipping/pricing/{{pricing_id}} — Update Pricing Tier

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/pricing#put-update-pricing-tier)

Update an existing pricing tier.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/express-shipping/pricing/{{pricing_id}}?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pricing_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| weight_limit | number |  |  |  |
| base_price | number |  |  |  |
| is_active | boolean |  |  |  |
| description | string |  |  |  |



### Request body (raw)

```json
{
  "weight_limit": 5,
  "base_price": 12.5,
  "is_active": true,
  "description": "Up to 5.0 kg"
}
```

### Response 200 OK



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "{{pricing_id}}",
    "tenant_id": "{{tenant_id}}",
    "express_shipping_service_id": "{{service_id}}",
    "weight_limit": "5.000",
    "base_price": "12.50",
    "is_active": true,
    "description": "Up to 5.0 kg"
  }
}
```

## DELETE /express-shipping/pricing/{{pricing_id}} — Delete Pricing Tier

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/pricing#delete-delete-pricing-tier)

Delete a pricing tier.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/express-shipping/pricing/{{pricing_id}}?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pricing_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |



### Response 200 OK



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true
}
```

## POST /express-shipping/pricing/sync — Bulk Sync Pricing Tiers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/pricing#post-bulk-sync-pricing-tiers)

Synchronize pricing tiers by weight for the tenant's express shipping service. Upserts existing weights, inserts new ones, and deletes missing weights.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/pricing/sync?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| items | array<object> |  |  |  |
| items.weight_limit | number |  |  |  |
| items.base_price | number |  |  |  |
| items.is_active | boolean |  |  |  |
| items.description | string |  |  |  |



### Request body (raw)

```json
{
  "items": [
    {
      "weight_limit": 1,
      "base_price": 5,
      "is_active": true,
      "description": "Up to 1kg"
    },
    {
      "weight_limit": 2.5,
      "base_price": 8.5
    },
    {
      "weight_limit": 4.5,
      "base_price": 10.31,
      "description": "Up to 4.5 kg"
    }
  ]
}
```

### Response 200 OK - Synced



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "summary": {
    "inserted": 1,
    "updated": 2,
    "deleted": 1,
    "total": 3
  },
  "data": []
}
```

## POST /express-shipping/pricing/calculate — Calculate Express Pricing

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/pricing#post-calculate-express-pricing)

Calculate domestic (warehouse → dropoff) and international fees. Weight in grams; units are computed from the "unit_size" setting (weight and weight_unit). Returns fee_breakdown and total delivery_fee. Currently only supports country_code="TUR" (Turkey). If computed units are outside the domestic delivery service range, HTTP 400 returns code `can_not_use_this_delivery_for_this_unit_size`, a localized message, and calculated/allowed unit fields.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/pricing/calculate?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Optional when auth context provides tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| dropoff_latitude | number |  |  |  |
| dropoff_longitude | number |  |  |  |
| weight | number |  |  |  |
| country_code | string |  |  |  |



### Request body (raw)

```json
{
  "dropoff_latitude": 36.19,
  "dropoff_longitude": 44.01,
  "weight": 750,
  "country_code": "TUR"
}
```

### Response 200 OK



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "fee_breakdown": {
      "domestic_fee": 2500,
      "international_fee": 10000
    },
    "delivery_fee": 12500,
    "unit_size": 1,
    "inputs": {
      "dropoff_latitude": 36.19,
      "dropoff_longitude": 44.01,
      "weight_grams": 750,
      "country_code": "TUR"
    },
    "pickup_location": {
      "latitude": 36.2,
      "longitude": 44
    },
    "delivery_service_id": "{{delivery_service_id}}",
    "express_shipping_service_id": "{{express_service_id}}"
  }
}
```

### Response 400 Validation Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "express_shipping_invalid_fields"
}
```

### Response 400 Unsupported Country Code



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "unsupported_country_code",
  "allowed": [
    "TUR"
  ]
}
```

### Response 400 Calculated Units Outside Domestic Service Range



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "can_not_use_this_delivery_for_this_unit_size",
  "message": "Your items require 6 units, but this service supports up to 4 units. Choose another service or adjust the package details.",
  "field": "unit_size",
  "calculated_unit_size": 6,
  "allowed_unit_min": 1,
  "allowed_unit_max": 4,
  "violation": "above_maximum"
}
```

## POST /express-shipping/pay — Pay For Package

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/payment#post-pay-for-package)

Initiates a NASS payment for an express shipping package. Requires tenant context.

### Response
- 200 OK: payment_id, status, redirect_url, transaction_params
- 4xx/5xx on error

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/pay

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| X-API-Key | header string | Yes | Static API key for Express Shipping (required) | {{api_key}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | string | Yes | (string, required): your package order_id |  |
| order_desc | string | No |  |  |
| back_ref | string | Yes | (string, required): where to redirect user after payment (must be http/https) |  |



### Request body (raw)

```json
{
  "order_id": "ORDER-123",
  "order_desc": "Express shipping payment",
  "back_ref": "https://app.example.com/payment/return"
}
```

### Response 200 Success - Payment Initiated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "payment_id": "pay-uuid-123",
    "status": "PENDING",
    "redirect_url": "https://nass.example/redirect",
    "transaction_params": {
      "AMOUNT": "1000",
      "CURRENCY": "368"
    }
  }
}
```

### Response 404 Error - Package Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Package not found"
}
```

### Response 502 Error - NASS Transaction Failed

Bad Gateway

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "NASS transaction failed"
}
```

## POST /express-shipping/pay/status — Check Payment Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/payment#post-check-payment-status)

Checks NASS transaction status by order_id (NASS ORDER). Updates the payment row and returns minimal status data.

### Response
- 200 OK: payment_id, order_id, status, paid, amount, currency, rrn, response_code, status_msg, timestamp
- 4xx/5xx on error

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/express-shipping/pay/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| X-API-Key | header string | Yes | Static API key for Express Shipping (required) | {{api_key}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | string | Yes | (string, required): NASS ORDER returned in transactionParams.ORDER |  |



### Request body (raw)

```json
{
  "order_id": "1234567890123456"
}
```

### Response 200 Success - Status Checked

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "payment_id": "pay-uuid-123",
    "order_id": "1234567890123456",
    "status": "SUCCESS",
    "paid": true,
    "amount": 5000,
    "currency": "IQD",
    "rrn": "505801383493",
    "response_code": "00",
    "status_msg": "Approved",
    "timestamp": "20250301104625"
  }
}
```

### Response 404 Error - Payment Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Payment not found"
}
```

## POST /express/package-entry — Package Entry

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/express#post-package-entry)

## API: Package Entry
### Endpoint

`POST /express/package-entry`

### Purpose

External endpoint for package entry. This API is called when a package is received at the facility. It either creates a new package record or updates an existing one.

- No Bearer token required

### Business Logic
1. Validate customer exists using provided customer_id
2. Check if package already exists using order_id
3. If package exists: Update weight, shipping price, tracking code, cargo information, and mark as received
4. If package doesn't exist: Create new unclaimed package with status=received, quantity=1, currency=USD
5. Calculate shipping price based on weight
6. Return package and customer details with claimed/unclaimed status

### Response Field Notes
- `is_claimed`: `true` if customer has already claimed this order with invoice, `false` if package arrived before customer claim
- `store_name`: Populated with store name if package was claimed by customer first, empty if unclaimed
- `package_type`: Populated with package type if package was claimed by customer first, empty if unclaimed (filled only after customer claim)
- `order_id`: The unique order identifier from the e-commerce platform (StarEx order_id parameter)
- `tracking_number`: Our internal Galina tracking number for this package

### Error Responses

### 400 - Missing Required Fields

### 404 - Customer Not Found

### 500 - Internal Server Error

### 500 - Service Unavailable

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/express/package-entry

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| X-API-Key | header string | Yes | API key authentication (required) header (required) | {{api_key}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| weight | number | No | (integer, optional): Package weight in grams from weighing system |  |
| customer_id | string | No | (string, optional): Customer ID to lookup the galina customer |  |
| order_id | string | No | (string, optional): Order ID that becomes our order_number |  |
| cargo_tracking_code | string | No | (string, optional): Cargo tracking code provided by shipping partner (separate from order_id) |  |
| cargo_info | array<string> | No | (array of strings, optional): Additional cargo information such as item codes, AWB numbers, etc. |  |
| label_data | object | No | (object, optional): Arbitrary JSON object for label payload/metadata |  |



### Request body (raw)

```json
{
  "weight": 1500,
  "customer_id": "GL222111",
  "order_id": "TREND-ORD-98765",
  "cargo_tracking_code": "AZO593623623",
  "cargo_info": [
    "1521235ATT35",
    "124125515"
  ],
  "label_data": {}
}
```

### Response 200 Success Response - Claimed Package

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "package": {
      "tracking_number": "GL1706371234567",
      "order_id": "TREND-ORD-98765",
      "quantity": 1,
      "price": 25.5,
      "currency": "USD",
      "store_name": "Trendyol",
      "hs_code": "0000.00.00",
      "package_type": "general_goods",
      "status": "received",
      "is_claimed": true
    },
    "customer": {
      "first_name": "Ahmed",
      "last_name": "Al-Rahman",
      "phone_number": "+964987654321",
      "address": "123 Main Street, Erbil, Iraq",
      "id_pin": "GL222111"
    },
    "label": ""
  },
  "message": "Package received successfully"
}
```

### Response 200 Success Response - Unclaimed Package

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "package": {
      "tracking_number": "GL1706371234568",
      "order_id": "NEW-ORD-12345",
      "quantity": 1,
      "price": 32.75,
      "currency": "USD",
      "store_name": "",
      "hs_code": "0000.00.00",
      "package_type": "",
      "status": "received",
      "is_claimed": false
    },
    "customer": {
      "first_name": "Fatima",
      "last_name": "Al-Zahra",
      "phone_number": "+964501234567",
      "address": "456 Al-Mansour Street, Baghdad, Iraq",
      "id_pin": "GL333222"
    },
    "label": ""
  },
  "message": "Package received successfully"
}
```

### Response 400 Error - Missing Required Fields

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error_code": "MISSING_REQUIRED_FIELDS",
  "message": "Missing required fields",
  "details": "Missing fields: weight, order_id, cargo_info"
}
```

### Response 404 Error - Customer Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error_code": "CUSTOMER_NOT_FOUND",
  "message": "Customer not found",
  "details": "Customer with customer_id INVALID123 not found"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error_code": "INTERNAL_SERVER_ERROR",
  "message": "Internal server error",
  "details": "Unable to process package update at this time"
}
```

### Response 400 400 - Missing Required Fields



```json
{
  "success": false,
  "error_code": "MISSING_REQUIRED_FIELDS",
  "message": "Missing required fields",
  "details": "Missing fields: weight, customer_id, order_id"
}
```

### Response 404 404 - Customer Not Found



```json
{
  "success": false,
  "error_code": "CUSTOMER_NOT_FOUND",
  "message": "Customer not found",
  "details": "Customer with customer_id GL222111 not found"
}
```

### Response 500 500 - Service Unavailable



```json
{
  "success": false,
  "error_code": "INTERNAL_SERVER_ERROR",
  "message": "Internal server error",
  "details": "Unable to process new package at this time"
}
```

## POST /express/package-update — Package Update

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/express#post-package-update)

## API: Package Update
### Endpoint

`POST /express/package-update`

### Purpose

Update existing express package fields by order_id (same fields accepted by package entry). Recomputes international fee and, if linked delivery exists, recomputes domestic fee and updates the delivery.

### Error Responses
- 400: `order_id is required`
- 404: `Package not found`
- 500: `Internal server error`

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/express/package-update

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| X-API-Key | header string | Yes | API key authentication (required) header (required) | {{api_key}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | string | Yes | (string, required): Order ID to locate the package |  |
| weight | number | No | (integer, optional): Updated weight in grams |  |
| cargo_tracking_code | string | No | (string \| string[], optional): External tracking code(s) |  |
| cargo_info | array<string> | No | (string[], optional): Tags like fragile, electronics |  |



### Request body (raw)

```json
{
  "order_id": "{{order_id}}",
  "weight": 3200,
  "cargo_tracking_code": "STAREX-TRACK-123",
  "cargo_info": [
    "fragile",
    "electronics"
  ]
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order_id": "TREND-ORD-98765",
    "weight": 3200,
    "cargo_tracking_code": "STAREX-TRACK-123",
    "cargo_info": [
      "fragile",
      "electronics"
    ]
  }
}
```

### Response 200 Success Response (200)



```json
{
  "success": true,
  "data": { /* updated express_shipping_packages row */ }
}
```

## POST /express/webhook-status-update — Webhook Status Update

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/express#post-webhook-status-update)

## API: Webhook Status Update
### Endpoint

`POST /express/webhookStatusUpdate`

### Purpose

External webhook for package status updates. Called whenever package status changes during transit.

- No Bearer token required

### External statuses are mapped to internal statuses
- `pending` → `pending`
- `claimedByCustomer` → `claimed`
- `executedByShipper` → `in_transit`
- `inTransitInternational` → `in_transit_international`
- `inWarehouseDomestic` → `in_warehouse`
- `inProgressDomestic` → `out_for_delivery`
- `delivered` → `delivered`
- `canceled` → `cancelled`
- `accepted_at_facility` → `in_transit`
- `package_being_packaged` → `in_transit`
- `in_transit_outside_iraq` → `in_transit_international`
- `arrived_in_iraq` → `in_warehouse`

### Business Logic
1. Locate package using the provided order_id
2. Map external status to internal status representation
3. Update package status and maintain status history
4. Webhook always returns success response for reliability

### Success Response (Update Failed)

### Note

This endpoint always returns success status even if package is not found or update fails. This is intentional webhook design to prevent unnecessary retries. The `processed` field indicates whether the update was successfully applied.

### Possible Error Codes
- `MISSING_REQUIRED_FIELDS`: Missing order_id, status, timestamp, or details
- `INTERNAL_SERVER_ERROR`: JSON parsing or severe system error

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/express/webhook-status-update

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| X-API-Key | header string | Yes | API key authentication (required) header (required) | {{api_key}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | string | Yes | (string, required): Order ID to identify the package |  |
| status | string | Yes | (string, required): New status value from external system |  |
| timestamp | string | Yes | (string, required): ISO 8601 timestamp when status change occurred |  |
| details | string | Yes | (string, required): Human-readable details about the status change |  |



### Request body (raw)

```json
{
  "order_id": "TREND-ORD-98765",
  "status": "in_transit_outside_iraq",
  "timestamp": "2025-01-27T12:00:00Z",
  "details": "Package en route to Iraq"
}
```

### Response 200 Success Response - Status Updated

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order_id": "TREND-ORD-98765",
    "status": "in_transit_outside_iraq",
    "processed": true,
    "timestamp": "2025-01-27T12:00:00Z"
  },
  "message": "Status update processed successfully"
}
```

### Response 200 Success Response - Package Not Found

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "order_id": "NON-EXISTENT-ORDER",
    "processed": false
  },
  "message": "Webhook received but package not found"
}
```

### Response 500 Error - Missing Required Fields

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error_code": "MISSING_REQUIRED_FIELDS",
  "message": "Missing required fields",
  "details": "Missing fields: timestamp, details"
}
```

### Response 200 Success Response (Package Not Found)



```json
{
  "success": true,
  "data": {
    "order_id": "TREND-ORD-98765",
    "processed": false
  },
  "message": "Webhook received but package not found"
}
```

### Response 200 Success Response (Update Failed)



```json
{
  "success": true,
  "data": {
    "order_id": "TREND-ORD-98765",
    "processed": false
  },
  "message": "Webhook received but status update failed"
}
```

### Response 500 Error Response (500 - Only for severe errors)



```json
{
  "success": false,
  "error_code": "INTERNAL_SERVER_ERROR",
  "message": "Internal server error",
  "details": "JSON parsing failed"
}
```

## POST /express/update-order/TREND-ORD-98765 — Update Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/express-shipping/express#post-update-order)

## API: Update Order
### Current implementation limitation

The router in `functions/express/index.ts` matches only `POST /express/update-order`, while `apis/order.ts` extracts `order_id` from the last URL segment. The documented `/express/update-order/:order_id` request does not reach that handler and returns 404. Treat this as an intended contract with a routing defect, and resolve the backend mismatch before using the success examples as a runnable test.

### Endpoint

`POST /express/update-order/:order_id`

### Purpose

Update order details after measurement or status changes. Only weight, size, and status fields are allowed.

### Field Restrictions
- Only `weight`, `size`, and `status` fields are accepted
- Any other fields will result in a validation error
- Status updates are handled via the database RPC function for proper history tracking

### Business Logic
1. Extract order_id from URL path
2. Validate only allowed fields are provided
3. Update weight and size directly in database
4. Update status via update_package_status RPC function
5. Return summary of updated fields

### Error Responses

### 400 - Missing Order ID

### 400 - Invalid Fields

### 404 - Order Not Found

### 500 - Update Failed

Authentication: Send an approved tenant API key in X-API-Key.

Request: POST https://staging-api.ondi.io/functions/v1/express/update-order/TREND-ORD-98765

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| X-API-Key | header string | Yes | API key authentication (required) header (required) | {{api_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | path string | Yes | (string, required): Order ID in the URL path |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| weight | number | No | (integer, optional): Updated package weight in grams |  |
| size | object | No | (object, optional): Updated package dimensions |  |
| size.length | number | No | (number, optional): Length in centimeters |  |
| size.width | number | No | (number, optional): Width in centimeters |  |
| size.height | number | No | (number, optional): Height in centimeters |  |
| status | string | No | (string, optional): Updated package status (uses update_package_status RPC) |  |



### Request body (raw)

```json
{
  "weight": 3000,
  "size": {
    "length": 35,
    "width": 25,
    "height": 15
  },
  "status": "executedByShipper"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Order updated successfully",
  "data": {
    "order_id": "TREND-ORD-98765",
    "updatedFields": {
      "weight": 3000,
      "size": {
        "length": 35,
        "width": 25,
        "height": 15
      },
      "status": "executedByShipper"
    },
    "timestamp": "2025-01-27T12:00:00Z"
  }
}
```

### Response 400 Error - Invalid Fields

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "Invalid fields for update: products, customerEmail. Only weight, size, and status are allowed"
}
```

### Response 404 Error - Order Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "Order not found"
}
```

### Response 400 400 - Missing Order ID



```json
{
  "success": false,
  "error": "Order ID is required in URL path"
}
```

### Response 500 500 - Update Failed



```json
{
  "success": false,
  "error": "Failed to update package status"
}
```

## GET /system/events — List Events

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-events#get-list-events)

Retrieves a paginated list of system events with optional filtering by event type, source module, target module, status, and entity type.

- User must have 'manage:system' permission or be a system admin

### Response
- Returns a paginated list of system events
- Includes pagination metadata (total count, page, limit)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/events?tenant_id={{tenant_id}}&event_type=&source_module=&target_module=&status=&entity_type=&page=1&limit=100

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | UUID of the tenant (optional for system admins) (optional for system admins): UUID of the tenant | {{tenant_id}} |
| event_type | query string | No | Filter by event type (optional) |  |
| source_module | query string | No | Filter by source module (optional) |  |
| target_module | query string | No | Filter by target module (optional) |  |
| status | query string | No | Filter by event status (optional, e.g., 'pending', 'processed', 'failed') Filter by event status (pending, processed, failed) |  |
| entity_type | query string | No | Filter by entity type (optional) |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 100) Number of items per page (default: 100) | 100 |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Events retrieved successfully",
  "events": [
    {
      "id": "event-uuid-1",
      "tenant_id": "tenant-uuid",
      "event_type": "inventory_updated",
      "source_module": "inventory",
      "target_module": "warehouse",
      "entity_type": "inventory_item",
      "entity_id": "item-uuid-1",
      "event_data": {
        "previous_quantity": 100,
        "new_quantity": 95,
        "warehouse_id": "warehouse-uuid-1"
      },
      "status": "processed",
      "retry_count": 0,
      "created_at": "2023-04-01T10:00:00Z",
      "processed_at": "2023-04-01T10:01:00Z",
      "error_message": null
    },
    {
      "id": "event-uuid-2",
      "tenant_id": "tenant-uuid",
      "event_type": "delivery_assigned",
      "source_module": "logistics",
      "target_module": "notifications",
      "entity_type": "delivery",
      "entity_id": "delivery-uuid-1",
      "event_data": {
        "driver_id": "driver-uuid-1",
        "delivery_date": "2023-04-05T09:00:00Z"
      },
      "status": "pending",
      "retry_count": 0,
      "created_at": "2023-04-01T11:30:00Z",
      "processed_at": null,
      "error_message": null
    }
  ],
  "page": 1,
  "limit": 100,
  "total": 2
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /system/events — Publish Event

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-events#post-publish-event)

Creates a new system event for inter-module communication. Events are used to notify other modules about changes or actions that occurred in the system.

- Must have system-generated token OR 'manage:system' permission or be a system admin

### Business Rules
- event_data must be a valid JSON object
- All required fields must be provided
- Event is created with status 'pending' and retry_count of 0

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/system/events?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | UUID of the tenant (optional if in auth context) (optional if in auth context): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| event_type | string |  |  |  |
| source_module | string |  |  |  |
| target_module | string |  |  |  |
| entity_type | string |  |  |  |
| entity_id | string |  |  |  |
| event_data | object |  |  |  |
| event_data.previous_quantity | number |  |  |  |
| event_data.new_quantity | number |  |  |  |
| event_data.warehouse_id | string |  |  |  |



### Request body (raw)

```json
{
  "event_type": "inventory_updated",
  "source_module": "inventory",
  "target_module": "warehouse",
  "entity_type": "inventory_item",
  "entity_id": "{{item_id}}",
  "event_data": {
    "previous_quantity": 100,
    "new_quantity": 95,
    "warehouse_id": "{{warehouse_id}}"
  }
}
```

### Request example 1

```json
{
  "event_type": "string", // required - Type of event (e.g., inventory_updated, delivery_assigned)
  "source_module": "string", // required - Module that published the event
  "target_module": "string", // optional - Target module for the event
  "entity_type": "string", // required - Type of entity related to the event
  "entity_id": "uuid", // required - ID of the related entity
  "event_data": {} // required - JSON object containing event payload data
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Event published successfully",
  "event": {
    "id": "event-uuid-new",
    "tenant_id": "tenant-uuid",
    "event_type": "inventory_updated",
    "source_module": "inventory",
    "target_module": "warehouse",
    "entity_type": "inventory_item",
    "entity_id": "item-uuid-1",
    "event_data": {
      "previous_quantity": 100,
      "new_quantity": 95,
      "warehouse_id": "warehouse-uuid-1"
    },
    "status": "pending",
    "retry_count": 0,
    "created_at": "2023-04-01T15:00:00Z",
    "processed_at": null,
    "error_message": null
  }
}
```

### Response 400 Error - Missing Required Field

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required entity_id"
}
```

### Response 400 Error - Invalid Event Data

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Event data must be object"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## PUT /system/events/:id — Update Event Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-events#put-update-event-status)

Updates the status of an existing system event. This endpoint is typically used by event processors to mark events as processed or failed.

- Must have system-generated token OR 'manage:system' permission or be a system admin

### Business Rules
- Status must be one of: pending, processed, failed
- If status is changed to 'processed' and processed_at is not provided, current timestamp will be used
- If status is changed to 'failed', retry_count will be incremented
- Event must exist in the system

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/system/events/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the event to update (required) | {{event_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | UUID of the tenant (optional if in auth context) (optional if in auth context): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| processed_at | string |  |  |  |
| error_message | null |  |  |  |



### Request body (raw)

```json
{
  "status": "processed",
  "processed_at": "2023-04-01T15:05:00Z",
  "error_message": null
}
```

### Request example 1

```json
{
  "status": "string", // required - New status (pending, processed, failed)
  "processed_at": "string", // optional - ISO timestamp when the event was processed
  "error_message": "string" // optional - Error message if the event processing failed
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Event updated successfully",
  "event": {
    "id": "event-uuid-2",
    "tenant_id": "tenant-uuid",
    "event_type": "delivery_assigned",
    "source_module": "logistics",
    "target_module": "notifications",
    "entity_type": "delivery",
    "entity_id": "delivery-uuid-1",
    "event_data": {
      "driver_id": "driver-uuid-1",
      "delivery_date": "2023-04-05T09:00:00Z"
    },
    "status": "processed",
    "retry_count": 0,
    "created_at": "2023-04-01T11:30:00Z",
    "processed_at": "2023-04-01T15:05:00Z",
    "error_message": null
  }
}
```

### Response 200 Success Response - Failed Status

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Event updated successfully",
  "event": {
    "id": "event-uuid-2",
    "tenant_id": "tenant-uuid",
    "event_type": "delivery_assigned",
    "source_module": "logistics",
    "target_module": "notifications",
    "entity_type": "delivery",
    "entity_id": "delivery-uuid-1",
    "event_data": {
      "driver_id": "driver-uuid-1",
      "delivery_date": "2023-04-05T09:00:00Z"
    },
    "status": "failed",
    "retry_count": 1,
    "created_at": "2023-04-01T11:30:00Z",
    "processed_at": null,
    "error_message": "Target module unavailable"
  }
}
```

### Response 400 Error - Missing Status

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required status"
}
```

### Response 400 Error - Invalid Status

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid status"
}
```

## GET /system/audit-logs — List Audit Logs

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/audit-logs#get-list-audit-logs)

Retrieves a paginated list of audit logs with optional filtering. Audit logs track all API requests and responses for security and compliance purposes.

- User must have 'manage:operations:tenant' permission or be a system admin

### Response
- Returns a paginated list of audit logs
- Includes pagination metadata (total count, page, limit)
- Each audit log entry includes:
  - Request details (method, endpoint, data)
  - Response details (status code, data)
  - Actor information (user ID)
  - Timestamps
  - IP address and user agent

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/system/audit-logs?tenant_id={{tenant_id}}&actor_id=&start_date=&end_date=&endpoint=&method=&status_code=&page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| X-API-Key | header string |  |  | {{api_key}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | UUID of the tenant (optional for system admins) (optional for system admins): UUID of the tenant | {{tenant_id}} |
| actor_id | query string | No | Filter by user ID who performed the action (optional) |  |
| start_date | query string | No | Filter by start date (ISO format, optional) Filter by start date (ISO format) |  |
| end_date | query string | No | Filter by end date (ISO format, optional) Filter by end date (ISO format) |  |
| endpoint | query string | No | Filter by API endpoint (optional) |  |
| method | query string | No | Filter by HTTP method (optional) |  |
| status_code | query string | No | Filter by HTTP status code (optional) |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 10) Number of items per page (default: 10) | 10 |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Audit logs retrieved successfully",
  "audit_logs": [
    {
      "id": "audit-uuid-1",
      "tenant_id": "tenant-uuid",
      "actor_id": "user-uuid-1",
      "endpoint": "/api/deliveries",
      "method": "POST",
      "request_data": {
        "pickup_location": "location-1",
        "delivery_location": "location-2"
      },
      "response_data": {
        "id": "delivery-uuid-1",
        "status": "created"
      },
      "status_code": 201,
      "ip_address": "192.168.1.1",
      "user_agent": "Mozilla/5.0...",
      "created_at": "2023-04-01T10:00:00Z"
    },
    {
      "id": "audit-uuid-2",
      "tenant_id": "tenant-uuid",
      "actor_id": "user-uuid-2",
      "endpoint": "/api/deliveries/delivery-uuid-1",
      "method": "PUT",
      "request_data": {
        "status": "in_transit"
      },
      "response_data": {
        "id": "delivery-uuid-1",
        "status": "in_transit"
      },
      "status_code": 200,
      "ip_address": "192.168.1.2",
      "user_agent": "Mozilla/5.0...",
      "created_at": "2023-04-01T11:30:00Z"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 2
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /system/storefront/markets — List All Markets

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/overview#get-list-all-markets)

Retrieves a paginated list of all markets across all tenants, with optional filtering by status. Requires system admin access.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/storefront/markets?page=1&limit=20&status=active&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of markets per page | 20 |
| status | query string |  | Filter by market status (e.g., 'active', 'pending', 'suspended') | active |
| language | query string |  | Preferred language for response messages | en |
| search | query string |  | Search by market name |  |



### Response 200 Success Response



```json
{
  "success": true,
  "data": {
    "markets": [
      {
        "id": "market-uuid-1",
        "tenant_id": "tenant-uuid-1",
        "store_name": "Global Mart",
        "status": "active",
        "created_at": "2023-10-01T10:00:00Z"
      },
      {
        "id": "market-uuid-2",
        "tenant_id": "tenant-uuid-2",
        "store_name": "Local Bazaar",
        "status": "active",
        "created_at": "2023-10-05T14:00:00Z"
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 2,
      "totalPages": 1
    }
  },
  "message": "Markets retrieved successfully"
}
```

## GET /system/storefront/market-domains — List Market Domains

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/market-domain-administration#get-list-market-domains)

Returns paginated custom domain records for the system storefront hosting page.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/storefront/market-domains?page=1&limit=20&status=&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| status | query string |  | pending_dns \| active \| disconnecting \| archived |  |
| search | query string |  | Search by custom domain, market slug/name, or tenant name |  |



### Response 200 Success



```json
{
  "success": true,
  "message": "market_custom_domains_retrieved_successfully",
  "data": {
    "items": [
      {
        "id": "domain-uuid",
        "market_id": "market-uuid",
        "tenant_id": "tenant-uuid",
        "custom_domain": "shop.example.com",
        "status": "active",
        "dns_verified_at": "2026-03-27T12:30:00Z",
        "market": {
          "id": "market-uuid",
          "slug": "my-store",
          "store_name": {
            "en": "My Store"
          }
        },
        "tenant": {
          "id": "tenant-uuid",
          "name": {
            "en": "Tenant One"
          }
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

## GET /system/storefront/market-domains/available-markets — List Available Markets For Domain Connection

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/market-domain-administration#get-list-available-markets-for-domain-connection)

Lists markets that do not currently have an active or pending custom domain.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/storefront/market-domains/available-markets?search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| search | query string | No | Optional market slug or store name search |  |



### Response 200 Success



```json
{
  "success": true,
  "message": "markets_retrieved_successfully",
  "data": {
    "markets": [
      {
        "id": "market-uuid",
        "tenant_id": "tenant-uuid",
        "slug": "my-store",
        "store_name": {
          "en": "My Store"
        }
      }
    ]
  }
}
```

## GET /system/storefront/market-uuid/custom-domain — Get Market Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/market-domain-administration#get-get-market-domain)

Returns the currently connected custom domain for the selected market.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/storefront/market-uuid/custom-domain

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success



```json
{
  "success": true,
  "message": "market_custom_domain_retrieved_successfully",
  "data": {
    "custom_domain": {
      "id": "domain-uuid",
      "market_id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "custom_domain": "shop.example.com",
      "status": "pending_dns"
    }
  }
}
```

## POST /system/storefront/market-uuid/custom-domain/connect — Connect Market Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/market-domain-administration#post-connect-market-domain)

Connects a custom domain to the selected market from the system admin hosting page.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/system/storefront/market-uuid/custom-domain/connect

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| custom_domain | string |  |  |  |



### Request body (raw)

```json
{
  "custom_domain": "shop.example.com"
}
```

### Response 201 Success



```json
{
  "success": true,
  "message": "market_custom_domain_connected_successfully",
  "data": {
    "custom_domain": {
      "id": "domain-uuid",
      "market_id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "custom_domain": "shop.example.com",
      "status": "pending_dns"
    }
  }
}
```

## POST /system/storefront/market-uuid/custom-domain/verify — Verify Market Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/market-domain-administration#post-verify-market-domain)

Re-checks Vercel/DNS verification for the selected market's custom domain.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/system/storefront/market-uuid/custom-domain/verify

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Verified



```json
{
  "success": true,
  "message": "market_custom_domain_verified_successfully",
  "data": {
    "verified": true,
    "custom_domain": {
      "id": "domain-uuid",
      "custom_domain": "shop.example.com",
      "status": "active",
      "dns_verified_at": "2026-03-27T12:30:00Z"
    }
  }
}
```

### Response 200 Pending DNS



```json
{
  "success": true,
  "message": "market_custom_domain_dns_pending",
  "data": {
    "verified": false,
    "custom_domain": {
      "id": "domain-uuid",
      "custom_domain": "shop.example.com",
      "status": "pending_dns",
      "dns_verified_at": null
    }
  }
}
```

## DELETE /system/storefront/market-uuid/custom-domain — Disconnect Market Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/market-domain-administration#delete-disconnect-market-domain)

Disconnects the currently connected custom domain for the selected market.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/system/storefront/market-uuid/custom-domain

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success



```json
{
  "success": true,
  "message": "market_custom_domain_disconnected_successfully",
  "data": {
    "custom_domain": {
      "id": "domain-uuid",
      "custom_domain": "shop.example.com",
      "status": "archived",
      "disconnected_at": "2026-03-27T12:45:00Z"
    }
  }
}
```

## GET /system/delivery/customer-portal-domains — List Customer Portal Domains

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/customer-portal-domain-administration#get-list-customer-portal-domains)

Returns paginated customer portal custom domain records across tenants.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/delivery/customer-portal-domains?page=1&limit=20&status=&search=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| status | query string |  | pending_dns \| dns_verified \| active \| disconnecting \| archived |  |
| search | query string |  | Search by custom domain or tenant name/slug |  |
| language | query string |  |  | en |



### Response 200 Success



```json
{
  "success": true,
  "message": "Customer portal custom domains retrieved successfully",
  "data": {
    "items": [
      {
        "id": "domain-uuid",
        "tenant_id": "tenant-uuid",
        "custom_domain": "portal.tenant.com",
        "status": "active",
        "dns_verified_at": "2026-04-30T10:00:00Z",
        "tenant": {
          "id": "tenant-uuid",
          "slug": "tenant-one",
          "name": {
            "en": "Tenant One"
          }
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

## GET /system/delivery/customer-portal-domains/available-tenants — List Available Tenants For Domain Connection

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/customer-portal-domain-administration#get-list-available-tenants-for-domain-connection)

Lists tenants that do not have an active or pending customer portal custom domain.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/delivery/customer-portal-domains/available-tenants?search=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| search | query string | No | Optional tenant slug or name search |  |
| language | query string |  |  | en |



### Response 200 Success



```json
{
  "success": true,
  "message": "Tenants retrieved successfully",
  "data": {
    "tenants": [
      {
        "id": "tenant-uuid",
        "slug": "tenant-one",
        "name": {
          "en": "Tenant One"
        }
      }
    ]
  }
}
```

## GET /system/delivery/:tenantId/customer-portal/custom-domain — Get Tenant Customer Portal Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/customer-portal-domain-administration#get-get-tenant-customer-portal-domain)

Returns the customer portal custom domain currently attached to the tenant.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/delivery/:tenantId/customer-portal/custom-domain?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes |  | tenant-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  |  | en |



### Response 200 Success



```json
{
  "success": true,
  "message": "Customer portal custom domain retrieved successfully",
  "data": {
    "custom_domain": {
      "id": "domain-uuid",
      "tenant_id": "tenant-uuid",
      "custom_domain": "portal.tenant.com",
      "status": "pending_dns"
    }
  }
}
```

## POST /system/delivery/:tenantId/customer-portal/custom-domain/connect — Connect Customer Portal Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/customer-portal-domain-administration#post-connect-customer-portal-domain)

Connects a customer portal custom domain on behalf of a tenant from the system admin console.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/system/delivery/:tenantId/customer-portal/custom-domain/connect?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes |  | tenant-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| custom_domain | string |  |  |  |



### Request body (raw)

```json
{
  "custom_domain": "portal.tenant.com"
}
```

### Response 201 Created



```json
{
  "success": true,
  "message": "Customer portal custom domain connected successfully",
  "data": {
    "custom_domain": {
      "id": "domain-uuid",
      "tenant_id": "tenant-uuid",
      "custom_domain": "portal.tenant.com",
      "status": "pending_dns"
    }
  }
}
```

## POST /system/delivery/:tenantId/customer-portal/custom-domain/verify — Verify Customer Portal Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/customer-portal-domain-administration#post-verify-customer-portal-domain)

Re-checks Vercel/DNS verification for the tenant's customer portal domain.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/system/delivery/:tenantId/customer-portal/custom-domain/verify?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes |  | tenant-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  |  | en |



### Response 200 Verified



```json
{
  "success": true,
  "message": "Customer portal custom domain verified successfully",
  "data": {
    "verified": true,
    "custom_domain": {
      "id": "domain-uuid",
      "status": "active",
      "dns_verified_at": "2026-04-30T10:00:00Z"
    }
  }
}
```

### Response 200 Pending DNS



```json
{
  "success": true,
  "message": "Customer portal custom domain DNS verification is still pending",
  "data": {
    "verified": false,
    "custom_domain": {
      "id": "domain-uuid",
      "status": "pending_dns",
      "dns_verified_at": null
    }
  }
}
```

## DELETE /system/delivery/:tenantId/customer-portal/custom-domain — Disconnect Customer Portal Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/customer-portal-domain-administration#delete-disconnect-customer-portal-domain)

Disconnects the tenant's customer portal custom domain (admin override).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/system/delivery/:tenantId/customer-portal/custom-domain?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes |  | tenant-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  |  | en |



### Response 200 Success



```json
{
  "success": true,
  "message": "Customer portal custom domain disconnected successfully",
  "data": {
    "custom_domain": {
      "id": "domain-uuid",
      "status": "archived",
      "disconnected_at": "2026-04-30T11:00:00Z"
    }
  }
}
```

## GET /system/storefront/analytics — Get Global Storefront Analytics

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/overview#get-get-global-storefront-analytics)

Retrieves global analytics and insights across all storefronts on the platform. Requires system admin access.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/storefront/analytics?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response



```json
{
  "success": true,
  "data": {
    "analytics": {
      "total_markets": 150,
      "active_markets": 135,
      "pending_markets": 10,
      "total_orders": 25430,
      "total_revenue": 1250670.5,
      "average_order_value": 49.18,
      "top_performing_market": {
        "id": "market-uuid-7",
        "store_name": "Super Store",
        "revenue": 85000
      }
    }
  }
}
```

## PUT /system/storefront/markets/:marketId/status — Update Market Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/overview#put-update-market-status)

Updates a market governance status. Allowed transitions: pending_approval -> approved, pending_approval -> rejected, approved -> suspended, suspended -> approved. Accepts either `notes` or legacy `reason` for admin comments. Requires system admin access.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/system/storefront/markets/:marketId/status?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| marketId | path string | Yes |  | market-uuid-1 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| reason | string |  |  |  |



### Request body (raw)

```json
{
  "status": "suspended",
  "reason": "Violation of terms of service."
}
```

### Response 200 Success Response



```json
{
  "success": true,
  "message": "Market suspended successfully",
  "market": {
    "id": "market-uuid-1",
    "status": "suspended",
    "admin_notes": "Violation of terms of service."
  }
}
```

### Response 404 Error - Market Not Found



```json
{
  "success": false,
  "message": "Market not found"
}
```

### Response 409 Error - Invalid Transition



```json
{
  "success": false,
  "message": "Market status transition is not allowed",
  "data": null
}
```

## GET /system/payment-methods — Get Global Payment Methods

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/manage-global-payment-methods#get-get-global-payment-methods)

Retrieves all globally configured payment methods.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/payment-methods

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success



```json
{
  "success": true,
  "data": {
    "payment_methods": [
      {
        "id": "pm-global-cod",
        "name": "Cash on Delivery",
        "is_enabled": true,
        "config": {}
      },
      {
        "id": "pm-global-wallet",
        "name": "Wallet",
        "is_enabled": true,
        "config": {}
      }
    ]
  }
}
```

## POST /system/payment-methods — Create Global Payment Method

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/storefront-administration/manage-global-payment-methods#post-create-global-payment-method)

Adds a new globally available payment method.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/system/payment-methods

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| is_enabled | boolean |  |  |  |
| config | object |  |  |  |
| config.provider | string |  |  |  |
| config.api_key | string |  |  |  |



### Request body (raw)

```json
{
  "name": "Credit Card",
  "is_enabled": true,
  "config": {
    "provider": "stripe",
    "api_key": "pk_test_..."
  }
}
```

### Response 201 Success



```json
{
    "success": true,
    "data": {
        "payment_method": { ... new payment method object ... }
    },
    "message": "Payment method created successfully"
}
```

## GET /system/restaurant/locations — List Restaurant Locations

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/restaurant-administration#get-list-restaurant-locations)

Retrieves restaurant locations across tenants for the Ondi admin portal Restaurants page. Results are grouped by tenant and paginated by tenant group. Requires `manage:system` permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/restaurant/locations?page=1&limit=10&search=&tenant_id=&city=&area=&active_status=&operational_status=&admin_status=&include_empty_tenants=false

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Tenant-group page number (default: 1) | 1 |
| limit | query string |  | Tenant groups per page (default: 10, max: 50) | 10 |
| search | query string |  | Search by tenant name, tenant slug, location name, location slug, or location code |  |
| tenant_id | query string |  | Filter by exact tenant id |  |
| city | query string |  | Filter locations by city |  |
| area | query string |  | Filter locations by area |  |
| active_status | query string |  | Filter by location active state: active \| inactive |  |
| operational_status | query string |  | Filter by runtime state: open \| closed |  |
| admin_status | query string |  | Filter by governance state: draft \| pending_approval \| approved \| rejected \| suspended |  |
| include_empty_tenants | query string |  | When true, returns matching tenants even if they currently have no matching locations | false |



### Response 200 Success Response



```json
{
  "success": true,
  "message": "Restaurants retrieved successfully",
  "data": {
    "groups": [
      {
        "tenant": {
          "id": "tenant-uuid",
          "name": {
            "en": "Tenant One"
          },
          "slug": "tenant-one",
          "status": "active"
        },
        "summary": {
          "total_locations": 2,
          "active_locations": 2,
          "inactive_locations": 0,
          "open_locations": 1,
          "closed_locations": 1
        },
        "locations": [
          {
            "id": "location-uuid",
            "code": 1001,
            "name": {
              "en": "Downtown Branch"
            },
            "slug": "downtown-branch",
            "admin_status": "approved",
            "active": true,
            "is_open": true,
            "city": "Erbil",
            "area": "Ankawa",
            "full_address": "Street 60, Ankawa, Erbil",
            "latitude": 36.23,
            "longitude": 43.98,
            "accept_dining": true,
            "accept_delivery": true,
            "accept_takeout": true,
            "created_at": "2026-03-20T12:00:00Z"
          }
        ]
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total_groups": 25,
      "total_locations": 118
    },
    "filters": {
      "search": "",
      "tenant_id": null,
      "city": null,
      "area": null,
      "active_status": null,
      "operational_status": null,
      "admin_status": null,
      "include_empty_tenants": false
    }
  }
}
```

### Response 200 Success Response - Empty Result



```json
{
  "success": true,
  "message": "Restaurants retrieved successfully",
  "data": {
    "groups": [],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total_groups": 0,
      "total_locations": 0
    },
    "filters": {
      "search": "missing-search",
      "tenant_id": null,
      "city": null,
      "area": null,
      "active_status": null,
      "operational_status": null,
      "admin_status": null,
      "include_empty_tenants": false
    }
  }
}
```

### Response 400 Error - Invalid Parameters



```json
{
  "success": false,
  "message": "Must be one of: active, inactive"
}
```

### Response 403 Error - Insufficient Permissions



```json
{
  "success": false,
  "message": "Insufficient Permissions",
  "data": null
}
```

## GET /system/restaurant/locations/:locationId — Get Restaurant Location Detail

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/restaurant-administration#get-get-restaurant-location-detail)

Retrieves one restaurant location for the Ondi admin portal detail page. Requires `manage:system` permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/restaurant/locations/:locationId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Restaurant location id | {{location_id}} |



### Response 200 Success Response



```json
{
  "success": true,
  "message": "Restaurant location retrieved successfully",
  "data": {
    "id": "location-uuid",
    "tenant_id": "tenant-uuid",
    "name": {
      "en": "Downtown Branch"
    },
    "slug": "downtown-branch",
    "active": true,
    "admin_status": "approved",
    "approved_by": "admin-user-uuid",
    "approved_at": "2026-03-20T12:05:00Z",
    "admin_notes": "Approved for public access",
    "is_open": true,
    "languages": [
      "en",
      "ar"
    ],
    "time_zone": "Asia/Baghdad",
    "created_at": "2026-03-20T12:00:00Z",
    "updated_at": "2026-03-20T12:10:00Z",
    "code": 1001,
    "latitude": 36.23,
    "longitude": 43.98,
    "address": {
      "city": "Erbil",
      "area": "Ankawa",
      "full_address": "Street 60, Ankawa, Erbil",
      "latitude": 36.23,
      "longitude": 43.98
    },
    "branding_settings": {
      "logo": "https://cdn.example.com/logo.png",
      "cover_image": "https://cdn.example.com/cover.png",
      "banners": []
    },
    "is_manual_mode": false,
    "manual_is_open": true,
    "availability_version": 12,
    "accept_dining": true,
    "accept_delivery": true,
    "accept_takeout": true,
    "tenant": {
      "id": "tenant-uuid",
      "name": {
        "en": "Tenant One"
      },
      "slug": "tenant-one",
      "status": "active"
    }
  }
}
```

### Response 404 Error - Location Not Found



```json
{
  "success": false,
  "message": "Location Not Found",
  "data": null
}
```

### Response 400 Error - Invalid Location Id



```json
{
  "success": false,
  "message": "Invalid UUID"
}
```

### Response 403 Error - Insufficient Permissions



```json
{
  "success": false,
  "message": "Insufficient Permissions",
  "data": null
}
```

## PUT /system/restaurant/locations/:locationId/status — Update Restaurant Location Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/restaurant-administration#put-update-restaurant-location-status)

Updates a restaurant location governance status. Allowed transitions: pending_approval → approved, pending_approval → rejected, approved → suspended, suspended → approved. Requires `manage:system` permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/system/restaurant/locations/:locationId/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| locationId | path string | Yes | Restaurant location id | {{location_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "status": "approved",
  "notes": "Ready for public launch"
}
```

### Response 200 Success Response - Approved



```json
{
  "success": true,
  "message": "Restaurant location approved successfully",
  "data": {
    "id": "location-uuid",
    "tenant_id": "tenant-uuid",
    "slug": "downtown-branch",
    "admin_status": "approved",
    "approved_by": "admin-user-uuid",
    "approved_at": "2026-03-20T12:05:00Z",
    "admin_notes": "Ready for public launch"
  }
}
```

### Response 200 Success Response - Suspended



```json
{
  "success": true,
  "message": "Restaurant location suspended successfully",
  "data": {
    "id": "location-uuid",
    "tenant_id": "tenant-uuid",
    "slug": "downtown-branch",
    "admin_status": "suspended",
    "approved_by": "admin-user-uuid",
    "approved_at": "2026-03-20T12:05:00Z",
    "admin_notes": "Compliance issue"
  }
}
```

### Response 200 Success Response - Rejected



```json
{
  "success": true,
  "message": "Restaurant location rejected successfully",
  "data": {
    "id": "location-uuid",
    "tenant_id": "tenant-uuid",
    "slug": "downtown-branch",
    "admin_status": "rejected",
    "approved_by": null,
    "approved_at": null,
    "admin_notes": "Does not meet quality standards"
  }
}
```

### Response 400 Error - Invalid Transition



```json
{
  "success": false,
  "message": "Restaurant location status transition is not allowed",
  "data": null
}
```

### Response 404 Error - Location Not Found



```json
{
  "success": false,
  "message": "Location Not Found",
  "data": null
}
```

## GET /system/demo-requests — List Demo Requests

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/demo-requests#get-list-demo-requests)

Retrieves a paginated list of demo requests submitted by prospective tenants. Supports filtering by status and searching.

- Requires `Bearer` token
- User must be a System Admin

### Response
- Returns list of requests with pagination metadata.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/demo-requests?page=1&limit=10&status=&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) Page number (default 1) | 1 |
| limit | query string |  | Items per page (default: 10) Items per page (default 10) | 10 |
| status | query string |  | Filter by status: Requested, Under Review, In Progress, Converted, Canceled, No Response Filter by specific status |  |
| search | query string |  | Search by name, email, or phone number Search string for name, email, or phone |  |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "data_retrieved_successfully",
  "data": [
    {
      "id": "a1b2c3d4-e5f6-7890-1234-56789abcdef0",
      "created_at": "2023-11-27T10:00:00Z",
      "updated_at": "2023-11-27T10:00:00Z",
      "name": "John Doe",
      "email": "john.doe@company.com",
      "phone_number": "+15551234567",
      "notes": "Interested in full suite",
      "interested_modules": [
        "delivery",
        "storefront"
      ],
      "status": "Requested",
      "status_history": [
        {
          "status": "Requested",
          "at": "2023-11-27T10:00:00Z"
        }
      ]
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

## PUT /system/demo-requests/:id/status — Update Demo Request Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/demo-requests#put-update-demo-request-status)

Updates the status of a demo request. Tracks the status change in the history.

- Requires `Bearer` token
- User must be a System Admin

### Response
- Returns the updated demo request object.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/system/demo-requests/:id/status

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the demo request | request-uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string | Yes | New status. Valid values: `Requested` `Under Review` `In Progress` `Converted` `Canceled` `No Response` |  |



### Request body (raw)

```json
{
  "status": "In Progress"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "status_updated_successfully",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-1234-56789abcdef0",
    "status": "Under Review",
    "status_history": [
      {
        "status": "Requested",
        "at": "2023-11-27T10:00:00Z"
      },
      {
        "status": "Under Review",
        "at": "2023-11-27T10:30:00Z",
        "by": "admin-uuid"
      }
    ],
    "updated_at": "2023-11-27T10:30:00Z"
  }
}
```

### Response 400 Error - Invalid Status

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_status_transition"
}
```

## GET /system/notification-templates — List Notification Templates

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/notification-templates#get-list-notification-templates)

Browse notification templates used by the system’s notification flows.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/notification-templates?page=1&limit=50

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 50 |
| search | query string | No |  |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "templates": [
    {
      "id": "uuid",
      "key": "assignment_accepted",
      "channels": [
        "in_app",
        "push"
      ],
      "title_template": "Delivery accepted",
      "body_template": "{{driverName}} accepted delivery #{{deliveryCode}}.",
      "variables": [
        "driverName",
        "deliveryCode"
      ],
      "actions": {
        "dispatcher": {
          "target": "internal",
          "path": "/operations/deliveries/{{deliveryId}}"
        }
      },
      "is_active": true,
      "updated_at": "2026-04-03T12:00:00Z"
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 50
}
```

## GET /system/notification-templates/assignment_accepted — Get Notification Template

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/notification-templates#get-get-notification-template)

Read one notification template, including its declared variables and portal actions.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system/notification-templates/assignment_accepted

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "template": {
    "id": "uuid",
    "key": "assignment_accepted",
    "channels": [
      "in_app",
      "push"
    ],
    "title_template": "Delivery accepted",
    "body_template": "{{driverName}} accepted delivery #{{deliveryCode}}.",
    "variables": [
      "driverName",
      "deliveryCode",
      "deliveryId"
    ],
    "actions": {
      "dispatcher": {
        "target": "internal",
        "path": "/operations/deliveries/{{deliveryId}}"
      },
      "driver_app": {
        "target": "internal",
        "path": "/driver/deliveries/{{deliveryId}}"
      }
    },
    "is_active": true,
    "engine": "handlebars",
    "tenant_id": null
  }
}
```

## PATCH /system/notification-templates/assignment_accepted/actions — Upsert Portal Action

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/notification-templates#patch-upsert-portal-action)

As a system administrator, replace one portal’s notification navigation action while preserving actions for other portals. Paths may reference only variables already declared by the template.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/system/notification-templates/assignment_accepted/actions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| portal | string |  |  |  |
| target | string |  |  |  |
| path | string |  |  |  |



### Request body (raw)

```json
{
  "portal": "dispatcher",
  "target": "internal",
  "path": "/operations/deliveries/{{deliveryId}}"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "actions": {
    "dispatcher": {
      "target": "internal",
      "path": "/operations/deliveries/{{deliveryId}}"
    }
  }
}
```

### Response 400 Error - Undeclared Variable

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "action_path_contains_undeclared_variables",
  "undeclared_variables": [
    "unknownVar"
  ],
  "declared_variables": [
    "driverName",
    "deliveryCode",
    "deliveryId"
  ]
}
```

## DELETE /system/notification-templates/assignment_accepted/actions/dispatcher — Delete Portal Action

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/notification-templates#delete-delete-portal-action)

As a system administrator, remove one portal’s navigation action from a notification template. Other portal actions remain unchanged.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/system/notification-templates/assignment_accepted/actions/dispatcher

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "actions": null
}
```

## PATCH /system/notification-templates/assignment_accepted/variables — Update Template Variables

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/notification-templates#patch-update-template-variables)

As a system administrator, replace a notification template’s declared variable list. Variables still referenced by the title, body or action paths cannot be removed.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/system/notification-templates/assignment_accepted/variables

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| variables | array<string> |  |  |  |



### Request body (raw)

```json
{
  "variables": [
    "driverName",
    "deliveryCode",
    "deliveryId"
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "variables": [
    "driverName",
    "deliveryCode",
    "deliveryId"
  ]
}
```

### Response 400 Error - Removing Referenced Variable

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "cannot_remove_referenced_variables",
  "referenced_variables": [
    "driverName",
    "deliveryId"
  ]
}
```

## PUT /system-brand-settings/:id — Update Brand Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-brand-settings#put-update-brand-settings)

Update system brand settings. Requires system admin access. All fields are optional but must match their expected data types.

### Validation Rules
- `brand_name` must be a JSONB object
- `assets` must be a JSONB object
- `colors` must be a JSONB object
- `configurations` must be a JSONB object
- `landing_page` must be a JSONB object
- `is_active` must be a boolean
- `default_preset_id` must be a string

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: PUT https://staging-api.ondi.io/functions/v1/system-brand-settings/:id?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token for authentication (required) | Bearer {{access_token}} |
| Content-Type | header string | Yes | application/json (required) | application/json |
| X-API-Key | header string |  | Alternative authentication method using API key |  |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | ID of the system brand settings to update |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| brand_name | object | No | JSONB object with language keys |  |
| brand_name.en | string |  |  |  |
| brand_name.ar | string |  |  |  |
| brand_name.ku | string |  |  |  |
| landing_page | object | No | JSONB object for landing page visibility, hero, docs, quick access, modules, and auth images |  |
| landing_page.book_demo_link | object |  |  |  |
| landing_page.book_demo_link.visible | boolean |  |  |  |
| landing_page.hero | object |  |  |  |
| landing_page.hero.title | string |  |  |  |
| landing_page.hero.subtitle | string |  |  |  |
| landing_page.hero.description | string |  |  |  |
| landing_page.hero.image | string |  |  |  |
| landing_page.documentation | object |  |  |  |
| landing_page.documentation.visible | boolean |  |  |  |
| landing_page.documentation.platform | object |  |  |  |
| landing_page.documentation.platform.visible | boolean |  |  |  |
| landing_page.documentation.platform.url | string |  |  |  |
| landing_page.documentation.api | object |  |  |  |
| landing_page.documentation.api.visible | boolean |  |  |  |
| landing_page.documentation.api.url | string |  |  |  |
| landing_page.quick_access | object |  |  |  |
| landing_page.quick_access.visible | boolean |  |  |  |
| landing_page.quick_access.items | array<object> |  |  |  |
| landing_page.quick_access.items.id | string |  |  |  |
| landing_page.quick_access.items.visible | boolean |  |  |  |
| landing_page.quick_access.items.title | string |  |  |  |
| landing_page.quick_access.items.description | string |  |  |  |
| landing_page.quick_access.items.image | string |  |  |  |
| landing_page.quick_access.items.link_text | string |  |  |  |
| landing_page.quick_access.items.link_url | string |  |  |  |
| landing_page.modules | object |  |  |  |
| landing_page.modules.visible | boolean |  |  |  |
| landing_page.modules.items | array<object> |  |  |  |
| landing_page.modules.items.id | string |  |  |  |
| landing_page.modules.items.visible | boolean |  |  |  |
| landing_page.modules.items.title | string |  |  |  |
| landing_page.modules.items.description | string |  |  |  |
| landing_page.auth_images | object |  |  |  |
| landing_page.auth_images.login | string |  |  |  |
| landing_page.auth_images.forgot_password | string |  |  |  |
| configurations | object | No | JSONB object for brand configurations |  |
| configurations.theme | string |  |  |  |
| is_active | boolean | No | Boolean value to enable/disable the brand settings |  |
| default_preset_id | string | No | String value for the default preset ID |  |
| assets |  | No | JSONB object for brand assets |  |
| colors |  | No | JSONB object for brand colors |  |



### Request body (raw)

```json
{
  "brand_name": {
    "en": "Ondi",
    "ar": "",
    "ku": ""
  },
  "landing_page": {
    "book_demo_link": {
      "visible": true
    },
    "hero": {
      "title": "Ondi",
      "subtitle": "Right on the distribution",
      "description": "Manage e-commerce, restaurant operations, delivery logistics, and more all from a single unified platform.",
      "image": ""
    },
    "documentation": {
      "visible": true,
      "platform": {
        "visible": true,
        "url": "https://docs.ondi.io"
      },
      "api": {
        "visible": true,
        "url": "https://api-docs.ondi.io"
      }
    },
    "quick_access": {
      "visible": true,
      "items": [
        {
          "id": "store-front",
          "visible": true,
          "title": "Store Front",
          "description": "Customer-facing e-commerce storefront",
          "image": "",
          "link_text": "store.ondi.io",
          "link_url": "https://store.ondi.io"
        }
      ]
    },
    "modules": {
      "visible": true,
      "items": [
        {
          "id": "store-front",
          "visible": true,
          "title": "Store front (E-Commerce)",
          "description": "Complete online store solution with product management, inventory, and checkout."
        }
      ]
    },
    "auth_images": {
      "login": "",
      "forgot_password": ""
    }
  },
  "configurations": {
    "theme": "dark"
  },
  "is_active": true,
  "default_preset_id": "preset-123"
}
```

### Request example 1

```json
{
  "brand_name": {
    "en": "Ondi",
    "ar": "",
    "ku": ""
  },
  "landing_page": {
    "book_demo_link": {
      "visible": true
    },
    "hero": {
      "title": "Ondi",
      "subtitle": "Right on the distribution",
      "description": "Manage e-commerce, restaurant operations, delivery logistics, and more all from a single unified platform.",
      "image": ""
    }
  },
  "configurations": {
    "theme": "dark"
  },
  "is_active": true,
  "default_preset_id": "preset-123"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "System brand settings updated successfully",
  "systemBrandSettings": {
    "id": "1",
    "brand_name": {
      "en": "Ondi",
      "ar": "",
      "ku": ""
    },
    "assets": {
      "logo": "new-logo-url",
      "favicon": "new-favicon-url"
    },
    "colors": {
      "primary": "#111111",
      "secondary": "#222222"
    },
    "configurations": {
      "theme": "dark"
    },
    "landing_page": {
      "book_demo_link": {
        "visible": true
      },
      "hero": {
        "title": "Ondi",
        "subtitle": "Right on the distribution",
        "description": "Manage e-commerce, restaurant operations, delivery logistics, and more all from a single unified platform.",
        "image": ""
      },
      "documentation": {
        "visible": true,
        "platform": {
          "visible": true,
          "url": "https://docs.ondi.io"
        },
        "api": {
          "visible": true,
          "url": "https://api-docs.ondi.io"
        }
      },
      "quick_access": {
        "visible": true,
        "items": [
          {
            "id": "store-front",
            "visible": true,
            "title": "Store Front",
            "description": "Customer-facing e-commerce storefront",
            "image": "",
            "link_text": "store.ondi.io",
            "link_url": "https://store.ondi.io"
          }
        ]
      },
      "modules": {
        "visible": true,
        "items": [
          {
            "id": "store-front",
            "visible": true,
            "title": "Store front (E-Commerce)",
            "description": "Complete online store solution with product management, inventory, and checkout."
          }
        ]
      },
      "auth_images": {
        "login": "",
        "forgot_password": ""
      }
    },
    "is_active": true,
    "default_preset_id": "preset-123",
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-06-15T12:30:45Z",
    "updated_by": "user-id-123"
  }
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "System brand settings updated successfully",
  "systemBrandSettings": {
    "id": "1",
    "brand_name": {
      "en": "Ondi",
      "ar": "",
      "ku": ""
    },
    "landing_page": {
      "book_demo_link": {
        "visible": true
      },
      "hero": {
        "title": "Ondi",
        "subtitle": "Right on the distribution",
        "description": "Manage e-commerce, restaurant operations, delivery logistics, and more all from a single unified platform.",
        "image": ""
      }
    },
    "is_active": true,
    "default_preset_id": "preset-123"
  }
}
```

### Response 400 Error (400 Bad Request)



```json
{
  "success": false,
  "message": "landing_page must be a JSONB object"
}
```

### Response 403 Error (403 Forbidden)



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 404 Error (404 Not Found)



```json
{
  "success": false,
  "message": "Invalid route"
}
```

## GET /system-modules — List System Modules

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-modules#get-list-system-modules)

Retrieve a list of system modules. Requires super admin access.

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/system-modules?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token for authentication (required) | Bearer {{access_token}} |
| X-API-Key | header string |  | Alternative authentication method using API key |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "System modules retrieved successfully",
  "modules": [
    {
      "id": "1",
      "name": "User Management",
      "description": "User management module for creating and managing users",
      "version": "1.0.0",
      "is_active": true,
      "is_enabled": true,
      "created_at": "2023-01-01T00:00:00Z",
      "updated_at": "2023-01-01T00:00:00Z"
    },
    {
      "id": "2",
      "name": "Role Management",
      "description": "Role management module for creating and managing roles",
      "version": "1.0.0",
      "is_active": true,
      "is_enabled": true,
      "created_at": "2023-01-01T00:00:00Z",
      "updated_at": "2023-01-01T00:00:00Z"
    }
  ]
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /system-modules/:moduleCode — Get System Module

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-modules#get-get-system-module)

Retrieve details of a specific system module by its module code. Includes full configuration including settings schema, default settings, features, dependencies, and UI config.

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/system-modules/:moduleCode?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token for authentication (required) | Bearer {{access_token}} |
| X-API-Key | header string |  | Alternative authentication method using API key |  |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| moduleCode | path string | Yes | Code of the module to retrieve (e.g., delivery, warehouse, storefront) Code of the system module to retrieve (e.g., 'storefront', 'delivery', 'warehouse') | storefront |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Module retrieved successfully",
  "module": {
    "id": "uuid",
    "module_code": "storefront",
    "name": "Store front",
    "description": "E-commerce storefront module",
    "version": "1.0.0",
    "dependencies": [
      "warehouse"
    ],
    "features": [
      "product_catalog",
      "shopping_cart"
    ],
    "is_active": true,
    "is_enabled": true,
    "settings_schema": {},
    "default_settings": {},
    "icon": "https://...",
    "ui_config": {},
    "name_multilingual": {
      "en": "Store front",
      "ar": "واجهة المتجر"
    },
    "description_multilingual": {
      "en": "E-commerce...",
      "ar": "..."
    }
  },
  "payment_methods": []
}
```

## PUT /system-modules/:moduleCode — Update System Module

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-modules#put-update-system-module)

Update a system module. Requires super admin access. Supports updating module properties including name, description, version, active status, and enabled status.

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: PUT https://staging-api.ondi.io/functions/v1/system-modules/:moduleCode?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token for authentication (required) | Bearer {{access_token}} |
| Content-Type | header string | Yes | application/json (required) | application/json |
| X-API-Key | header string |  | Alternative authentication method using API key |  |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| moduleCode | path string | Yes | Code of the module to update (e.g., delivery, warehouse, storefront) Code of the system module to update (e.g., 'delivery', 'warehouse', 'storefront') | storefront |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| description.ar | string |  |  |  |
| version | string |  |  |  |
| features | array<string> |  |  |  |
| dependencies | array<string> |  |  |  |
| default_settings | object |  |  |  |
| default_settings.setting1 | string |  |  |  |
| default_settings.setting2 | boolean |  |  |  |
| ui_config | object |  |  |  |
| ui_config.dashboard_widgets | array<string> |  |  |  |
| ui_config.menu_position | number |  |  |  |
| is_active | boolean |  |  |  |
| is_enabled | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Updated Module Name",
    "ar": "اسم الوحدة المحدث"
  },
  "description": {
    "en": "Updated description",
    "ar": "الوصف المحدث"
  },
  "version": "1.0.1",
  "features": [
    "new_feature",
    "existing_feature"
  ],
  "dependencies": [
    "other_module"
  ],
  "default_settings": {
    "setting1": "value1",
    "setting2": true
  },
  "ui_config": {
    "dashboard_widgets": [
      "widget1",
      "widget2"
    ],
    "menu_position": 1
  },
  "is_active": true,
  "is_enabled": true
}
```

### Request example 1

```json
{
  "name": "Updated Module Name",
  "description": "Updated description",
  "version": "1.0.1",
  "is_active": true,
  "is_enabled": true
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "System module updated successfully",
  "module": {
    "id": "1",
    "name": "Updated Module Name",
    "description": "Updated description",
    "version": "1.0.1",
    "is_active": true,
    "is_enabled": true,
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-06-15T12:30:45Z"
  }
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 404 Error - Module Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Module not found"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /system-settings/mobile-staging-access — Get Mobile Staging Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-settings#get-get-mobile-staging-access)

Returns only safe global mobile staging status to authenticated system administrators: enabled, passcode_configured, and passcode_updated_at. It never returns the PIN or its hash.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/system-settings/mobile-staging-access?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  |  | en |



## PUT /system-settings/mobile-staging-access — Update Mobile Staging Access

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-settings#put-update-mobile-staging-access)

Enables/disables the global Operation-app staging switch and optionally replaces its PIN. Requires system_admin. A new 8–12 digit PIN replaces the old PIN without asking for it; omit passcode to retain the existing hash. Enabling is rejected until a PIN exists. Raw PINs are redacted from audits.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/system-settings/mobile-staging-access?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| enabled | boolean |  |  |  |
| passcode | string |  |  |  |



### Request body (raw)

```json
{
  "enabled": true,
  "passcode": "12345678"
}
```

## GET /system-settings — Get System Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-settings#get-get-system-settings)

Retrieve system settings. Requires system admin access.

The `general` bucket includes language settings such as `default_system_language` and `tenant_available_languages`. `general.unit_size.settings.missing_unit_size_fallback` controls delivery orders that provide neither a unit size nor usable measurements. Its allowed values are `service_min`, `service_half`, `service_max`, and `error`; `missing_unit_size_fallback_options` returns every value with a localized `label` for the admin selector. The `warehouse.package` setting includes `auto_execute_delivery_order_on_receive`, which defaults to false and controls whether fully received warehouse items auto-activate related draft delivery orders.

### Response

Wallet Settings returns two entries under `system_settings.wallet`: `wallet_minimum_balance.settings.minimum_balance` for Drivers Minimum Balance and `customer_wallet_minimum_balance.settings.minimum_balance` for Customer Minimum Balance. These are the defaults used when a tenant has no override of the corresponding key. The new customer default starts at the previous shared value.

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: GET https://staging-api.ondi.io/functions/v1/system-settings?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token for authentication (required) | Bearer {{access_token}} |
| X-API-Key | header string |  | Alternative authentication method using API key |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for response localization (e.g., 'en') | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "System settings retrieved successfully",
  "system_settings": {
    "general": {
      "default_system_language": {
        "settings": {
          "default_language": "en"
        },
        "description": "Default language used as the system-wide localization fallback"
      },
      "tenant_available_languages": {
        "settings": {
          "available_languages": [
            "en",
            "ar",
            "ku",
            "fa"
          ],
          "language_names": {
            "en": "English",
            "ar": "Arabic",
            "ku": "Kurdish",
            "fa": "Farsi"
          }
        },
        "description": "Languages available for tenant and app language configuration"
      }
    },
    "warehouse": {
      "package": {
        "settings": {
          "handoff_required": false,
          "auto_execute_delivery_order_on_receive": false
        },
        "description": "Warehouse package handling settings. auto_execute_delivery_order_on_receive controls whether a related draft delivery order is automatically moved to pending after its warehouse item is fully received."
      }
    }
  }
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 403 Error - Forbidden (Not System Admin)

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized access"
}
```

### Response 500 Error - Database Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Database error"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "System settings retrieved successfully",
  "system_settings": {
    "general": {
      "default_system_language": {
        "settings": {
          "default_language": "en"
        },
        "description": "Default language used as the system-wide localization fallback"
      },
      "tenant_available_languages": {
        "settings": {
          "available_languages": [
            "en",
            "ar",
            "ku",
            "fa"
          ]
        },
        "description": "Languages available for tenant and app language configuration"
      }
    },
    "warehouse": {
      "package": {
        "settings": {
          "handoff_required": false,
          "auto_execute_delivery_order_on_receive": false
        },
        "description": "Warehouse package handling settings. auto_execute_delivery_order_on_receive controls whether a related draft delivery order is automatically moved to pending after its warehouse item is fully received."
      }
    }
  }
}
```

## PUT /system-settings — Update System Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-settings#put-update-system-settings)

Update system settings. Requires system admin access.

If `setting_key = unit_size`, send `settings.missing_unit_size_fallback` as one of `service_min`, `service_half`, `service_max`, or `error`. Existing unit calculation fields are preserved, and the response returns canonical `missing_unit_size_fallback_options` with localized labels.

If `setting_key = package`, `settings` may include:

Both fields must be booleans when provided. `auto_execute_delivery_order_on_receive=false` keeps warehouse receiving behavior unchanged; true lets backend receive logic move related draft delivery orders to pending after their warehouse item is fully received.

If `setting_key = default_system_language`, `settings` must be:

If `setting_key = tenant_available_languages`, `settings` must be:

If `setting_key = phone_login`, `settings` must be:

If `setting_key = storefront_custom_domain_vercel`, `settings` must be:

### Field purpose
- `api_token`: authenticates Edge Function calls to Vercel for add, verify, inspect, and delete domain operations.
- `project_id`: tells the backend which Vercel project owns the storefront custom domains.
- `team_id`: needed when the Vercel project belongs to a team/workspace instead of a personal account.
- `deploy_hook_url`: optional fallback used by the admin redeploy action when you want deploy-hook based rebuilds.
### Rules
- `api_token` is required.
- `project_id` is required.
- `team_id` is optional.
- `deploy_hook_url` is optional but must be a valid URL when present.

### Response

### Wallet minimum settings

send `{ "setting_key": "customer_wallet_minimum_balance", "settings": { "minimum_balance": -1000000 } }` for customers or use `setting_key=wallet_minimum_balance` for drivers. Each accepts only `minimum_balance` as a required finite JSON number. Strings, missing values, arrays, or unknown fields return HTTP 400 with localized validation errors. Negative values allow debt down to the limit; zero allows no debt. The response returns the saved `system_settings.settings.minimum_balance`. Frontend integration: show both fields in Wallet Settings and save each key independently. Customer balance checks use only the customer key; driver operations use the existing key.

Authentication: Use the authentication method supported by this endpoint: a bearer session or an approved tenant API key in X-API-Key.

Request: PUT https://staging-api.ondi.io/functions/v1/system-settings?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | Bearer token for authentication (required) | Bearer {{access_token}} |
| Content-Type | header string | Yes | application/json (required) | application/json |
| X-API-Key | header string |  | Alternative authentication method using API key |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| setting_key | string | Yes | (required) |  |
| settings | object | Yes | (required) |  |
| settings.actions | array<object> |  |  |  |
| settings.actions.key | string |  |  |  |
| settings.actions.label | string |  |  |  |
| settings.types | array<object> |  |  |  |
| settings.types.code | string |  |  |  |
| settings.types.label | string |  |  |  |
| settings.types.allowed_statuses | array<string> |  |  |  |
| settings.types.action_keys | array<string> |  |  |  |



### Request body (raw)

```json
{
  "setting_key": "exception_types",
  "settings": {
    "actions": [
      {
        "key": "retry",
        "label": "Retry delivery"
      },
      {
        "key": "return_to_sender",
        "label": "Return to sender"
      },
      {
        "key": "cancel",
        "label": "Cancel order"
      }
    ],
    "types": [
      {
        "code": "customer_refused",
        "label": "Customer refused",
        "allowed_statuses": [
          "pending",
          "assigned",
          "en_route_to_pickup",
          "in_transit"
        ],
        "action_keys": [
          "retry",
          "return_to_sender",
          "cancel"
        ]
      }
    ]
  }
}
```

### If setting_key = package, settings may include

```json
{
  "handoff_required": false,
  "auto_execute_delivery_order_on_receive": false
}
```

### If setting_key = default_system_language, settings must be

```json
{
  "default_language": "en"
}
```

### If setting_key = tenant_available_languages, settings must be

```json
{
  "available_languages": [
    "en",
    "ar",
    "ku",
    "fa"
  ]
}
```

### If setting_key = phone_login, settings must be

```json
{
  "allowed_countries": [
    {
      "code": "IN",
      "name": "India",
      "dial_code": "+91"
    }
  ],
  "default_country": {
    "code": "IN",
    "name": "India",
    "dial_code": "+91"
  }
}
```

### If setting_key = storefront_custom_domain_vercel, settings must be

```json
{
  "api_token": "vercel_api_token_here",
  "project_id": "prj_123456789",
  "team_id": "team_123456789",
  "deploy_hook_url": "https://api.vercel.com/v1/integrations/deploy/prj_123456789/trigger"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "System settings updated successfully",
  "systemSettings": {
    "id": "uuid",
    "setting_key": "exception_types",
    "settings": {
      "actions": [
        {
          "key": "retry",
          "label": "Retry delivery"
        },
        {
          "key": "return_to_sender",
          "label": "Return to sender"
        },
        {
          "key": "cancel",
          "label": "Cancel order"
        }
      ],
      "types": [
        {
          "code": "customer_refused",
          "label": "Customer refused",
          "allowed_statuses": [
            "pending",
            "assigned",
            "en_route_to_pickup",
            "in_transit"
          ],
          "action_keys": [
            "retry",
            "return_to_sender",
            "cancel"
          ]
        }
      ]
    },
    "is_active": true,
    "created_at": "timestamp",
    "updated_at": "timestamp",
    "updated_by": "uuid"
  }
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 403 Error - Forbidden (Not System Admin)

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized access"
}
```

### Response 500 Error - Database Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Database error"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "System settings updated successfully",
  "system_settings": {
    "id": "uuid",
    "setting_key": "default_system_language",
    "settings": {
      "default_language": "en"
    },
    "is_active": true
  }
}
```

## PUT /system-settings — Update Transfer Manifest Defaults

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/system-configuration/system-settings#put-update-transfer-manifest-defaults)

System-admin update for tenant-scoped transfer-manifest defaults. `settings.branch_only_destinations=false` preserves branch-or-address manifest order destinations. When true, all three manifest destination/order-creation endpoints require `destination_branch_id`; address-only requests return HTTP 400 `manifest_destination_branch_required`. `settings.allow_warehouse_staging_without_assigned_driver` is boolean and defaults to false; when enabled, unassigned Transfer Manifest P&D staging may use the tenant assignment fallback driver. `available_delivery_service_ids` is an array of service UUIDs and defaults to `[]`, meaning no G2G limitation. When non-empty, `default_delivery_service_id` must be included. The partial object is merged with existing defaults.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/system-settings

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| setting_key | string |  |  |  |
| settings | object |  |  |  |
| settings.branch_only_destinations | boolean |  |  |  |
| settings.allow_warehouse_staging_without_assigned_driver | boolean |  |  |  |
| settings.default_delivery_service_id | null |  |  |  |
| settings.available_delivery_service_ids | array |  |  |  |



### Request body (raw)

```json
{
  "setting_key": "transfer_manifest",
  "settings": {
    "branch_only_destinations": false,
    "allow_warehouse_staging_without_assigned_driver": false,
    "default_delivery_service_id": null,
    "available_delivery_service_ids": []
  }
}
```

### Response 200 Updated



```json
{
  "success": true,
  "message": "system_settings_updated_successfully",
  "system_settings": {
    "setting_key": "transfer_manifest",
    "scope": "tenant",
    "settings": {
      "branch_only_destinations": false
    }
  }
}
```

### Response 400 Invalid branch-only destination value



```json
{
  "success": false,
  "message": "settings.branch_only_destinations: Must be true or false",
  "details": [
    {
      "field": "settings.branch_only_destinations",
      "message": "Must be true or false"
    }
  ]
}
```

## POST /public/mobile-staging-access/verify — Verify Mobile Staging Access (Production Only)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#post-verify-mobile-staging-access-production-only)

Verifies the global 8–12 digit Operation-app staging PIN. Mobile clients must call this route against the production backend even when their current backend is staging. The public staging switch must be enabled. Raw PINs are redacted from audits. Responses: 200 authorized; 400 malformed PIN; 401 incorrect PIN; 403 disabled; 500 generic failure.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/public/mobile-staging-access/verify?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| X-Ondi-Client | header string |  |  | eaglepost |
| X-Ondi-Client-Version | header string |  |  | 1.0.0 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| passcode | string |  |  |  |



### Request body (raw)

```json
{
  "passcode": "12345678"
}
```

## GET /public/system-settings — Get Public System Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-public-system-settings)

Retrieves public system settings. When `key` is provided, returns a single setting by `setting_key`. Otherwise returns all active public settings, optionally filtered by `type`.

Public `general` settings include `default_system_language`, `tenant_available_languages`, and `default_location`. `default_location` contains numeric `latitude` and `longitude` values and reflects the effective tenant override when `tenant_id` resolves from request context or query. The legacy `default_location_cordinates` field remains available for backward compatibility.

### Frontend integration contract
- Read `settings.general.default_location.latitude` and `settings.general.default_location.longitude` for the initial map location.
- `GET /public/system-settings?key=default_location` returns the same object directly in `settings`.

### Responses
- 200 OK (list)
- 200 OK (single)
- 404 Not Found (when key specified and not found)

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/system-settings?language=en&key=&type=

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized messages | en |
| key | query string | No | Optional setting_key to fetch a single public setting `setting_key` to fetch a specific setting |  |
| type | query string | No | Optional setting_type to filter the list (e.g., 'mobile', 'external_links') `setting_type` to filter the list (e.g., 'mobile', 'external_links', 'general') |  |



### Response 200 Success - All Public Settings

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "system_settings_retrieved_successfully",
  "settings": {
    "general": {
      "default_location": {
        "latitude": 36.20528,
        "longitude": 44.04711
      },
      "default_system_language": {
        "default_language": "en"
      },
      "tenant_available_languages": {
        "available_languages": [
          "en",
          "ar",
          "ku",
          "fa"
        ],
        "language_names": {
          "en": "English",
          "ar": "Arabic",
          "ku": "Kurdish",
          "fa": "Farsi"
        }
      }
    }
  }
}
```

### Response 200 Success - Default Location

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "system_setting_retrieved_successfully",
  "settings": {
    "latitude": 36.20528,
    "longitude": 44.04711
  }
}
```

### Response 200 Success - Public Links

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "system_setting_retrieved_successfully",
  "settings": {
    "website_url": "https://ondi.io",
    "documentation_url": "https://docs.ondi.io",
    "api_docs_url": "https://api.ondi.io",
    "pos_portal_url": "https://pos.ondi.io",
    "restaurant_portal_url": "https://food.ondi.io",
    "express_portal_url": "https://express.ondi.io",
    "delivery_customer_portal_url": "https://track.ondi.io"
  }
}
```

### Response 200 Success - Single Setting By Key

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "system_setting_retrieved_successfully",
  "settings": {
    "default_language": "en"
  }
}
```

### Response 404 Error - Setting Not Found (by key)

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "system_setting_not_found"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "internal_server_error"
}
```

## GET /public/config/versions — Get Config Versions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-config-versions)

Check which cached resources changed before downloading configuration again. Compare each resource’s version number with the version saved by your app, then refetch only resources whose versions changed. The cache_enabled flag reports whether caching is enabled for the resolved scope.

The response separates global, tenant and user version maps. Global-only requests work without authentication; user versions require the matching bearer session. Save the ETag with the maps: a validator matching the server snapshot returns 304 with no body, while 200 returns the current versions.

Authentication: Global and tenant version maps are public. User-scoped versions require the matching bearer session.

Request: GET https://staging-api.ondi.io/functions/v1/public/config/versions?tenant_id={{tenant_id}}&user_id={{user_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | No | Bearer session for user-scoped versions. Omit for public global or tenant version maps. | Bearer {{access_token}} |
| If-None-Match | header string | No | Previously saved ETag. A matching server validator returns 304. The staging edge currently emits a weak tag that can return 200 when resent unchanged; compare the returned version maps. Responses use Cache-Control: no-store. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Optional tenant UUID. Omit for global versions, or let the bearer session supply its tenant. A conflicting tenant scope returns 403. | {{tenant_id}} |
| user_id | query string | No | Optional user UUID. Requires a matching bearer session; the signed-in user is inferred when omitted. A conflicting user scope returns 403. | {{user_id}} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | no-store |
| ETag | header string |  |  | "rv:exampleetag" |



```json
{
  "success": true,
  "global": {
    "system.settings": 42,
    "i18n.config": 7
  },
  "tenant": {
    "delivery.zones": 11,
    "warehouse.categories": 5
  },
  "user": {
    "addresses.list": 3
  },
  "server_time": "2026-05-24T10:00:00.000Z"
}
```

### Response 304 Not Modified (ETag Match)

Not Modified

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Cache-Control | header string |  |  | no-store |
| ETag | header string |  |  | "rv:exampleetag" |



### Response 400 Error - Invalid tenant_id

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "tenant_id_required"
}
```

## GET /public/storefront/{{slug}}/brands — List Storefront Brands

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-list-storefront-brands)

List active brands for a public storefront with pagination and optional search.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/{{slug}}/brands?page=1&limit=20&search=&language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | URL-friendly identifier of the storefront |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number (optional, default: 1) | 1 |
| limit | query string | No | Items per page (optional, default: 20) | 20 |
| search | query string | No | Search by brand name/description (optional) |  |
| language | query string | No | Language code (optional, default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store-slug",
      "store_name": {
        "en": "My Store"
      },
      "status": "approved"
    },
    "brands": [
      {
        "id": "brand-uuid",
        "name": {
          "en": "Apple",
          "ar": "آبل"
        },
        "slug": "apple",
        "logo_url": "https://example.com/apple.png",
        "is_active": true
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

## GET /public/storefront/{{slug}}/catalog/{{list_type}} — Browse Storefront Product Feed

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-browse-storefront-product-feed)

Browse pre-ranked product feeds for a public storefront. Uses SQL procedures for minimal runtime cost and consistent response shape.

### Response
- Same shape as catalog browse: { success, data: { market, products, pagination } }

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/{{slug}}/catalog/{{list_type}}?page=1&limit=12&category_id=&window_days=&language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | URL-friendly identifier of the storefront (e.g., 'my-store-slug') Storefront slug |  |
| list_type | path string | Yes | Feed type: trending \| new-arrivals \| top-selling \| recommended One of `trending`, `new-arrivals`, `top-selling`, `recommended` | trending |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (Optional, default: 1) Page number (default: 1) | 1 |
| limit | query string | No | Number of products per page (Optional, default: 12) Items per page (default: 12) | 12 |
| category_id | query string | No | Filter by category ID (Optional) |  |
| window_days | query string | No | Window in days for ranking (Optional; used by trending/top-selling). Defaults: trending=14, top-selling=90 Ranking window (trending default 14, top-selling default 90) |  |
| language | query string | No | Language code for response localization (Optional, default: en) Localization language code | en |



### Response 200 Success Response (trending)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store-slug",
      "store_name": {
        "en": "My Store"
      },
      "status": "approved",
      "banners": []
    },
    "products": [
      {
        "id": "product-uuid",
        "name": {
          "en": "Product Name"
        },
        "description": {
          "en": "Product description"
        },
        "stock_quantity": 10,
        "rating": 4.6,
        "total_reviews": 23
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 12,
      "total": 12,
      "totalPages": 1
    }
  }
}
```

### Response 404 Error - Store Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Store not found"
}
```

## GET /public/verify-domain/:subdomain — Verify Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-verify-domain)

Verifies if a domain/subdomain is associated with a tenant in the system.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/verify-domain/:subdomain?language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| subdomain | path string | Yes | Subdomain to verify (e.g., 'company' for company.example.com) The subdomain to verify (e.g., 'company' for company.example.com) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "tenantId": "tenant-uuid",
  "message": "Domain verified successfully"
}
```

### Response 404 Error - Domain Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Domain not found"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /public/system-brand-settings — Get System Brand Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-system-brand-settings)

Retrieves the active system brand settings including brand name, assets, colors, and configurations.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/system-brand-settings?language=en

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "System brand settings retrieved successfully",
  "brandSettings": {
    "brand_name": "System Brand Name",
    "assets": {
      "logo": "https://example.com/logo.png",
      "favicon": "https://example.com/favicon.ico"
    },
    "colors": {
      "primary": "#3f51b5",
      "secondary": "#f50057",
      "accent": "#ff4081"
    },
    "configurations": {
      "theme": "light",
      "defaultLanguage": "en"
    }
  }
}
```

### Response 500 Error Response

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /public/tenants/:tenantId/brand-settings — Get Tenant Brand Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-tenant-brand-settings)

Retrieves tenant brand settings. Falls back to system defaults if no tenant branding exists. Checks if the tenant exists and is active before returning brand settings.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/tenants/:tenantId/brand-settings?language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant to retrieve brand settings for |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response - Tenant Brand Settings

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Tenant brand settings retrieved successfully",
  "brandSettings": {
    "brand_name": "Tenant Brand Name",
    "assets": {
      "logo": "https://example.com/tenant-logo.png",
      "favicon": "https://example.com/tenant-favicon.ico"
    },
    "colors": {
      "primary": "#3f51b5",
      "secondary": "#f50057",
      "accent": "#ff4081"
    },
    "configurations": {
      "theme": "dark",
      "defaultLanguage": "en"
    },
    "metadata": {
      "lastUpdated": "2023-01-01T00:00:00Z"
    }
  }
}
```

### Response 200 Success Response - System Default Settings

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "System brand settings retrieved successfully",
  "brandSettings": {
    "brand_name": "System Brand Name",
    "assets": {
      "logo": "https://example.com/logo.png",
      "favicon": "https://example.com/favicon.ico"
    },
    "colors": {
      "primary": "#3f51b5",
      "secondary": "#f50057",
      "accent": "#ff4081"
    },
    "configurations": {
      "theme": "light",
      "defaultLanguage": "en"
    }
  }
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 404 Error - Tenant Not Active

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not active"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /public/deliveries/:deliveryId/tracking — Get Delivery Tracking

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-delivery-tracking)

Public endpoint for real-time delivery tracking. Returns current status, location, and estimated delivery information.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/deliveries/:deliveryId/tracking?tracking_token=&language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| deliveryId | path string | Yes | UUID of the delivery to track |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tracking_token | query string | No | Optional security token for accessing protected delivery tracking Security token for accessing protected delivery tracking |  |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery tracking retrieved successfully",
  "data": {
    "status": "in_transit",
    "currentLocation": {
      "type": "Point",
      "coordinates": [
        -73.935242,
        40.73061
      ]
    },
    "lastLocationAt": "2024-01-01T12:00:00Z",
    "eta": "2024-01-01T15:30:00Z",
    "estimatedTravelTime": 90,
    "estimatedDistanceKm": 25.5,
    "statusTimeline": [
      {
        "status": "picked_up",
        "timestamp": "2024-01-01T12:00:00Z",
        "location": {
          "latitude": 40.73061,
          "longitude": -73.935242
        },
        "notes": "Picked up from warehouse"
      }
    ],
    "type": "Delivery",
    "routePath": {
      "distance": 25.5,
      "duration": 90,
      "polyline": "encoded_polyline_string"
    },
    "destination": {
      "address": "123 Delivery St, New York, NY 10001",
      "latitude": 40.75061,
      "longitude": -73.945242
    }
  }
}
```

### Response 400 Error - Missing Delivery ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery ID is required"
}
```

### Response 401 Error - Invalid Tracking Token

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid tracking token"
}
```

### Response 404 Error - Delivery Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery not found"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Delivery tracking retrieved successfully",
  "data": {
    "status": "in_transit",
    "currentLocation": {
      "type": "Point",
      "coordinates": [longitude, latitude]
    },
    "lastLocationAt": "2024-01-01T12:00:00Z",
    "eta": "2024-01-01T15:30:00Z",
    "estimatedTravelTime": 90,
    "estimatedDistanceKm": 25.5,
    "statusTimeline": [
      {
        "status": "picked_up",
        "timestamp": "2024-01-01T12:00:00Z",
        "location": {
          "latitude": 0,
          "longitude": 0
        },
        "notes": "Picked up from warehouse"
      }
    ],
    "type": "Delivery",
    "routePath": {
      "distance": 25.5,
      "duration": 90,
      "polyline": "encoded_polyline_string"
    },
    "destination": {
      "address": "123 Delivery St",
      "latitude": 0,
      "longitude": 0
    }
  }
}
```

## GET /public/color-palettes — Get Color Palettes

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-color-palettes)

Retrieves all available color palettes from the system, ordered by creation date (newest first).

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/color-palettes?language=en

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Color palettes retrieved successfully",
  "colorPalettes": [
    {
      "id": "uuid-123",
      "created_at": "2024-01-01T00:00:00Z",
      "name": "Default Theme",
      "palette": {
        "primary": {
          "dark": "#007867",
          "main": "#00A76F",
          "light": "#5BE49B",
          "darker": "#004B50",
          "lighter": "#C8FAD6",
          "contrastText": "#FFFFFF"
        },
        "secondary": {
          "dark": "#1939B7",
          "main": "#3366FF",
          "light": "#84A9FF",
          "darker": "#091A7A",
          "lighter": "#D6E4FF",
          "contrastText": "#FFFFFF"
        }
      }
    },
    {
      "id": "uuid-456",
      "created_at": "2024-01-02T00:00:00Z",
      "name": "Ocean Theme",
      "palette": {
        "primary": {
          "dark": "#004080",
          "main": "#0066CC",
          "light": "#4D94FF",
          "darker": "#002654",
          "lighter": "#B3D6FF",
          "contrastText": "#FFFFFF"
        },
        "secondary": {
          "dark": "#2E7A7A",
          "main": "#4DB8B8",
          "light": "#7DD6D6",
          "darker": "#1A4949",
          "lighter": "#C7EEEE",
          "contrastText": "#FFFFFF"
        }
      }
    }
  ]
}
```

### Response 500 Error Response

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Color palettes retrieved successfully",
  "colorPalettes": [
    {
      "id": "uuid-123",
      "created_at": "2024-01-01T00:00:00Z",
      "name": "Default Theme",
      "palette": {
        "primary": {
          "dark": "#007867",
          "main": "#00A76F",
          "light": "#5BE49B",
          "darker": "#004B50",
          "lighter": "#C8FAD6",
          "contrastText": "#FFFFFF"
        },
        "secondary": {
          "dark": "#1939B7",
          "main": "#3366FF",
          "light": "#84A9FF",
          "darker": "#091A7A",
          "lighter": "#D6E4FF",
          "contrastText": "#FFFFFF"
        }
      }
    }
  ]
}
```

## GET /public/storefront/{{slug}}/catalog — Browse Storefront Products

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-browse-storefront-products)

Browse products in a public storefront. Returns active products with only primary variants for listing view. Supports recursive category filtering, advanced search, and featured product types.

### Featured Types
- `most_loved`: Products ordered by Most Loved score (wishlist_count × favorite_weight + qualified_review_count × review_weight). Only includes products that have stats calculated.
- Future types: `trending`, `new_arrivals`, `top_selling` (extensible design)

### Search Behavior
- When no search is provided: Returns only primary variants
- When search matches product name/description: Returns primary variant only
- When search matches variant name/description: Returns matching variant (may include multiple instances of same product with different variants)

### Category Filtering
- When category_id is provided: Returns products from the specified category and all its child categories (recursive)
- Uses database RPC for optimal performance

### Example Requests
- Get most loved products: `GET /public/storefront/my-store/catalog?featured_type=most_loved`
- Get most loved in category: `GET /public/storefront/my-store/catalog?featured_type=most_loved&category_id=category-uuid`
- Regular browse: `GET /public/storefront/my-store/catalog` (default ordering)

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/{{slug}}/catalog?page=1&limit=12&category_id=&brand_id=&search=&featured_type=&language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | URL-friendly identifier of the storefront (e.g., 'my-store-slug') |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number for pagination (Optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of products per page (Optional, default: 12) Products per page (default: 12) | 12 |
| category_id | query string | No | Filter by category ID and all its child categories (Optional) Filter by category ID and all its child categories recursively |  |
| brand_id | query string | No | Filter by brand ID (Optional) |  |
| search | query string | No | Search products by name, description, or variant details (Optional) |  |
| featured_type | query string | No | Featured product type filter (Optional). Use 'most_loved' to get products ordered by Most Loved score (wishlist + qualified reviews). When not provided, uses default ordering by created_at DESC. Featured product type. Use `most_loved` to get products ordered by Most Loved score (calculated from wishlist additions and qualified reviews 4-5 stars). When not provided, uses default ordering by created_at DESC. |  |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store-slug",
      "store_name": {"en": "My Store"},
      "status": "approved",
      "banners": []
    },
    "products": [
      {
        "id": "product-uuid",
        "name": {"en": "Product Name"},
        "description": {"en": "Product description"},
        "stock_quantity": 10,
        "category": {
          "id": "category-uuid",
          "name": {"en": "Electronics"}
        },
        "variant": {
          "id": "variant-uuid",
          "name": {"en": "Standard Edition"},
          "description": {"en": "Standard variant description"},
          "price": 29.99,
          "sku": "PROD-001",
          "images": ["image1.jpg", "image2.jpg"],
          "stock_quantity": 10
        },
        "rating": 4.6,
        "total_reviews": 23
         }
       }
     ],
     "pagination": {
       "page": 1,
       "limit": 12,
       "total": 50,
       "totalPages": 5
     }
   }
 }
```

### Response 404 Error - Store Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Store not found"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store-slug",
      "store_name": {"en": "My Store"},
      "status": "approved",
      "banners": []
    },
    "products": [
      {
        "id": "product-uuid",
        "name": {"en": "Product Name"},
        "description": {"en": "Product description"},
        "stock_quantity": 10,
        "category": {
          "id": "category-uuid",
          "name": {"en": "Category Name"}
        },
        "variant": {
          "id": "variant-uuid",
          "name": {"en": "Variant Name"},
          "description": {"en": "Variant description"},
          "price": 29.99,
          "sku": "PROD-001",
          "images": ["image1.jpg"],
          "stock_quantity": 10
        },
        "rating": 4.6,
        "total_reviews": 23
         }
       }
     ],
     "pagination": {
       "page": 1,
       "limit": 12,
       "total": 50,
       "totalPages": 5
     }
   }
 }
```

## GET /public/storefront/{{slug}}/products/{{product_id}} — Get Storefront Product

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-storefront-product)

Get detailed information about a specific product including all variants.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/{{slug}}/products/{{product_id}}?language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | URL-friendly identifier of the storefront |  |
| product_id | path string | Yes | UUID of the product to retrieve |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store-slug",
      "store_name": {
        "en": "My Store"
      }
    },
    "product": {
      "id": "product-uuid",
      "name": {
        "en": "Product Name"
      },
      "description": {
        "en": "Product description"
      },
      "rating": 4.6,
      "total_reviews": 23,
      "variants": [
        {
          "id": "variant-uuid",
          "price": 29.99,
          "stock_quantity": 10,
          "is_primary": true,
          "variant_options": {
            "size": "M",
            "color": "blue"
          }
        }
      ]
    }
  }
}
```

## GET /public/storefront/{{slug}}/products/{{product_id}}/similar — Get Similar Storefront Products

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-similar-storefront-products)

Returns random similar products from the same categories as the given product.

- Input: `product_id`
- Logic: find products sharing at least one category with the reference product, exclude the reference product, only active products with at least one active in-stock variant.
- Output: same product listing shape (one best variant per product).

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/{{slug}}/products/{{product_id}}/similar?limit=10&language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | URL-friendly identifier of the storefront (e.g., 'my-store-slug') |  |
| product_id | path string | Yes | Reference product UUID |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| limit | query string | No | Number of similar products to return (Optional, default: 10, max: 50) | 10 |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store-slug",
      "store_name": {
        "en": "My Store"
      },
      "status": "approved"
    },
    "products": [
      {
        "id": "product-uuid-2",
        "name": {
          "en": "Similar Product"
        },
        "description": {
          "en": "Similar product description"
        },
        "stock_quantity": 5,
        "variant": {
          "id": "variant-uuid-2",
          "name": {
            "en": "Standard"
          },
          "description": {
            "en": "Variant description"
          },
          "price": 19.99,
          "sku": "PROD-002",
          "images": [
            "image.jpg"
          ],
          "stock_quantity": 5
        },
        "rating": 4.2,
        "total_reviews": 11
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

## GET /public/storefront/{{slug}}/categories — Get Storefront Categories

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-storefront-categories)

Get all categories available in the storefront.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/{{slug}}/categories?language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | URL-friendly identifier of the storefront |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store-slug",
      "store_name": {
        "en": "My Store"
      }
    },
    "categories": [
      {
        "id": "category-uuid",
        "name": {
          "en": "Electronics"
        },
        "description": {
          "en": "Electronic devices and accessories"
        },
        "parent_id": null,
        "sort_order": 1
      }
    ]
  }
}
```

## GET /public/storefront/{{slug}}/info — Get Storefront Information

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-storefront-information)

Get basic information about the storefront including branding and theme settings.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/{{slug}}/info?language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | URL-friendly identifier of the storefront |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "slug": "my-store-slug",
    "is_valid": true,
    "exists": true,
    "market": {
      "id": "market-uuid",
      "slug": "my-store-slug",
      "store_name": {
        "en": "My Store"
      },
      "status": "approved",
      "currency": {
        "currency_code": "IQD",
        "currency_mode": "code",
        "currency_name": "Iraqi Dinar",
        "currency_symbol": "د.ع.‏",
        "system_currency": {
          "currency_code": "USD",
          "currency_mode": "symbol",
          "currency_name": "US Dollar",
          "currency_symbol": "$"
        },
        "exchange_rate_to_system": 2
      },
      "logo_url": "https://example.com/logo.png",
      "theme_settings": {
        "primary_color": "#007AFF",
        "secondary_color": "#FFFFFF",
        "accent_color": "#FF9500",
        "theme_style": "modern"
      }
    }
  }
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "data": {
    "id": "market-uuid",
    "slug": "my-store-slug",
    "store_name": {
      "en": "My Store"
    },
    "tagline": {
      "en": "Quality products for everyone"
    },
    "logo_url": "https://example.com/logo.png",
    "theme_settings": {
      "primary_color": "#007AFF",
      "secondary_color": "#FFFFFF",
      "accent_color": "#FF9500",
      "theme_style": "modern"
    },
    "banners": [],
    "status": "approved",
    "created_at": "2024-01-01T00:00:00Z"
  }
}
```

## GET /public/storefront/resolve-domain — Resolve Storefront By Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-resolve-storefront-by-domain)

Resolves an active custom domain to the storefront validation payload, including the owning `tenant_id`, effective tenant `currency`, explicit `default_currency`, the effective tenant `default_country`, and the effective tenant `allowed_countries` list derived from phone login settings.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/resolve-domain?domain=shop.example.com&language=en

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| domain | query string |  | Custom domain to resolve | shop.example.com |
| language | query string | No | Language code for localized response localization (Optional, default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "slug": "my-store-slug",
    "is_valid": true,
    "exists": true,
    "market": {
      "id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "slug": "my-store-slug",
      "store_name": {
        "en": "My Store"
      },
      "status": "approved",
      "currency": {
        "currency_code": "IQD",
        "currency_mode": "code",
        "currency_name": "Iraqi Dinar",
        "currency_symbol": "د.ع.‏",
        "system_currency": {
          "currency_code": "USD",
          "currency_mode": "symbol",
          "currency_name": "US Dollar",
          "currency_symbol": "$"
        },
        "exchange_rate_to_system": 2
      },
      "default_currency": {
        "currency_code": "IQD",
        "currency_mode": "code",
        "currency_name": "Iraqi Dinar",
        "currency_symbol": "د.ع.‏",
        "system_currency": {
          "currency_code": "USD",
          "currency_mode": "symbol",
          "currency_name": "US Dollar",
          "currency_symbol": "$"
        },
        "exchange_rate_to_system": 2
      },
      "default_country": {
        "code": "CA",
        "name": "Canada",
        "dial_code": "+1"
      },
      "allowed_countries": [
        {
          "code": "CA",
          "name": "Canada",
          "dial_code": "+1"
        },
        {
          "code": "IN",
          "name": "India",
          "dial_code": "+91"
        }
      ],
      "logo_url": "https://example.com/logo.png",
      "theme_settings": {
        "primary_color": "#007AFF",
        "secondary_color": "#FFFFFF",
        "accent_color": "#FF9500",
        "theme_style": "modern"
      }
    }
  }
}
```

### Response 404 Error - Store Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Store not found"
}
```

## GET /public/storefront/{{slug}}/validate — Validate Storefront Slug

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-validate-storefront-slug)

Validates if a slug exists and points to a published and active storefront. On valid storefronts, the response includes the owning `tenant_id`, effective tenant `currency`, explicit `default_currency`, the effective tenant `default_country`, and the effective tenant `allowed_countries` list derived from phone login settings.

**Use Cases:
- Frontend route validation
- Subdomain verification
- URL availability checking
- SEO-friendly URL validation
- Custom domain setup

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/{{slug}}/validate?language=en

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | URL-friendly identifier to validate (e.g., 'my-store-slug') |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response | en |



### Response 200 Success - Valid Slug

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "slug": "my-store-slug",
    "is_valid": true,
    "exists": true,
    "market": {
      "id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "slug": "my-store-slug",
      "store_name": {
        "en": "My Store"
      },
      "status": "approved",
      "currency": {
        "currency_code": "IQD",
        "currency_mode": "code",
        "currency_name": "Iraqi Dinar",
        "currency_symbol": "د.ع.‏",
        "system_currency": {
          "currency_code": "USD",
          "currency_mode": "symbol",
          "currency_name": "US Dollar",
          "currency_symbol": "$"
        },
        "exchange_rate_to_system": 2
      },
      "default_currency": {
        "currency_code": "IQD",
        "currency_mode": "code",
        "currency_name": "Iraqi Dinar",
        "currency_symbol": "د.ع.‏",
        "system_currency": {
          "currency_code": "USD",
          "currency_mode": "symbol",
          "currency_name": "US Dollar",
          "currency_symbol": "$"
        },
        "exchange_rate_to_system": 2
      },
      "default_country": {
        "code": "CA",
        "name": "Canada",
        "dial_code": "+1"
      },
      "allowed_countries": [
        {
          "code": "CA",
          "name": "Canada",
          "dial_code": "+1"
        },
        {
          "code": "IN",
          "name": "India",
          "dial_code": "+91"
        }
      ]
    }
  }
}
```

### Response 200 Success - Invalid Slug

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "slug": "non-existent-slug",
    "is_valid": false,
    "exists": false,
    "market": null
  }
}
```

### Response 200 Success (200 OK) - Valid Slug



```json
{
  "success": true,
  "data": {
    "slug": "my-store-slug",
    "is_valid": true,
    "exists": true,
    "market": {
      "id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "slug": "my-store-slug",
      "store_name": {
        "en": "My Store"
      },
      "status": "approved",
      "default_currency": {
        "currency_code": "IQD"
      },
      "default_country": {
        "code": "CA",
        "name": "Canada",
        "dial_code": "+1"
      },
      "allowed_countries": [
        {
          "code": "CA",
          "name": "Canada",
          "dial_code": "+1"
        }
      ]
    }
  }
}
```

## GET /public/storefront/{{slug}}/ads/{{placement}} — Get Public Advertisements

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-public-advertisements)

Retrieves active advertisements for a specific placement on a public storefront. No authentication required.

- **None required** - Public endpoint

### Response Data
- Only active advertisements within date range
- Sorted by priority (highest first)
- Multi-language content
- Ready for frontend display

### Features
- Public access for frontend integration
- Automatic filtering of active/scheduled ads
- Priority-based ordering
- Placement-specific results
- Performance optimized

### Use Cases
- Website banner integration
- Mobile app advertisement display
- Category page promotions
- Product page cross-sells
- Checkout upsells

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/{{slug}}/ads/{{placement}}?limit=5

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | URL-friendly identifier of the store/market |  |
| placement | path string | Yes | Advertisement placement (home, category, product, checkout, global) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| limit | query string | No | Maximum number of advertisements to return (default: 5) Maximum ads to return (default: 5) | 5 |



### Response 200 Success - Public Advertisements

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "advertisements": [
    {
      "id": "ad-uuid",
      "type": "banner",
      "placement": "home",
      "title": {
        "en": "Special Offer",
        "ar": "عرض خاص",
        "ku": "پێشکەشکراوی تایبەت"
      },
      "content": {
        "en": "Limited time offer - don't miss out!",
        "ar": "عرض لفترة محدودة - لا تفوت الفرصة!",
        "ku": "پێشکەشکراوی کاتی سنووردار - لە دەستت مەدە!"
      },
      "image_url": "https://example.com/special-offer.jpg",
      "link_url": "/special-offers",
      "priority": 10,
      "is_currently_active": true
    }
  ],
  "placement": "home",
  "slug": "my-store-slug",
  "store_name": {
    "en": "My Store",
    "ar": "متجري",
    "ku": "دوکانەکەم"
  }
}
```

### Response 404 Error - Store Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Store not found"
}
```

## POST /public/ads/{{advertisement_id}}/view — Record Advertisement View

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#post-record-advertisement-view)

Records when an advertisement is viewed by a user. Used for analytics tracking.

- **None required** - Public endpoint

### Features
- Anonymous tracking support
- User session correlation
- Browser fingerprinting
- Geographic tracking via IP
- Graceful error handling

### Use Cases
- Impression tracking
- User engagement analysis
- Geographic performance data
- A/B testing metrics
- Campaign reach measurement

### Implementation Notes
- Call when advertisement becomes visible
- Handle failures gracefully (don't break user experience)
- Respect privacy regulations
- Consider rate limiting for abuse prevention

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/public/ads/{{advertisement_id}}/view

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| advertisement_id | path string | Yes | UUID of the viewed advertisement |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| viewer_info | object |  | Object containing viewer context |  |
| viewer_info.user_id | string | No | Authenticated user ID |  |
| viewer_info.session_id | string | No | Session identifier |  |
| viewer_info.user_agent | string | No | Browser user agent |  |
| viewer_info.ip | string | No | Client IP address |  |



### Request body (raw)

```json
{
  "viewer_info": {
    "user_id": "user-uuid",
    "session_id": "session-uuid",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "ip": "192.168.1.1"
  }
}
```

### Response 200 Success - View Recorded

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "View recorded successfully"
}
```

### Response 200 Success - Graceful Failure

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "View processed"
}
```

## POST /public/ads/{{advertisement_id}}/click — Record Advertisement Click

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#post-record-advertisement-click)

Records when an advertisement is clicked by a user. Used for conversion tracking and analytics.

- **None required** - Public endpoint

### Features
- Click-through tracking
- User journey correlation
- Conversion attribution
- Geographic click data
- Fraud detection support

### Use Cases
- CTR calculation
- Conversion tracking
- User engagement measurement
- Campaign effectiveness
- ROI analysis
- Attribution modeling

### Implementation Notes
- Call immediately on advertisement click
- Fire before navigation to target URL
- Handle network failures gracefully
- Consider click fraud prevention
- Respect user privacy settings

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/public/ads/{{advertisement_id}}/click

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| advertisement_id | path string | Yes | UUID of the clicked advertisement |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| clicker_info | object |  | Object containing click context |  |
| clicker_info.user_id | string | No | Authenticated user ID |  |
| clicker_info.session_id | string | No | Session identifier |  |
| clicker_info.user_agent | string | No | Browser user agent |  |
| clicker_info.ip | string | No | Client IP address |  |



### Request body (raw)

```json
{
  "clicker_info": {
    "user_id": "user-uuid",
    "session_id": "session-uuid",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
    "ip": "192.168.1.1"
  }
}
```

### Response 200 Success - Click Recorded

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Click recorded successfully"
}
```

### Response 200 Success - Graceful Failure

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Click processed"
}
```

## GET /public/storefront/markets — List Storefront Markets (with primary variant)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-list-storefront-markets-with-primary-variant)

Returns a paginated list of published markets, each with at most one primary active variant. Useful for discovery/listing pages.

### Response
- markets: Array of markets, each with `country` { code, name, dial_code } and optional primary_variant
- pagination: page, limit, total, totalPages

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/markets?page=1&limit=10&search=&language=en&country=

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number (default: 1) Page number (default 1) | 1 |
| limit | query string | No | Items per page (default: 10) Page size (default 10) | 10 |
| search | query string | No | Search by store name or slug (optional) Filter by store name or slug |  |
| language | query string | No | Localization language (optional, default: en) Localization language code | en |
| country | query string | No | Optional. ISO 3166-1 alpha-2/alpha-3. Filters by tenant domestic country (tenant_settings override, else system_settings default). ISO country code; filters by tenant domestic country (system_settings / tenant_settings `domestic_country`) |  |



### Response 200 Success - Markets With Primary Variant

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "markets": [
      {
        "id": "market-uuid-1",
        "code": 1000,
        "slug": "store-one",
        "store_name": {
          "en": "Store One"
        },
        "status": "approved",
        "publish_status": "published",
        "logo_url": "https://example.com/logo.png",
        "theme_settings": {
          "primary_color": "#007AFF"
        },
        "banners": [],
        "country": {
          "code": "CA",
          "name": "Canada",
          "dial_code": "+1"
        },
        "primary_variant": {
          "id": "variant-uuid-1",
          "name": {
            "en": "Default Variant"
          },
          "description": null,
          "price": 29.99,
          "images": [],
          "product_id": "product-uuid-1"
        }
      },
      {
        "id": "market-uuid-2",
        "code": 1001,
        "slug": "store-two",
        "store_name": {
          "en": "Store Two"
        },
        "status": "approved",
        "publish_status": "published",
        "logo_url": null,
        "theme_settings": {},
        "banners": [],
        "country": null,
        "primary_variant": null
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 2,
      "totalPages": 1
    }
  }
}
```

## GET /public/push/onesignal — Get OneSignal Public Config

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-get-onesignal-public-config)

Returns the public OneSignal App ID for frontend use.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/push/onesignal

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "appId": "05a0dd64-de54-4a08-a65e-dd8f2b52b686"
}
```

## POST /public/demo-request — Create Demo Request

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#post-create-demo-request)

Submits a new demo request from a prospective tenant.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/public/demo-request

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string | Yes | Contact person's name (2-100 chars) |  |
| email | string | Yes | Valid email address |  |
| phone_number | string | Yes | Contact number (6-20 chars) |  |
| notes | string | No | Additional details or requirements (max 2000 chars) |  |
| interested_modules | array<string> | No | Array of modules interested in (e.g., 'delivery', 'storefront', 'restaurant') |  |



### Request body (raw)

```json
{
  "name": "John Doe",
  "email": "john.doe@company.com",
  "phone_number": "+15551234567",
  "notes": "We are a logistics company looking to modernize our fleet management.",
  "interested_modules": [
    "delivery",
    "storefront"
  ]
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "demo_request_submitted_successfully",
  "data": {
    "id": "a1b2c3d4-e5f6-7890-1234-56789abcdef0",
    "status": "Requested"
  }
}
```

### Response 400 Error - Validation Failed

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "validation_failed",
  "errors": [
    "name:min_length",
    "email:invalid_email",
    "phone_number:min_length"
  ]
}
```

### Response 201 Success (201 Created)



```json
{
  "success": true,
  "message": "demo_request_submitted_successfully",
  "data": {
    "id": "request-uuid",
    "status": "Requested"
  }
}
```

### Response 400 Error (400 Bad Request) - Validation Failed



```json
{
  "success": false,
  "message": "validation_failed",
  "errors": [
    "email:invalid_email"
  ]
}
```

## GET /public/system-modules — List Public System Modules

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/general#get-list-public-system-modules)

Retrieves a list of active public system modules. Useful for populating dropdowns in forms like 'Request Demo'.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/system-modules?language=en

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "modules_retrieved_successfully",
  "modules": [
    {
      "code": "delivery",
      "name": "Delivery Management",
      "description": "Comprehensive delivery tracking and fleet management"
    },
    {
      "code": "storefront",
      "name": "E-commerce Storefront",
      "description": "Online store builder and management"
    }
  ]
}
```

## GET /public/restaurant/locations/:code/staff — List Staff by Location Code

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/overview#get-list-staff-by-location-code)

Look up restaurant staff using the location’s public code.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/locations/:code/staff

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| code | path string | Yes | Dynamic path value from the request URL. |  |



## GET /public/restaurants — List Restaurants

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/info#get-list-restaurants)

Browse public restaurant listings with search, language and pagination options.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurants?search={{search}}&page={{page}}&limit={{limit}}&lang={{lang}}

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| search | query string |  |  | {{search}} |
| page | query string |  |  | {{page}} |
| limit | query string |  |  | {{limit}} |
| lang | query string |  |  | {{lang}} |



## GET /public/restaurant/:slug/info — Get Restaurant Info

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/info#get-get-restaurant-info)

Load the public information for the restaurant identified by its slug.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/info

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



## GET /public/restaurant/:slug/tables — List Tables

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/tables#get-list-tables-2)

Browse a restaurant’s tables through its public location page.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/tables?page={{page}}&limit={{limit}}

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | {{page}} |
| limit | query string |  |  | {{limit}} |



## GET /public/restaurant/:slug/tables/by-code/:code — Get Table by Code

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/tables#get-get-table-by-code)

Resolve a table code within a restaurant, such as when opening a table link.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/tables/by-code/:code

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |
| code | path string | Yes | Dynamic path value from the request URL. |  |



## GET /public/restaurant/:slug/menu — Get Menu

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/menu-and-products#get-get-menu-2)

Load a restaurant’s public menu, optionally narrowed to a category.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/menu?category_id={{category_id}}&page=1&limit=12

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| category_id | query string |  |  | {{category_id}} |
| page | query string |  |  | 1 |
| limit | query string |  |  | 12 |



## GET /public/restaurant/:slug/categories — List Categories

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/menu-and-products#get-list-categories-2)

Browse public restaurant categories for catalog navigation.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/categories?page={{page}}&limit={{limit}}&parent_id={{parent_id}}&is_active={{is_active}}&includeAvailableCounts={{includeAvailableCounts}}&search={{search}}

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | {{page}} |
| limit | query string |  |  | {{limit}} |
| parent_id | query string |  |  | {{parent_id}} |
| is_active | query string |  |  | {{is_active}} |
| includeAvailableCounts | query string |  |  | {{includeAvailableCounts}} |
| search | query string |  |  | {{search}} |



## GET /public/restaurant/:slug/menus — List Menus

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/menu-and-products#get-list-menus-2)

Browse the menus listed for a restaurant.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/menus?page={{page}}&limit={{limit}}&search={{search}}&lang={{lang}}

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | {{page}} |
| limit | query string |  |  | {{limit}} |
| search | query string |  |  | {{search}} |
| lang | query string |  |  | {{lang}} |



## GET /public/restaurant/:slug/products — List Products

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/menu-and-products#get-list-products)

Browse public restaurant products by menu, category or search term.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/products?page={{page}}&limit={{limit}}&menu_id={{menu_id}}&category_id={{category_id}}&search={{search}}&lang={{lang}}

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | {{page}} |
| limit | query string |  |  | {{limit}} |
| menu_id | query string |  |  | {{menu_id}} |
| category_id | query string |  |  | {{category_id}} |
| search | query string |  |  | {{search}} |
| lang | query string |  |  | {{lang}} |



## GET /public/restaurant/:slug/products/:productId — Get Product (by ID path)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/menu-and-products#get-get-product-by-id-path)

Returns product details including ingredients.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/products/:productId

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |
| productId | path string | Yes | Dynamic path value from the request URL. |  |



## GET /public/restaurant/:slug/products/product — Get Product (by sku or product_id query)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/menu-and-products#get-get-product-by-sku-or-product-id-query)

Returns product details including ingredients.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/products/product?sku={{sku}}&product_id={{product_id}}

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| sku | query string |  |  | {{sku}} |
| product_id | query string |  |  | {{product_id}} |



## POST /public/restaurant/:slug/orders — Create Order from Cart

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/orders#post-create-order-from-cart)

Optional user_id allows creating an order for another user; defaults to the authenticated user.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/public/restaurant/:slug/orders

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| slug | path string | Yes | Dynamic path value from the request URL. |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| cart_id | string |  |  |  |
| order_type | string |  |  |  |
| payment_method | string |  |  |  |
| table_id | null |  |  |  |
| session_id | null |  |  |  |
| tip_amount | number |  |  |  |
| notes | null |  |  |  |
| user_id | string |  |  |  |



### Request body (raw)

```json
{
  "cart_id": "{{cart_id}}",
  "order_type": "dine_in",
  "payment_method": "pay_at_counter",
  "table_id": null,
  "session_id": null,
  "tip_amount": 0,
  "notes": null,
  "user_id": "{{user_id}}"
}
```

## POST /restaurant/sessions — Create Dine-In Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/sessions#post-create-dine-in-session)

Creates a dine-in session. The opener is the authenticated user. Optional customer_id can be passed; otherwise it falls back to opener.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/sessions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| location_id | string |  |  |  |
| slug | string |  |  |  |
| table_id | string |  |  |  |
| customer_id | string |  |  |  |
| started_at | string |  |  |  |
| closed_at | string |  |  |  |
| note | string |  |  |  |



### Request body (raw)

```json
{
  "location_id": "{{location_id}}",
  "slug": "{{slug}}",
  "table_id": "{{table_id}}",
  "customer_id": "{{customer_id}}",
  "started_at": "{{started_at}}",
  "closed_at": "{{closed_at}}",
  "note": "{{note}}"
}
```

## GET /restaurant/sessions — List Sessions (by user)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/sessions#get-list-sessions-by-user-2)

Lists sessions for the authenticated user (or user_id query if allowed).

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/restaurant/sessions?time_filter={{time_filter}}&sort={{sort}}&page={{page}}&limit={{limit}}

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| time_filter | query string |  | CSV of past,present,future | {{time_filter}} |
| sort | query string |  | asc\|desc | {{sort}} |
| page | query string |  |  | {{page}} |
| limit | query string |  |  | {{limit}} |



## POST /restaurant/sessions/:session_id/close — Close Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/public/sessions#post-close-session-2)

Close a restaurant dining session and record its closing time.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/restaurant/sessions/:session_id/close

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| session_id | path string | Yes | Dynamic path value from the request URL. |  |



## GET /public/storefront/:storeSlug/validate — Validate Store Slug

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/store#get-validate-store-slug)

Check if a store slug exists and is publicly accessible.

Returns market info including currency, allowed countries, and theme settings when valid.

### No auth required.

### Response fields
- `is_valid`: true when market exists, is published, and is active
- `exists`: true when market exists regardless of publish/active state
- `market`: full market context (null when not valid)

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/validate

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes | Store slug to validate | my-store |



### Response 200 Valid Store

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "slug": "my-store",
    "is_valid": true,
    "exists": true,
    "market": {
      "id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store",
        "ar": "متجري"
      },
      "status": "active",
      "currency": {
        "code": "USD",
        "symbol": "$"
      },
      "default_currency": {
        "code": "USD",
        "symbol": "$"
      },
      "default_country": {
        "code": "US",
        "name": "United States",
        "dial_code": "+1"
      },
      "allowed_countries": [
        {
          "code": "US",
          "name": "United States",
          "dial_code": "+1"
        }
      ],
      "logo_url": "https://cdn.example.com/logo.png",
      "theme_settings": {}
    }
  }
}
```

### Response 200 Store Not Found

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "slug": "unknown-store",
    "is_valid": false,
    "exists": false,
    "market": null
  }
}
```

## GET /public/storefront/:storeSlug/info — Get Store Info

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/store#get-get-store-info)

Get detailed store/market information including branding, theme settings, and banners.

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/info

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes | Store slug | my-store |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "id": "market-uuid",
    "code": 1001,
    "slug": "my-store",
    "store_name": {
      "en": "My Store",
      "ar": "متجري",
      "ku": "فرۆشگاکەم"
    },
    "tagline": {
      "en": "Quality you can trust"
    },
    "logo_url": "https://cdn.example.com/logo.png",
    "theme_settings": {
      "primary_color": "#3B82F6"
    },
    "banners": [],
    "status": "active",
    "created_at": "2024-01-01T00:00:00Z"
  }
}
```

### Response 404 Store Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Store not found"
}
```

## GET /public/storefront/resolve-domain — Resolve Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/store#get-resolve-custom-domain)

Resolve a custom domain to its corresponding storefront market.

Used by storefronts hosted on custom domains to determine which market to load.

### No auth required.

Response is cached with `Cache-Control: public, max-age=60`.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/resolve-domain?domain=shop.example.com

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| domain | query string |  | Custom domain or hostname to resolve to a store | shop.example.com |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | public, max-age=60 |



```json
{
  "success": true,
  "data": {
    "slug": "my-store",
    "is_valid": true,
    "exists": true,
    "market": {
      "id": "market-uuid",
      "tenant_id": "tenant-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      },
      "status": "active",
      "currency": {
        "code": "USD",
        "symbol": "$"
      },
      "logo_url": null,
      "theme_settings": {}
    }
  }
}
```

### Response 404 Domain Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Store not found"
}
```

## GET /public/delivery/customer-portal/resolve-domain — Resolve Customer Portal Custom Domain

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/store#get-resolve-customer-portal-custom-domain)

Resolve a custom domain to its corresponding tenant's customer portal.

Used by the customer portal hosted on a custom domain to determine which tenant + branding to load.

### No auth required.

Response is cached with `Cache-Control: public, max-age=300`.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/delivery/customer-portal/resolve-domain?domain=portal.tenant.com

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| domain | query string |  | Custom domain or hostname to resolve to a tenant customer portal | portal.tenant.com |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | public, max-age=300 |



```json
{
  "success": true,
  "data": {
    "tenant_id": "tenant-uuid",
    "tenant_slug": "tenant-one",
    "custom_domain": "portal.tenant.com",
    "portal_settings": {
      "pickup_settings": {
        "mode": "address",
        "allow_change": true,
        "default_pickup_address_id": null
      },
      "delivery_service_settings": {
        "default_delivery_service_id": null
      },
      "cod_settings": {
        "enabled": true
      },
      "delivery_creation_fields": {},
      "package_settings": {},
      "visibility_settings": {},
      "support_ticket_settings": {
        "enabled": true,
        "allow_customer_close": true,
        "default_priority": "normal"
      },
      "wallet_settings": {}
    },
    "brand_settings": {
      "name": {
        "en": "Tenant One"
      },
      "page_title": "Tenant One Portal"
    },
    "assets": {
      "favicon_url": "https://cdn.example.com/favicon.ico",
      "logo_url": "https://cdn.example.com/logo.png"
    },
    "theme": {
      "colors": {
        "primary": "#3B82F6"
      }
    }
  }
}
```

### Response 404 Domain Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "error": "domain_not_found",
  "message": "Domain not found"
}
```

## GET /public/storefront/markets — List All Markets

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/store#get-list-all-markets-2)

List all published active markets with their primary variant (first listed product image/price).

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/markets?page=1&limit=10

### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) page number | 1 |
| limit | query string |  | Items per page (default: 10) results per page | 10 |
| search | query string | No | Search by store name or slug filter by store name (en) or slug |  |
| country | query string | No | Filter by ISO country code (e.g. US, IQ) ISO 2-3 letter country code filter |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "markets": [
      {
        "id": "market-uuid",
        "code": 1001,
        "slug": "my-store",
        "store_name": {
          "en": "My Store",
          "ar": "متجري"
        },
        "status": "active",
        "publish_status": "published",
        "logo_url": "https://cdn.example.com/logo.png",
        "theme_settings": {},
        "banners": [],
        "country": {
          "code": "IQ",
          "name": "Iraq",
          "dial_code": "+964"
        },
        "primary_variant": {
          "id": "variant-uuid",
          "slug": "sample-product-default",
          "name": {
            "en": "Sample Product"
          },
          "price": 25,
          "images": [
            "https://cdn.example.com/img.jpg"
          ],
          "product_id": "product-uuid",
          "product_slug": "sample-product",
          "has_variant": false
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

## GET /public/storefront/:storeSlug/catalog — Browse Products (Catalog)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/products#get-browse-products-catalog)

Browse all active products in a storefront catalog.

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/catalog?page=1&limit=12

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes |  | my-store |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number (default: 1) pagination | 1 |
| limit | query string |  | Products per page (default: 12) pagination | 12 |
| search | query string | No | Search by product name or description search across name/description |  |
| category_id | query string | No | Filter by category UUID filter by category |  |
| brand_id | query string | No | Filter by brand UUID filter by brand |  |
| featured_type | query string | No | Featured products type: most_loved `most_loved` for popularity-sorted products |  |
| pagination.total | query string |  | total matching products for current filters (not the length of the current page) |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      },
      "status": "active",
      "banners": []
    },
    "products": [
      {
        "id": "product-uuid",
        "slug": "macbook-pro-laptop",
        "name": {
          "en": "MacBook Pro Laptop"
        },
        "description": {
          "en": "High-performance laptop"
        },
        "has_variant": true,
        "stock_quantity": 50,
        "rating": 4.8,
        "total_reviews": 120,
        "variant": {
          "id": "variant-uuid",
          "slug": "macbook-pro-space-gray-16gb",
          "name": {
            "en": "MacBook Pro Space Gray 16GB"
          },
          "description": {
            "en": ""
          },
          "price": 1999.99,
          "sku": "MBP-001",
          "images": [],
          "stock_quantity": 50,
          "attributes": {}
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 12,
      "total": 24,
      "totalPages": 2
    }
  }
}
```

### Response 404 Store Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Store not found"
}
```

## GET /public/storefront/:storeSlug/catalog/:listType — Browse Product Feed (New Arrivals / Trending / Top Selling / Recommended)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/products#get-browse-product-feed-new-arrivals-trending-top-selling-recommended)

Curated product feeds powered by RPCs.

`listType` must be one of:
- `new-arrivals` — recently added products
- `trending` — gaining traction in the last N days
- `top-selling` — highest order volume
- `recommended` — personalized recommendations (uses user context if available)

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/catalog/:listType?page=1&limit=12

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes |  | my-store |
| listType | path string | Yes | One of: new-arrivals, trending, top-selling, recommended | new-arrivals |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 12 |
| category_id | query string | No | Filter by category (supported for new-arrivals, trending, top-selling) |  |
| window_days | query string | No | Time window in days (trending default 14, top-selling default 90) |  |



### Response 200 Success - New Arrivals

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      },
      "status": "active",
      "banners": []
    },
    "products": [
      {
        "id": "product-uuid",
        "slug": "wireless-earbuds",
        "name": {
          "en": "Wireless Earbuds"
        },
        "description": {
          "en": ""
        },
        "has_variant": true,
        "stock_quantity": 30,
        "rating": 4.5,
        "total_reviews": 80,
        "categories": [
          {
            "id": "cat-uuid",
            "name": {
              "en": "Electronics"
            }
          }
        ],
        "variant": {
          "id": "variant-uuid",
          "slug": "wireless-earbuds-black",
          "name": {
            "en": "Wireless Earbuds Black"
          },
          "price": 49.99,
          "sku": "WE-BLK",
          "images": [],
          "stock_quantity": 30,
          "attributes": {}
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 12,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

## GET /public/storefront/:storeSlug/products/:productId — Get Product by ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/products#get-get-product-by-id)

Get a single product with all active variants.

### Variants are returned sorted

primary variant first, then by creation date ascending.

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/products/:productId

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes |  | my-store |
| productId | path string | Yes | Product UUID | product-uuid |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      }
    },
    "product": {
      "id": "product-uuid",
      "slug": "macbook-pro-laptop",
      "market_id": "market-uuid",
      "brand_id": "brand-uuid",
      "name": {
        "en": "MacBook Pro Laptop",
        "ar": "لابتوب ماك بوك برو"
      },
      "description": {
        "en": "High-performance laptop"
      },
      "price": 1999.99,
      "is_active": true,
      "has_variant": true,
      "tags": [
        "laptop",
        "apple"
      ],
      "images": [
        "https://cdn.example.com/product.jpg"
      ],
      "stock_quantity": 80,
      "rating": 4.8,
      "total_reviews": 120,
      "variant_options": [
        "colors",
        "ram"
      ],
      "available_option_values": {
        "colors": [
          "Space Gray",
          "Silver"
        ],
        "ram": [
          "16GB",
          "32GB"
        ]
      },
      "variants": [
        {
          "id": "variant-uuid",
          "slug": "macbook-pro-space-gray-16gb",
          "name": {
            "en": "MacBook Pro Space Gray 16GB"
          },
          "price": 1999.99,
          "sku": "MBP-SG-16",
          "stock_quantity": 40,
          "is_active": true,
          "is_primary": true,
          "images": [],
          "attributes": {
            "colors": "Space Gray",
            "ram": "16GB"
          }
        }
      ],
      "categories": [
        {
          "id": "cat-uuid",
          "name": {
            "en": "Electronics"
          }
        }
      ],
      "brand": {
        "id": "brand-uuid",
        "name": {
          "en": "Apple"
        }
      },
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    }
  }
}
```

### Response 404 Product Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Product not found"
}
```

## GET /public/storefront/:storeSlug/products/slug/:productSlug — Get Product by Slug

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/products#get-get-product-by-slug)

Get a single product by its URL slug with all active variants.

Identical response shape to **Get Product by ID** — use this endpoint for SEO-friendly product pages.

### Variants are returned sorted

primary variant first, then by creation date ascending.

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/products/slug/:productSlug

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes |  | my-store |
| productSlug | path string | Yes | Product slug | macbook-pro-laptop |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      }
    },
    "product": {
      "id": "product-uuid",
      "slug": "macbook-pro-laptop",
      "market_id": "market-uuid",
      "brand_id": "brand-uuid",
      "name": {
        "en": "MacBook Pro Laptop",
        "ar": "لابتوب ماك بوك برو"
      },
      "description": {
        "en": "High-performance laptop"
      },
      "price": 1999.99,
      "is_active": true,
      "has_variant": true,
      "tags": [
        "laptop",
        "apple"
      ],
      "images": [
        "https://cdn.example.com/product.jpg"
      ],
      "stock_quantity": 80,
      "rating": 4.8,
      "total_reviews": 120,
      "variant_options": [
        "colors",
        "ram"
      ],
      "available_option_values": {
        "colors": [
          "Space Gray",
          "Silver"
        ],
        "ram": [
          "16GB",
          "32GB"
        ]
      },
      "variants": [
        {
          "id": "variant-uuid",
          "slug": "macbook-pro-space-gray-16gb",
          "name": {
            "en": "MacBook Pro Space Gray 16GB"
          },
          "price": 1999.99,
          "sku": "MBP-SG-16",
          "stock_quantity": 40,
          "is_active": true,
          "is_primary": true,
          "images": [],
          "attributes": {
            "colors": "Space Gray",
            "ram": "16GB"
          }
        }
      ],
      "categories": [
        {
          "id": "cat-uuid",
          "name": {
            "en": "Electronics"
          }
        }
      ],
      "brand": {
        "id": "brand-uuid",
        "name": {
          "en": "Apple"
        }
      },
      "created_at": "2024-01-01T00:00:00Z",
      "updated_at": "2024-01-01T00:00:00Z"
    }
  }
}
```

### Response 404 Product Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Product not found"
}
```

## GET /public/storefront/:storeSlug/products/:productId/similar — Get Similar Products

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/products#get-get-similar-products)

Get products similar to the given product (same categories, via RPC).

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/products/:productId/similar?page=1&limit=10

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes |  | my-store |
| productId | path string | Yes |  | product-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  | Max 50 | 10 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      },
      "status": "active"
    },
    "products": [
      {
        "id": "product-uuid",
        "slug": "wireless-earbuds",
        "name": {
          "en": "Wireless Earbuds"
        },
        "description": {
          "en": ""
        },
        "has_variant": true,
        "stock_quantity": 30,
        "rating": 4.5,
        "total_reviews": 80,
        "variant": {
          "id": "variant-uuid",
          "slug": "wireless-earbuds-black",
          "name": {
            "en": "Wireless Earbuds Black"
          },
          "price": 49.99,
          "sku": "WE-BLK",
          "images": [],
          "stock_quantity": 30,
          "attributes": {}
        }
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 10,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

## GET /public/storefront/:storeSlug/categories — Get Categories

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/categories#get-get-categories)

Get the full hierarchical category tree for a storefront.

Returns nested parent → children structure.

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/categories

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes |  | my-store |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      }
    },
    "categories": [
      {
        "id": "cat-uuid",
        "name": {
          "en": "Electronics",
          "ar": "إلكترونيات"
        },
        "slug": "electronics",
        "image_url": null,
        "parent_id": null,
        "is_active": true,
        "children": [
          {
            "id": "subcat-uuid",
            "name": {
              "en": "Laptops"
            },
            "slug": "laptops",
            "parent_id": "cat-uuid",
            "is_active": true,
            "children": []
          }
        ]
      }
    ]
  }
}
```

## GET /public/storefront/:storeSlug/brands — List Brands

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/brands#get-list-brands-2)

List all active brands for a storefront.

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: GET https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/brands?page=1&limit=20

### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes |  | my-store |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |
| search | query string | No | Search brand name |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "market": {
      "id": "market-uuid",
      "slug": "my-store",
      "store_name": {
        "en": "My Store"
      },
      "status": "active"
    },
    "brands": [
      {
        "id": "brand-uuid",
        "name": {
          "en": "Apple",
          "ar": "آبل"
        },
        "logo_url": "https://cdn.example.com/apple-logo.png",
        "is_active": true
      }
    ],
    "pagination": {
      "page": 1,
      "limit": 20,
      "total": 1,
      "totalPages": 1
    }
  }
}
```

## POST /public/storefront/:storeSlug/delivery/price — Calculate Delivery Fee

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/public-apis/storefront/delivery#post-calculate-delivery-fee)

Calculate delivery fee for a cart before placing an order.

Does **not** create any order or booking.

### No auth required.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/public/storefront/:storeSlug/delivery/price

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| storeSlug | path string | Yes |  | my-store |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_service_id | string | Yes | UUID of the delivery service |  |
| dropoff | object |  |  |  |
| dropoff.latitude | number | Yes | / `dropoff.longitude`: customer drop-off coordinates |  |
| dropoff.longitude | number |  |  |  |
| items | array<object> | Yes | array of `{ variant_id, quantity }` |  |
| items.variant_id | string |  |  |  |
| items.quantity | number |  |  |  |



### Request body (raw)

```json
{
  "delivery_service_id": "service-uuid",
  "dropoff": {
    "latitude": 33.3152,
    "longitude": 44.3661
  },
  "items": [
    {
      "variant_id": "variant-uuid-1",
      "quantity": 2
    },
    {
      "variant_id": "variant-uuid-2",
      "quantity": 1
    }
  ]
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "data": {
    "delivery_fee": 3.5,
    "currency": "USD",
    "breakdown": {
      "base_fee": 2,
      "distance_fee": 1.5
    }
  }
}
```

### Response 400 Validation Error

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "delivery_service_id is required"
}
```

## GET /tenants — List Tenants

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#get-list-tenants)

Retrieves a paginated list of tenants in the system. This endpoint supports filtering by status and searching by tenant slug.

- Only system administrators can access this endpoint

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants?language=en&page=1&limit=10&status=&search=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |
| page | query string | No | Page number for pagination (Optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of results per page (Optional, default: 10, max: 100) Number of results per page (default: 10, max: 100) | 10 |
| status | query string | No | Filter tenants by status (Optional, e.g., 'active', 'pending', 'suspended') Filter tenants by status (e.g., 'active', 'pending', 'suspended') |  |
| search | query string | No | Search term to filter tenants by slug (Optional) |  |



### Response 200 Success Response



```json
{
  "success": true,
  "message": "Tenants retrieved successfully",
  "tenants": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "name": {
        "en": "Tenant Name"
      },
      "slug": "tenant-slug",
      "status": "active",
      "contact_info": {},
      "metadata": {},
      "created_at": "2023-01-01T00:00:00Z",
      "created_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "updated_at": "2023-01-01T00:00:00Z",
      "updated_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "tenant_users": {
        "count": 5
      }
    }
  ],
  "total": 25,
  "page": 1,
  "limit": 10
}
```

### Response 400 Error - Invalid Parameters



```json
{
  "success": false,
  "message": "Invalid pagination parameters"
}
```

### Response 403 Error - Unauthorized



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 500 Error - Server Error



```json
{
  "success": false,
  "message": "Failed to fetch tenants"
}
```

### Response 500 Error - Internal Server Error



```json
{
  "success": false,
  "message": "Internal server error"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Tenants retrieved successfully",
  "tenants": [
    {
      "id": "uuid",
      "name": {
        "en": "Tenant Name"
      },
      "slug": "tenant-slug",
      "status": "active",
      "contact_info": {},
      "metadata": {},
      "created_at": "2023-01-01T00:00:00Z",
      "created_by": "uuid",
      "updated_at": "2023-01-01T00:00:00Z",
      "updated_by": "uuid",
      "tenant_users": {
        "count": 5
      }
    }
  ],
  "total": 25,
  "page": 1,
  "limit": 10
}
```

## POST /tenants — Create Tenant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#post-create-tenant)

Creates a new tenant in the system. This endpoint is restricted to system administrators only.

- Only system administrators can access this endpoint

### Important
- Tenant subscription gate fields are normalized immediately after creation via `recompute_tenant_status` (respects the global `tenant_subscription.enabled` setting).

### Required Fields
- `name`: Object containing multilingual tenant names (at least one language required)
- `slug`: URL-friendly identifier (must be unique, lowercase alphanumeric with hyphens only)

### Optional Fields
- `contact_info`: Object containing contact information (email, phone, address, etc.)
- `metadata`: Object containing additional tenant metadata

### Validations
- Slug must match pattern: ^[a-z0-9]+(?:-[a-z0-9]+)*$
- Slug must be unique across all tenants
- Name must be provided in at least one language
- Each language name must be a non-empty string

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenants?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language of the response Language code for localized response messages (e.g., 'en') | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.fr | string |  |  |  |
| slug | string |  |  |  |
| contact_info | object |  |  |  |
| contact_info.email | string |  |  |  |
| contact_info.phone | string |  |  |  |
| contact_info.address | string |  |  |  |
| metadata | object |  |  |  |
| metadata.industry | string |  |  |  |
| metadata.size | string |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Tenant Name",
    "fr": "Nom du Locataire"
  },
  "slug": "tenant-slug",
  "contact_info": {
    "email": "contact@example.com",
    "phone": "+1234567890",
    "address": "123 Main St"
  },
  "metadata": {
    "industry": "technology",
    "size": "enterprise"
  }
}
```

### Response 201 Success - Tenant Created

Created

```json
{
  "success": true,
  "message": "Tenant created successfully",
  "tenant": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "name": {
      "en": "Tenant Name",
      "fr": "Nom du Locataire"
    },
    "slug": "tenant-slug",
    "contact_info": {
      "email": "contact@example.com",
      "phone": "+1234567890",
      "address": "123 Main St"
    },
    "status": "pending",
    "metadata": {
      "industry": "technology",
      "size": "enterprise"
    },
    "subscription_status": "NONE",
    "effective_active": true,
    "effective_until": null,
    "created_at": "2023-01-01T00:00:00Z",
    "created_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "created_by_user": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "email": "admin@example.com"
    }
  }
}
```

### Response 400 Error - Missing Name

Bad Request

```json
{
  "success": false,
  "message": "Tenant name required in at least one language"
}
```

### Response 400 Error - Missing Slug

Bad Request

```json
{
  "success": false,
  "message": "Tenant slug required"
}
```

### Response 400 Error - Invalid Slug Format

Bad Request

```json
{
  "success": false,
  "message": "Invalid slug format"
}
```

### Response 400 Error - Slug Already In Use

Bad Request

```json
{
  "success": false,
  "message": "Slug already in use"
}
```

### Response 403 Error - Unauthorized

Forbidden

```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 500 Error - Server Error

Internal Server Error

```json
{
  "success": false,
  "message": "Failed to create tenant"
}
```

### Response 201 Success (201 Created)



```json
{
  "success": true,
  "message": "Tenant created successfully",
  "tenant": {
    "id": "uuid",
    "name": {
      "en": "Tenant Name",
      "fr": "Nom du Locataire"
    },
    "slug": "tenant-slug",
    "contact_info": {
      "email": "contact@example.com",
      "phone": "+1234567890",
      "address": "123 Main St"
    },
    "status": "pending",
    "metadata": {
      "industry": "technology",
      "size": "enterprise"
    },
    "subscription_status": "NONE",
    "effective_active": true,
    "effective_until": null,
    "created_at": "2023-01-01T00:00:00Z",
    "created_by": "uuid",
    "created_by_user": {
      "id": "uuid",
      "email": "admin@example.com"
    }
  }
}
```

### Response 400 Validation Error (400 Bad Request)



```json
{
  "success": false,
  "message": "Invalid name format"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /tenants/wizard — Create Tenant Wizard

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#post-create-tenant-wizard)

Creates a fully configured tenant in a single API call. If any step fails, the endpoint performs compensating rollback (best-effort) by deleting created resources (tenant + created admin user) and canceling Stripe objects when possible.

- Only system administrators can access this endpoint

### Subscription
- When system setting `tenant_subscription.enabled=true`: `subscription` is **required** (type: MONTHLY|ANNUAL|UNLIMITED, amount)
- When `tenant_subscription.enabled=false`: `subscription` is **optional** and ignored; no Stripe/subscription records created

### Notes
- Admin user is created immediately (password required)
- Email/phone are derived from tenant.contact_info
- Tenant admin role is resolved automatically
- Admin avatar uses brand_settings.assets.logo.light when provided, else tenant.logo_url

### Error

### Admin-user creation authorization

when admin_user.create=true, also requires manage:user_access:system with a system-capable JWT/API-key context. This check happens before tenant/Auth/role creation. The existing manage:system prerequisite remains. Requests without admin creation retain their existing authorization. Missing the new permission returns 403 {success:false,code:"rbac_forbidden",message:<localized>}. Frontend integration: require the new permission for the create-admin option and handle 403.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenants/wizard?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant | object |  |  |  |
| tenant.name | object |  |  |  |
| tenant.name.en | string |  |  |  |
| tenant.name.ar | string |  |  |  |
| tenant.name.ku | string |  |  |  |
| tenant.slug | string |  |  |  |
| tenant.contact_info | object |  |  |  |
| tenant.contact_info.email | string |  |  |  |
| tenant.contact_info.phone | string |  |  |  |
| tenant.logo_url | string |  |  |  |
| admin_user | object |  |  |  |
| admin_user.create | boolean |  |  |  |
| admin_user.password | string |  |  |  |
| modules | object |  |  |  |
| modules.enabled_module_ids | array<string> |  |  |  |
| subscription | object |  |  |  |
| subscription.type | string |  |  |  |
| subscription.amount | number |  |  |  |
| subscription.note_public | string |  |  |  |
| subscription.note_internal | string |  |  |  |
| brand_settings | object |  |  |  |
| brand_settings.brand_name | object |  |  |  |
| brand_settings.brand_name.en | string |  |  |  |
| brand_settings.brand_name.ar | string |  |  |  |
| brand_settings.brand_name.ku | string |  |  |  |
| brand_settings.assets | object |  |  |  |
| brand_settings.assets.logo | object |  |  |  |
| brand_settings.assets.logo.light | string |  |  |  |
| brand_settings.assets.logo.dark | string |  |  |  |
| brand_settings.assets.favicon | string |  |  |  |
| brand_settings.colors | object |  |  |  |
| brand_settings.colors.text | string |  |  |  |
| brand_settings.colors.accent | string |  |  |  |
| brand_settings.colors.primary | string |  |  |  |
| brand_settings.colors.secondary | string |  |  |  |
| brand_settings.colors.background | string |  |  |  |
| brand_settings.configurations | object |  |  |  |
| brand_settings.configurations.font | string |  |  |  |
| brand_settings.configurations.theme | string |  |  |  |
| brand_settings.configurations.animations | boolean |  |  |  |
| brand_settings.configurations.rtl_support | boolean |  |  |  |
| general_config | object |  |  |  |
| general_config.unit_size | object |  |  |  |
| general_config.unit_size.unit_length | number |  |  |  |
| general_config.unit_size.unit_weight | number |  |  |  |
| general_config.unit_size.weight_unit | string |  |  |  |
| general_config.unit_size.volumetric_divisor | number |  |  |  |
| general_config.unit_size.dimension_unit | string |  |  |  |
| general_config.unit_size.use_volumetric_weight | boolean |  |  |  |
| general_config.location | object |  |  |  |
| general_config.location.latitude | number |  |  |  |
| general_config.location.longitude | number |  |  |  |
| general_config.currency | object |  |  |  |
| general_config.currency.currency_code | string |  |  |  |
| general_config.currency.currency_name | string |  |  |  |
| general_config.currency.currency_symbol | string |  |  |  |
| general_config.currency.currency_mode | string |  |  |  |
| general_config.currency.exchange_rate_to_system | number |  |  |  |
| general_config.domestic_country | object |  |  |  |
| general_config.domestic_country.code | string |  |  |  |
| general_config.domestic_country.name | string |  |  |  |
| general_config.domestic_country.dial_code | string |  |  |  |
| general_config.driver | object |  |  |  |
| general_config.driver.mode | string |  |  |  |
| general_config.driver.percentage | object |  |  |  |
| general_config.driver.percentage.percentage_of_delivery_fee | number |  |  |  |
| general_config.driver.per_delivery | object |  |  |  |
| general_config.driver.per_delivery.base_amount | number |  |  |  |
| general_config.driver.driver_can_self_assign | boolean |  |  |  |
| general_config.driver.require_accept_or_reject_for_assignments | boolean |  |  |  |
| general_config.integration | object |  |  |  |
| general_config.integration.sms_channel | object |  |  |  |
| general_config.integration.sms_channel.selected_channel | string |  |  |  |
| general_config.warehouse | object |  |  |  |
| general_config.warehouse.handoff_required | boolean |  |  |  |
| general_config.express_shipping | object |  |  |  |
| general_config.express_shipping.customer_prefix_key | string |  |  |  |
| general_config.user_settings | object |  |  |  |
| general_config.user_settings.offline_threshold_seconds | number |  |  |  |
| general_config.user_settings.heartbeat_interval_seconds | number |  |  |  |
| general_config.mobile | object |  |  |  |
| general_config.mobile.show_tenant_list_page | boolean |  |  |  |
| general_config.storefront | object |  |  |  |
| general_config.storefront.review_auto_approval | object |  |  |  |
| general_config.storefront.review_auto_approval.enabled | boolean |  |  |  |
| general_config.storefront.review_auto_approval.auto_approve_ratings | array |  |  |  |
| general_config.delivery | object |  |  |  |
| general_config.delivery.return_requests | object |  |  |  |
| general_config.delivery.return_requests.destination_mode | string |  |  |  |
| general_config.delivery.return_requests.default_delivery_service_id | string |  |  |  |
| general_config.delivery.return_requests.auto_approval | boolean |  |  |  |
| general_config.delivery.on_demand_auto_assignment | object |  |  |  |
| general_config.delivery.on_demand_auto_assignment.enabled | boolean |  |  |  |
| general_config.delivery.on_demand_auto_assignment.priority_checks | object |  |  |  |
| general_config.delivery.on_demand_auto_assignment.priority_checks.driver_online | boolean |  |  |  |
| general_config.delivery.on_demand_auto_assignment.priority_checks.preferred_zone | boolean |  |  |  |
| general_config.delivery.on_demand_auto_assignment.priority_checks.vehicle_capacity | boolean |  |  |  |
| general_config.delivery.on_demand_auto_assignment.priority_checks.available_in_zone | boolean |  |  |  |
| general_config.delivery.on_demand_auto_assignment.max_retry_attempts | number |  |  |  |
| general_config.delivery.on_demand_auto_assignment.retry_after_minutes | number |  |  |  |
| general_config.delivery.on_demand_auto_assignment.notify_admin_on_failure | boolean |  |  |  |



### Request body (raw)

```json
{
  "tenant": {
    "name": {
      "en": "Acme Logistics",
      "ar": "",
      "ku": ""
    },
    "slug": "acme-logistics",
    "contact_info": {
      "email": "admin@acme.com",
      "phone": "+19876543210"
    },
    "logo_url": "https://..."
  },
  "admin_user": {
    "create": true,
    "password": "StrongPassword123!"
  },
  "modules": {
    "enabled_module_ids": [
      "uuid-1",
      "uuid-2"
    ]
  },
  "subscription": {
    "type": "MONTHLY",
    "amount": 99.99,
    "note_public": "",
    "note_internal": ""
  },
  "brand_settings": {
    "brand_name": {
      "en": "Acme",
      "ar": "",
      "ku": ""
    },
    "assets": {
      "logo": {
        "light": "url",
        "dark": "url"
      },
      "favicon": "url"
    },
    "colors": {
      "text": "#000000",
      "accent": "#FF9500",
      "primary": "#007AFF",
      "secondary": "#FFFFFF",
      "background": "#F5F5F5"
    },
    "configurations": {
      "font": "Roboto",
      "theme": "light",
      "animations": false,
      "rtl_support": false
    }
  },
  "general_config": {
    "unit_size": {
      "unit_length": 30,
      "unit_weight": 5,
      "weight_unit": "KG",
      "volumetric_divisor": 5000,
      "dimension_unit": "CM",
      "use_volumetric_weight": false
    },
    "location": {
      "latitude": 36.19,
      "longitude": 44.01
    },
    "currency": {
      "currency_code": "USD",
      "currency_name": "US Dollar",
      "currency_symbol": "$",
      "currency_mode": "code",
      "exchange_rate_to_system": 1
    },
    "domestic_country": {
      "code": "CA",
      "name": "Canada",
      "dial_code": "+1"
    },
    "driver": {
      "mode": "percentage",
      "percentage": {
        "percentage_of_delivery_fee": 0
      },
      "per_delivery": {
        "base_amount": 0
      },
      "driver_can_self_assign": false,
      "require_accept_or_reject_for_assignments": false
    },
    "integration": {
      "sms_channel": {
        "selected_channel": "twilio"
      }
    },
    "warehouse": {
      "handoff_required": false
    },
    "express_shipping": {
      "customer_prefix_key": ""
    },
    "user_settings": {
      "offline_threshold_seconds": 300,
      "heartbeat_interval_seconds": 30
    },
    "mobile": {
      "show_tenant_list_page": false
    },
    "storefront": {
      "review_auto_approval": {
        "enabled": false,
        "auto_approve_ratings": []
      }
    },
    "delivery": {
      "return_requests": {
        "destination_mode": "pickup_location",
        "default_delivery_service_id": "",
        "auto_approval": true
      },
      "on_demand_auto_assignment": {
        "enabled": false,
        "priority_checks": {
          "driver_online": true,
          "preferred_zone": true,
          "vehicle_capacity": true,
          "available_in_zone": true
        },
        "max_retry_attempts": 3,
        "retry_after_minutes": 1,
        "notify_admin_on_failure": true
      }
    }
  }
}
```

### Response 201 Success Response

Created

```json
{
  "success": true,
  "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
  "message": "Tenant created successfully"
}
```

### Response 403 Error - Unauthorized

Forbidden

```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 400 Error - Validation Error

Bad Request

```json
{
  "success": false,
  "message": "Validation error"
}
```

### Response 500 Error - Server Error

Internal Server Error

```json
{
  "success": false,
  "message": "Internal server error"
}
```

### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 201 Success (201)



```json
{
  "success": true,
  "tenant_id": "<uuid>",
  "message": "Tenant created successfully"
}
```

### Response 201 Error



```json
{
  "success": false,
  "message": "<error description>"
}
```

## GET /tenants/:tenantId — Get Tenant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#get-get-tenant)

Retrieves detailed information about a specific tenant, including associated users and statistics.

- System administrators can access any tenant
- Regular users can only access tenants they are associated with

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/:tenantId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | UUID of the tenant to retrieve |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language of the response Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Tenant retrieved successfully",
  "tenant": {
    "id": "uuid",
    "name": {
      "en": "Example Tenant"
    },
    "slug": "example-tenant",
    "status": "active",
    "contact_info": {
      "email": "contact@example.com",
      "phone": "+1234567890"
    },
    "metadata": {},
    "created_at": "2023-01-01T00:00:00Z",
    "created_by": "uuid",
    "updated_at": "2023-01-01T00:00:00Z",
    "updated_by": "uuid",
    "tenant_users": [
      {
        "user_id": "uuid",
        "created_at": "2023-01-01T00:00:00Z",
        "user": {
          "id": "uuid",
          "email": "user@example.com"
        }
      }
    ],
    "created_by_user": {
      "id": "uuid",
      "email": "admin@example.com"
    },
    "updated_by_user": {
      "id": "uuid",
      "email": "admin@example.com"
    },
    "statistics": {
      "active_users": 5
    }
  }
}
```

### Response 400 Missing Tenant ID (400 Bad Request)



```json
{
  "success": false,
  "message": "Tenant ID required"
}
```

### Response 404 Tenant Not Found (404 Not Found)



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Unauthorized Access (403 Forbidden)



```json
{
  "success": false,
  "message": "Unauthorized tenant access"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Failed to fetch tenant"
}
```

## PUT /tenants/:tenantId — Update Tenant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#put-update-tenant)

Update tenant details. Requires tenant admin or system admin access.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/tenants/:tenantId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| status | string |  |  |  |
| contact_info | object |  |  |  |
| contact_info.email | string |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Updated Name"
  },
  "status": "active",
  "contact_info": {
    "email": "updated@tenant.com"
  }
}
```

## GET /tenants/:tenantId/modules — List Tenant Modules

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#get-list-tenant-modules)

Retrieves a list of modules associated with a specific tenant. Requires authentication - user must be either a system administrator or a member of the specified tenant.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/:tenantId/modules?page=1&limit=10&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | The unique identifier of the tenant |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of results per page | 10 |
| language | query string |  | Language code for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Tenant modules retrieved successfully",
  "tenantModules": [
    {
      "id": "uuid",
      "tenant_id": "tenant-uuid",
      "system_modules": {
        "name": "module-name",
        "description": "Module description",
        "version": "1.0.0",
        "dependencies": {},
        "features": []
      },
      "created_at": "timestamp",
      "updated_at": "timestamp"
    }
  ],
  "total": 10,
  "page": 1,
  "limit": 10
}
```

### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## POST /tenants/:tenantId/modules/:moduleId — Enable Tenant Module

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#post-enable-tenant-module)

Enable a module for a specific tenant. This endpoint allows system administrators to enable modules for any tenant, while tenant administrators can only enable modules for their own tenant if they have the 'manage:tenant' permission.

- User must be either a system administrator or a tenant administrator with 'manage:tenant' permission

### Validations
- The module must be active at the system level
- The module must be enabled at the system level

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenants/:tenantId/modules/:moduleId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |
| moduleId | path string | Yes | ID of the module to enable |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Module enabled successfully",
  "tenantModule": {
    "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
    "module_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "is_enabled": true,
    "enabled_at": "2023-01-01T00:00:00Z",
    "enabled_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "updated_at": "2023-01-01T00:00:00Z",
    "updated_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 400 Error - Module Not Active



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Module not active at system level"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Module enabled successfully",
  "tenantModule": {
    "tenant_id": "tenant-uuid",
    "module_id": "module-uuid",
    "is_enabled": true,
    "enabled_at": "2023-01-01T00:00:00Z",
    "enabled_by": "user-uuid",
    "updated_at": "2023-01-01T00:00:00Z",
    "updated_by": "user-uuid"
  }
}
```

### Response 400 Invalid Module State (400 Bad Request)



```json
{
  "success": false,
  "message": "Module not enabled at system level"
}
```

### Response 400 Invalid Module State (400 Bad Request)



```json
{
  "success": false,
  "message": "Module not enabled"
}
```

### Response 400 Server Error (400 Bad Request)



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## DELETE /tenants/:tenantId/modules/:moduleId — Disable Tenant Module

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#delete-disable-tenant-module)

Disable a module for a specific tenant. This endpoint allows system administrators to disable modules for any tenant, while tenant administrators can only disable modules for their own tenant if they have the 'manage:tenant' permission.

- User must be either a system administrator or a tenant administrator with 'manage:tenant' permission

### Validations
- The module must be active at the system level
- The module must be enabled at the system level

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/tenants/:tenantId/modules/:moduleId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |
| moduleId | path string | Yes | ID of the module to disable |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Module enabled successfully",
  "tenantModule": {
    "tenant_id": "550e8400-e29b-41d4-a716-446655440000",
    "module_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "is_enabled": false,
    "enabled_at": "2023-01-01T00:00:00Z",
    "enabled_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "updated_at": "2023-01-01T00:00:00Z",
    "updated_by": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 400 Error - Module Not Active



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Module not active at system level"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Module enabled successfully",
  "tenantModule": {
    "tenant_id": "tenant-uuid",
    "module_id": "module-uuid",
    "is_enabled": false,
    "enabled_at": "2023-01-01T00:00:00Z",
    "enabled_by": "user-uuid",
    "updated_at": "2023-01-01T00:00:00Z",
    "updated_by": "user-uuid"
  }
}
```

### Response 400 Invalid Module State (400 Bad Request)



```json
{
  "success": false,
  "message": "Module not enabled at system level"
}
```

### Response 400 Invalid Module State (400 Bad Request)



```json
{
  "success": false,
  "message": "Module not enabled"
}
```

### Response 400 Server Error (400 Bad Request)



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /tenants/:tenantId/users — Add User to Tenant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#post-add-user-to-tenant)

Add a user to a tenant. Requires tenant admin or system admin access.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenants/:tenantId/users?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_id | string |  |  |  |
| tenant_user_type | string |  |  |  |
| is_primary | boolean |  |  |  |
| metadata | object |  |  |  |
| invite_token | string |  |  |  |



### Request body (raw)

```json
{
  "user_id": "user-uuid",
  "tenant_user_type": "individual",
  "is_primary": false,
  "metadata": {},
  "invite_token": "optional-invite-token"
}
```

## POST /tenants/:tenantId/api-keys — Create API Key

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#post-create-api-key-2)

Create a new API key for the tenant. Returns the API key only once.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenants/:tenantId/api-keys?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| user_id | string |  |  |  |
| expires_at | string |  |  |  |



### Request body (raw)

```json
{
  "name": "API Key Name",
  "user_id": "user-uuid",
  "expires_at": "2025-12-31T23:59:59Z"
}
```

## GET /tenants/:tenantId/api-keys — List API Keys

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#get-list-api-keys-2)

List all API keys for the tenant with pagination

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/:tenantId/api-keys?page=1&limit=10&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number | 1 |
| limit | query string |  | Items per page | 10 |



## DELETE /tenants/:tenantId/api-keys/:apiKeyId — Revoke API Key

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#delete-revoke-api-key-2)

Revoke an API key

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/tenants/:tenantId/api-keys/:apiKeyId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |
| apiKeyId | path string | Yes | ID of the API key to revoke |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |



## PUT /tenants/:tenantId/api-keys/:apiKeyId — Update API Key

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#put-update-api-key-2)

Update an API key's properties

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/tenants/:tenantId/api-keys/:apiKeyId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |
| apiKeyId | path string | Yes | ID of the API key to update |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| expires_at | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": "Updated Name",
  "expires_at": "2025-12-31T23:59:59Z",
  "is_active": true
}
```

## GET /tenants/:tenantId/addresses — List Tenant Addresses

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#get-list-tenant-addresses)

List all active addresses for a tenant

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/:tenantId/addresses?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |
| page | query string |  | Page number | 1 |
| limit | query string |  | Number of addresses per page | 10 |



## GET /tenants/:tenantId/brand-settings — Get Tenant Brand Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#get-get-tenant-brand-settings-2)

Read an active tenant’s brand name, assets, colors, configuration and color-palette preset. The authenticated tenant context takes precedence over a supplied tenant ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/:tenantId/brand-settings?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language of the response | en |



## PUT /tenants/:tenantId/brand-settings — Update Tenant Brand Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#put-update-tenant-brand-settings)

Update tenant brand settings. Requires system admin access.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/tenants/:tenantId/brand-settings

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| brand_name | string |  |  |  |
| assets | object |  |  |  |
| assets.logo | string |  |  |  |
| colors | object |  |  |  |
| colors.primary | string |  |  |  |
| configurations | object |  |  |  |
| configurations.theme | string |  |  |  |



### Request body (raw)

```json
{
  "brand_name": "Updated Brand Name",
  "assets": {
    "logo": "url-to-logo"
  },
  "colors": {
    "primary": "#000000"
  },
  "configurations": {
    "theme": "dark"
  }
}
```

## DELETE /tenants/:tenantId/users/:userId — Remove User from Tenant

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#delete-remove-user-from-tenant)

Remove a user from the tenant. Cannot remove the last admin.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/tenants/:tenantId/users/:userId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |
| userId | path string | Yes | ID of the user to remove |  |



## GET /tenants/:tenantId/settings — Get Tenant Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#get-get-tenant-settings)

Get tenant settings. Optionally filter by specific keys. `delivery.delivery_external_reference.settings.unique_per_customer` controls whether a root delivery order's non-empty `external_reference_id` must be unique for the same customer in this tenant; it defaults to `true`. Staged child legs may reuse their root order's reference. `general.timezone.settings.timezone` is the tenant-wide IANA timezone used for local business times. `general.unit_size.settings.missing_unit_size_fallback` is the effective missing-unit policy; `missing_unit_size_fallback_options` contains all supported `{ value, label }` choices localized for the request language. Route sessions are created explicitly and have no scheduling-enable tenant setting. If `phone_login` or `domestic_country` is returned, the response includes `options.allowed_countries` sourced from the system-level tenant phone-login allowlist so tenant admins can only choose countries permitted by the system. If 'currency' is present in any group, the response includes top-level 'currencies' with system and tenant context. If `delivery.assignment_change_settings.settings.default_driver_id` is set and the driver is still active, available, tenant-owned, and not deleted, the response also includes `default_driver` with display information (`full_name`, `phone`, `avatar_url`). Otherwise `default_driver` is `null` while the saved ID remains available so the admin UI can show a stale selection and clear it. If `delivery.transfer_manifest` is returned, `settings.driver_scan_mode` is always `continuous` or `manual`; missing or invalid stored values resolve to `continuous`. `settings.allow_warehouse_staging_without_assigned_driver` is always boolean and defaults to `false`; when true, unassigned Transfer Manifest P&D staging may use `delivery.assignment_change_settings.settings.default_driver_id`. Settings also include optional driver workflow booleans `allow_driver_create_manifest_from_form`, `allow_driver_scan_institution_qr`, and `allow_driver_scan_item_barcode`; at least one must be true. `settings.delivery_fee_origin` is `processing_point` (default) or `source_location` and controls the price basis for transfer-manifest-created orders without changing their processing-point pickup location. Its `settings.reasons` is the tenant-enabled array of `{ value, label, description }` exception reasons. When not configured, `allow_driver_create_manifest_from_form` defaults to true and the scan toggles default to false. Warehouse package settings are returned under `warehouse.package`; `auto_execute_delivery_order_on_receive` controls whether fully received warehouse items auto-activate related draft delivery orders.

`delivery.delivery_order.settings.edit.reason_required` controls whether tenant-admin/dispatcher delivery-order edits require a reason. It defaults to `false`.

### Frontend integration contract

read this effective tenant value to decide whether the edit form must require Reason. Backend enforcement remains authoritative.

Wallet limits are separate effective settings in the `wallet` group: `wallet_minimum_balance.settings.minimum_balance` is Drivers Minimum Balance; `customer_wallet_minimum_balance.settings.minimum_balance` is Customer Minimum Balance. Tenant overrides fall back to the corresponding system value. Customer delivery payments, draft reservations, fee increases, transfers and manual debits use the customer key; driver wallet operations use the existing driver key. New customer defaults and existing tenant overrides start at their previous wallet limit to preserve behavior.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/:tenantId/settings

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| keys | query string | No | Optional: Comma-separated list of setting keys | setting1,setting2 |



### Response 200 Success - With currencies



```json
{
  "success": true,
  "message": "tenant_settings_retrieved_successfully",
  "tenantSettings": {
    "general": {
      "currency": {
        "settings": {
          "currency_code": "IQD",
          "currency_symbol": "د.ع",
          "currency_name": "Iraqi Dinar",
          "exchange_rate_to_system": 0.00076
        },
        "description": null
      },
      "phone_login": {
        "settings": {
          "allowed_countries": [
            {
              "code": "IN",
              "name": "India",
              "dial_code": "+91"
            }
          ],
          "default_country": {
            "code": "IN",
            "name": "India",
            "dial_code": "+91"
          }
        },
        "description": "Phone login controls (allowed countries + default country)",
        "options": {
          "allowed_countries": [
            {
              "code": "CA",
              "name": "Canada",
              "dial_code": "+1"
            },
            {
              "code": "IN",
              "name": "India",
              "dial_code": "+91"
            },
            {
              "code": "TR",
              "name": "Turkey",
              "dial_code": "+90"
            }
          ],
          "default_country": [
            {
              "code": "CA",
              "name": "Canada",
              "dial_code": "+1"
            },
            {
              "code": "IN",
              "name": "India",
              "dial_code": "+91"
            },
            {
              "code": "TR",
              "name": "Turkey",
              "dial_code": "+90"
            }
          ]
        }
      },
      "domestic_country": {
        "settings": {
          "code": "CA",
          "name": "Canada",
          "dial_code": "+1"
        },
        "description": "Domestic country configuration for tenant (default: Canada)",
        "options": {
          "allowed_countries": [
            {
              "code": "CA",
              "name": "Canada",
              "dial_code": "+1"
            },
            {
              "code": "IN",
              "name": "India",
              "dial_code": "+91"
            },
            {
              "code": "TR",
              "name": "Turkey",
              "dial_code": "+90"
            }
          ]
        }
      }
    },
    "delivery": {
      "delivery_external_reference": {
        "settings": {
          "unique_per_customer": true
        },
        "description": "Controls customer-scoped external reference uniqueness for root delivery orders."
      },
      "assignment_change_settings": {
        "settings": {
          "enabled": true,
          "allow_unassign_after_pickup": false,
          "driver_can_self_unassign": false,
          "driver_can_self_unassign_after_pickup": false,
          "default_driver_id": "driver-uuid-123",
          "default_driver": {
            "full_name": "John Driver",
            "phone": "+1234567890",
            "avatar_url": "https://example.com/avatar.jpg"
          }
        },
        "description": null
      },
      "transfer_manifest": {
        "settings": {
          "enabled": true,
          "receiving_mode": "scan_each_item",
          "allow_driver_select_processing_point": false,
          "allow_driver_create_manifest_from_form": true,
          "allow_driver_scan_institution_qr": false,
          "allow_driver_scan_item_barcode": false,
          "allow_warehouse_staging_without_assigned_driver": false,
          "default_delivery_service_id": null,
          "default_delivery_service": null
        },
        "description": "Controls G2G transfer manifest pickup, receiving, processing, and default delivery service behavior for institution branch shipments."
      }
    },
    "warehouse": {
      "package": {
        "settings": {
          "handoff_required": false,
          "auto_execute_delivery_order_on_receive": false
        },
        "description": "Warehouse package handling settings. auto_execute_delivery_order_on_receive controls whether a related draft delivery order is automatically moved to pending after its warehouse item is fully received."
      }
    }
  },
  "currencies": {
    "system": {
      "currency_code": "USD",
      "currency_symbol": "$",
      "currency_name": "US Dollar"
    },
    "tenant": {
      "currency_code": "IQD",
      "currency_symbol": "د.ع",
      "currency_name": "Iraqi Dinar",
      "exchange_rate_to_system": 0.00076
    }
  }
}
```

### Response 200 Success - Return request settings



```json
{
  "success": true,
  "message": "tenant_settings_retrieved_successfully",
  "tenantSettings": {
    "delivery": {
      "return_requests": {
        "settings": {
          "auto_approval": true,
          "destination_mode": "pickup_location",
          "specific_warehouse_id": null,
          "destination_mode_options": [
            "pickup_location",
            "pickup_zone_warehouse",
            "specific_warehouse"
          ],
          "default_delivery_service_id": "service-uuid",
          "default_delivery_service": {
            "id": "service-uuid",
            "name": "Standard Delivery"
          }
        },
        "description": "Return requests configuration (tenant-scoped)"
      }
    }
  }
}
```

### Response 200 Success - Exception type settings



```json
{
  "success": true,
  "message": "tenant_settings_retrieved_successfully",
  "tenantSettings": {
    "delivery": {
      "exception_types": {
        "settings": {
          "actions": [
            {
              "key": "retry",
              "label": "Retry",
              "description": "Try to deliver the parcel again"
            },
            {
              "key": "return_to_sender",
              "label": "Return to sender",
              "description": "Send the parcel back to its original sender"
            },
            {
              "key": "cancel",
              "label": "Cancel order",
              "description": "Cancel this delivery order"
            }
          ],
          "types": [
            {
              "code": "customer_refused",
              "label": "Customer refused",
              "allowed_statuses": [
                "pending",
                "assigned",
                "en_route_to_pickup",
                "in_transit"
              ],
              "action_keys": [
                "retry",
                "return_to_sender",
                "cancel"
              ]
            }
          ]
        },
        "description": "Delivery exception types"
      }
    }
  }
}
```

### Response 200 Success - Transfer manifest branch-only destination setting



```json
{
  "success": true,
  "message": "tenant_settings_retrieved_successfully",
  "tenantSettings": {
    "delivery": {
      "transfer_manifest": {
        "settings": {
          "branch_only_destinations": true
        }
      }
    }
  }
}
```

## PUT /tenants/:tenantId/settings/:settingKey — Update Tenant Setting

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#put-update-tenant-setting)

Update a specific tenant setting. If `settingKey=unit_size`, send `settingValue.missing_unit_size_fallback=service_min`; allowed values are `service_min`, `service_half`, `service_max`, and `error`. The update preserves unit calculation fields and the response returns the canonical localized option labels.

If settingKey='delivery_external_reference', send `{ "settingValue": { "unique_per_customer": true } }`. The field is required and must be boolean. `true` blocks a duplicate non-empty `external_reference_id` for the same customer and tenant at the root delivery-order level; `false` allows duplicates. The system default is `true`, and staged child legs may reuse their root order's reference. Unknown fields return HTTP 400 `validation_error`.

If settingKey='timezone', send `{ "settingValue": { "timezone": "Asia/Baghdad" } }`. The value must be a valid IANA timezone. Route sessions are created explicitly; timezone changes do not create or reschedule them. Invalid values return HTTP 400 with `details[].message = must_be_valid_iana_timezone`.

If settingKey='currency' and tenant currency_code differs from system currency_code, you must provide a positive 'exchange_rate_to_system'. If codes match, exchange_rate_to_system is normalized to 1.

If settingKey='phone_login':
- `settingValue.allowed_countries` can be an array of `{ code, name, dial_code }` OR `null`.
- When array is provided, it must be a subset of system-level `phone_login.allowed_countries`.
- `settingValue.default_country` must be null or one of the allowed countries.
- When `allowed_countries` is null, tenant falls back to system phone_login setting.

If settingKey='package', `settingValue.handoff_required` and `settingValue.auto_execute_delivery_order_on_receive` must be booleans when provided. `auto_execute_delivery_order_on_receive=false` is the default and keeps warehouse receiving behavior unchanged. When true, fully received warehouse items with related draft delivery orders are auto-executed to pending by backend receive logic.

If `settingKey='assignment_change_settings'`, `settingValue.default_driver_id` accepts a tenant-owned active and available driver UUID or `null` to clear it. The update is merged with the existing setting, so saving the driver does not erase sibling assignment controls. Missing, deleted, inactive, unavailable, and cross-tenant drivers are rejected with localized driver errors.

If settingKey='transfer_manifest', `settingValue.driver_scan_mode` accepts only `continuous` or `manual`; other values return HTTP 400 `validation_error` with `details[].message = must_be_continuous_or_manual`. `settingValue.allow_warehouse_staging_without_assigned_driver` is an optional boolean and defaults to `false`; non-boolean values return HTTP 400 `validation_error`. Send `settingValue.exception_reasons` as an array of canonical reason strings. To select the fee basis, send `settingValue.delivery_fee_origin` as `processing_point` or `source_location`; for example `{ "settingValue": { "delivery_fee_origin": "source_location" } }`. `source_location` prices promoted orders from the manifest source address while their saved pickup remains the processing point. Any other value returns HTTP 400 with `details[].message = must_be_processing_point_or_source_location`. The GET settings response exposes exception values as `settings.reasons`, an array of `{ value, label, description }` objects. `settingValue` may also include optional booleans `allow_driver_create_manifest_from_form`, `allow_driver_scan_institution_qr`, and `allow_driver_scan_item_barcode`. When provided, these fields must be booleans or the API returns HTTP 400 with `validation_error` and `details[].message = must_be_boolean`. If one or more driver workflow fields are submitted, at least one must be true or the API returns HTTP 400 with `details[].message = at_least_one_driver_manifest_entry_mode_required`. Omit a key to preserve/fallback through the stored setting object used by the frontend contract.

If `settingKey=delivery_order`, send `{ "settingValue": { "edit": { "reason_required": true } } }`. `reason_required` is mandatory and must be boolean. The backend deep-merges the `edit` section with existing effective delivery-order settings so future sibling sections are preserved. Unknown sections or edit fields return HTTP 400 `validation_error`.

### Frontend integration contract

save the toggle through this endpoint and use the returned tenant setting as the persisted value.

For `settingKey=customer_wallet_minimum_balance` or `settingKey=wallet_minimum_balance`, send `{ "settingValue": { "minimum_balance": -1000000 } }`. The customer key controls customer wallet balance checks; the existing key is Drivers Minimum Balance only. Both require a finite JSON number and reject unknown fields. Negative values allow debt down to the configured limit; zero allows no debt. Frontend integration: show both fields under Wallet Settings and save each using its own key.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/tenants/:tenantId/settings/:settingKey

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes | ID of the tenant |  |
| settingKey | path string | Yes | Key of the setting to update | delivery_external_reference |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| settingValue | object |  |  |  |
| settingValue.unique_per_customer | boolean |  |  |  |



### Request body (raw)

```json
{
  "settingValue": {
    "unique_per_customer": false
  }
}
```

### Response 200 Exception types updated



```json
{
  "success": true,
  "message": "Tenant setting updated successfully",
  "tenantSetting": {
    "tenant_id": "tenant-uuid",
    "setting_key": "exception_types",
    "setting_type": "delivery",
    "setting_value": {
      "types": [
        {
          "code": "customer_refused",
          "label": "Customer refused",
          "allowed_statuses": [
            "pending",
            "assigned",
            "en_route_to_pickup",
            "in_transit"
          ],
          "action_keys": [
            "retry",
            "return_to_sender",
            "cancel"
          ]
        }
      ]
    }
  }
}
```

### Response 400 Invalid exception action configuration



```json
{
  "success": false,
  "code": "invalid_exception_action_configuration",
  "field": "settingValue.types",
  "message": "The exception action configuration is invalid"
}
```

### Response 200 External reference uniqueness disabled



```json
{
  "success": true,
  "message": "Tenant setting updated successfully",
  "tenantSetting": {
    "tenant_id": "tenant-uuid",
    "setting_key": "delivery_external_reference",
    "setting_type": "delivery",
    "setting_value": {
      "unique_per_customer": false
    }
  }
}
```

### Response 200 Minimum wallet balance updated



```json
{
  "success": true,
  "message": "Tenant setting updated successfully",
  "tenantSetting": {
    "tenant_id": "tenant-uuid",
    "setting_key": "wallet_minimum_balance",
    "setting_type": "wallet",
    "setting_value": {
      "minimum_balance": -1000000
    }
  }
}
```

### Response 400 Invalid minimum wallet balance



```json
{
  "success": false,
  "code": "validation_error",
  "message": "Validation error",
  "details": [
    {
      "field": "settingValue.minimum_balance",
      "message": "Must be a number"
    }
  ]
}
```

### Response 400 Invalid timezone



```json
{
  "success": false,
  "message": "Validation error",
  "details": [
    {
      "field": "settingValue.timezone",
      "message": "must_be_valid_iana_timezone"
    }
  ]
}
```

### Response 200 Transfer manifest branch-only destinations updated



```json
{
  "success": true,
  "message": "tenant_setting_updated_successfully",
  "tenantSetting": {
    "tenant_id": "tenant-uuid",
    "setting_key": "transfer_manifest",
    "setting_type": "delivery",
    "setting_value": {
      "branch_only_destinations": true
    }
  }
}
```

### Response 400 Invalid transfer manifest branch-only destination value



```json
{
  "success": false,
  "message": "Validation error",
  "details": [
    {
      "field": "settingValue.branch_only_destinations",
      "message": "Must be true or false"
    }
  ]
}
```

## POST /tenants/labels/retry — Retry/Regenerate Label

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#post-retry-regenerate-label)

Retry or regenerate a label for a delivery, inventory item, or delivery package item. This endpoint allows tenant administrators to trigger label regeneration regardless of the previous status.

- User must be either a system administrator or a tenant administrator with 'manage:tenant' permission

### At least one of the following IDs must be provided
- `delivery_id` (optional): UUID of the delivery to regenerate label for
- `item_id` (optional): UUID of the inventory item to regenerate label for
- `delivery_package_item_id` (optional): UUID of the delivery package item to regenerate label for

### Validations
- At least one ID must be provided
- The entity must belong to the tenant (for tenant admins)
- System admins can regenerate labels for any tenant

### Label Generation Failed (500 Internal Server Error)

### Concurrent delivery-label regeneration

Manual retry still deliberately replaces the observed successful label version. If generation is already active, it is not superseded: HTTP 503 returns `code=label_generation_in_progress` with a localized retry message. Inventory-item and package-item behavior is unchanged.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenants/labels/retry?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_id | string |  |  |  |
| item_id | null |  |  |  |
| delivery_package_item_id | null |  |  |  |



### Request body (raw)

```json
{
  "delivery_id": "delivery-uuid",
  "item_id": null,
  "delivery_package_item_id": null
}
```

### Request example 1

```json
{
  "delivery_id": "550e8400-e29b-41d4-a716-446655440000"
}
```

### Request example 2

```json
{
  "item_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
```

### Request example 3

```json
{
  "delivery_package_item_id": "98ec3d92-82f8-44ff-a0bd-9f76bad284d7"
}
```

### Response 200 Success - Label Regenerated



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Label regenerated successfully",
  "url": "https://example.supabase.co/storage/v1/object/public/shipment-label-pdfs/delivery/shipment-labels/550e8400-e29b-41d4-a716-446655440000-2.pdf"
}
```

### Response 400 Error - Missing ID



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "At least one of delivery_id, item_id, or delivery_package_item_id is required"
}
```

### Response 404 Error - Entity Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery not found or does not belong to your tenant"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Label Generation Failed



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "pdf_render_failed",
  "message": "Label generation failed"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "Label regenerated successfully",
  "url": "https://example.supabase.co/storage/v1/object/public/documents/shipment-labels/uuid.pdf"
}
```

### Response 404 Entity Not Found (404 Not Found)



```json
{
  "success": false,
  "message": "Item not found or does not belong to your tenant"
}
```

### Response 404 Entity Not Found (404 Not Found)



```json
{
  "success": false,
  "message": "Package item not found or does not belong to your tenant"
}
```

### Response 500 Label Generation Failed (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Label generation failed"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /tenants/me/subscription — Get My Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#get-get-my-subscription)

Returns the authenticated tenant subscription. Response content type is always `application/json`.

### Response
- `success`: boolean
- `subscription_enabled`: boolean - whether the tenant subscription model is enabled globally (system setting)
- `data`: subscription object

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/me/subscription

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Response 200 200 OK

OK

```json
{
  "success": true,
  "subscription_enabled": true,
  "data": {}
}
```

## GET /tenants/me/subscription/history — Get My Subscription History

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#get-get-my-subscription-history)

Returns the authenticated tenant subscription history. Response content type is always `application/json`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/me/subscription/history

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## PUT /tenants/:tenantId/settings/transfer_manifest — Update G2G Available Services Setting

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/tenants#put-update-g2g-available-services-setting)

Tenant-admin contract for Available services for G2G. `available_delivery_service_ids` must be an array of active delivery-service UUIDs owned by the tenant. Missing or `[]` means no additional G2G limitation. When the list is non-empty, `default_delivery_service_id` must be included. The backend deduplicates IDs and rejects inactive, missing, or cross-tenant services.

### GET contract

`GET /tenants/:tenantId/settings?keys=transfer_manifest` returns normalized `settings.available_delivery_service_ids`, `settings.available_delivery_services` as `[{ "id": string, "name": string, "code": string, "image": string | null, "delivery_type": { "id": string, "name": string, "code": string }, "active": boolean }]`, and the existing `default_delivery_service`.

### Frontend integration contract

populate the settings multi-select from `GET /delivery/services?active=true&include_stats=false` without G2G context. Save selected IDs here. Operational G2G pickers use `GET /delivery/services?context=g2g&active=true&include_stats=false`. Handle HTTP 400 codes `g2g_available_delivery_services_invalid` and `g2g_default_delivery_service_must_be_available`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/tenants/:tenantId/settings/transfer_manifest

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes |  |  |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| settingValue | object |  |  |  |
| settingValue.default_delivery_service_id | string |  |  |  |
| settingValue.available_delivery_service_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "settingValue": {
    "default_delivery_service_id": "{{delivery_service_id}}",
    "available_delivery_service_ids": [
      "{{delivery_service_id}}"
    ]
  }
}
```

### Response 200 G2G service setting updated



```json
{
  "success": true,
  "message": "Tenant setting updated successfully",
  "tenantSetting": {
    "setting_key": "transfer_manifest",
    "setting_value": {
      "default_delivery_service_id": "{{delivery_service_id}}",
      "available_delivery_service_ids": [
        "{{delivery_service_id}}"
      ]
    }
  }
}
```

### Response 400 Default service is outside G2G allow-list



```json
{
  "success": false,
  "code": "g2g_default_delivery_service_must_be_available",
  "message": "The default G2G service must be included in Available services for G2G"
}
```

## POST /tenant-subscriptions — Create Recurring Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-operations#post-create-recurring-subscription)

Creates a new recurring subscription (MONTHLY/ANNUAL). Creates Stripe Customer if needed, creates subscription with inline pricing, and finalizes the invoice immediately.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenant-subscriptions

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | string |  |  |  |
| type | string |  |  |  |
| amount | number |  |  |  |
| note_public | string |  |  |  |



### Request body (raw)

```json
{
  "tenant_id": "<TENANT_UUID>",
  "type": "MONTHLY",
  "amount": 49.99,
  "note_public": "Standard Plan"
}
```

## POST /tenant-subscriptions/unlimited — Create Unlimited Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-operations#post-create-unlimited-subscription)

Creates a one-time 'UNLIMITED' subscription. AUTOMATICALLY CANCELS any existing recurring subscription for the tenant. Generates a one-off invoice. A tenant notification email (`tenant_subscription_unlimited_invoice_created`) is sent after Stripe invoice finalization webhook.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenant-subscriptions/unlimited

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | string |  |  |  |
| amount | number |  |  |  |
| note_public | string |  |  |  |



### Request body (raw)

```json
{
  "tenant_id": "<TENANT_UUID>",
  "amount": 500,
  "note_public": "Lifetime Access"
}
```

## GET /tenant-subscriptions — List Subscriptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-operations#get-list-subscriptions)

Lists all tenant subscriptions with pagination and filtering.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenant-subscriptions?page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  |  | 1 |
| limit | query string |  |  | 10 |
| search | query string | No |  |  |
| status | query string | No |  | ACTIVE |



## GET /tenant-subscriptions/:tenantId — Get Subscription Details

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-operations#get-get-subscription-details)

Gets detailed subscription info and history for a specific tenant. Response content type is always `application/json`. If there is an unpaid invoice, response includes `data.subscription.pendingInvoice` for both recurring (MONTHLY/ANNUAL) and UNLIMITED plans.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenant-subscriptions/:tenantId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenantId | path string | Yes |  | <TENANT_UUID> |



## POST /tenant-subscriptions/:id/disable — Disable Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-operations#post-disable-subscription)

Manually disables a subscription and sends a tenant admin notification email (`tenant_subscription_disabled`).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenant-subscriptions/:id/disable

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | <SUBSCRIPTION_UUID> |



## POST /tenant-subscriptions/:id/reactivate — Reactivate Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-operations#post-reactivate-subscription)

Reactivates a manually disabled subscription and sends a tenant admin notification email (`tenant_subscription_activated`).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenant-subscriptions/:id/reactivate

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | <SUBSCRIPTION_UUID> |



## PATCH /tenant-subscriptions/:id — Update Subscription (Price/Plan)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-operations#patch-update-subscription-price-plan)

Updates subscription details.

### Parameters
- `price_amount`: New price (optional)
- `type`: 'MONTHLY' or 'ANNUAL' (optional)
- `effective_from`: Controls how the change is applied to Stripe (optional, defaults to `immediate_no_prorate`).
  - `immediate_prorated`: Updates immediately, Stripe calculates the difference and sends an invoice.
  - `immediate_no_prorate`: Updates immediately, ignores price difference (no invoice).
  - `end_of_period`: Updates at the end of the current billing cycle (Schedule). *Note: Currently simplified to immediate_no_prorate in code until full Schedule support is added.*

When plan/amount changes, a tenant admin notification email (`tenant_subscription_plan_changed`) is emitted.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/tenant-subscriptions/:id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | <SUBSCRIPTION_UUID> |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| price_amount | number |  |  |  |
| type | string |  |  |  |
| effective_from | string |  |  |  |
| note_public | string |  |  |  |
| note_internal | string |  |  |  |



### Request body (raw)

```json
{
  "price_amount": 150,
  "type": "ANNUAL",
  "effective_from": "immediate_prorated",
  "note_public": "Plan Upgrade",
  "note_internal": "Admin update"
}
```

## POST /tenant-subscription-sweep — Trigger Nightly Sweep (Manual)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-operations#post-trigger-nightly-sweep-manual)

Manually triggers the subscription status sweep.

### Notes
- If system setting `tenant_subscription.enabled=false`, the sweep is skipped (no recompute, no emails).
- When enabled, sweep emits lifecycle and reminder emails (`tenant_subscription_warning`, `tenant_subscription_suspended`, `tenant_subscription_resumed`, `tenant_subscription_activated`, `tenant_subscription_due_reminder`, `tenant_subscription_suspension_notice`).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/tenant-subscription-sweep

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## GET /tenants/me/subscription — Get My Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin-operations#get-get-my-subscription-2)

Returns the current subscription details for the authenticated tenant admin. Response includes `subscription_enabled` (global system toggle) and `data` (subscription object). If there is an unpaid invoice, response includes `data.pendingInvoice` for both recurring (MONTHLY/ANNUAL) and UNLIMITED plans.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/me/subscription

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## GET /tenants/me/subscription/history — Get My Subscription History

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin-operations#get-get-my-subscription-history-2)

Returns the subscription history (logs/invoices) for the authenticated tenant admin.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/tenants/me/subscription/history

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## POST /tenant-subscription-sweep — Run Tenant Subscription Sweep (Internal)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-subscription-sweep-internal#post-run-tenant-subscription-sweep-internal)

### Runs the tenant-subscription sweep

recomputes eligible tenant statuses and sends applicable lifecycle, due-reminder, and suspension notices. No request body fields are consumed. When the subscription model is disabled, the request succeeds with `skipped: true` and zero counters.

Internal backend/cron only. Send `apikey: {{supabase_secret_key}}` with the default managed Supabase secret key (`SUPABASE_SECRET_KEYS.default`). Never put this value in a frontend or mobile application. Missing, invalid, publishable, legacy anon, and legacy service-role bearer credentials return HTTP 401 before work is dispatched. `Authorization: Bearer ...` alone is not accepted. Frontend handoff: none; application clients must not call this internal endpoint.

Authentication: Internal backend or cron only. Send the managed Supabase secret in apikey. Application clients must not call this endpoint.

Request: POST https://staging-api.ondi.io/functions/v1/tenant-subscription-sweep

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| apikey | header string |  | Default managed Supabase secret key; backend only. | {{supabase_secret_key}} |



### Request body (raw)

```json
{}
```

### Response 200 200 Sweep completed

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "processed": 0,
  "warnings": 0,
  "suspensions": 0,
  "activated": 0,
  "resumed": 0,
  "due_reminders": 0,
  "suspension_notices": 0
}
```

### Response 200 200 Subscription model disabled

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "skipped": true,
  "processed": 0,
  "warnings": 0,
  "suspensions": 0,
  "activated": 0,
  "resumed": 0,
  "due_reminders": 0,
  "suspension_notices": 0
}
```

### Response 500 500 Tenant lookup failed

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Tenant lookup failed"
}
```

### Response 401 401 Missing or invalid internal secret

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "error": "Unauthorized"
}
```

## GET /users/me — Get Current User

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#get-get-current-user)

Retrieves the current authenticated user. Returns user with warehouse_staff, restaurant_staff (object or null), panel_type, tenant_info, role, notification preferences, market/restaurant portal context flags, and `login_country` derived from the phone dial code when the user has a phone number. An inactive application user remains inactive across sign-in and other Supabase Auth updates; authentication activity never reactivates an inactive account. Inactive users receive HTTP 403 until an authorized user explicitly changes their application status to active.

Pending invited accounts activate when their confirmed invitation is completed with a first password. Inactive accounts remain inactive.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/users/me?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code (Optional) | en |



### Response 200 Success



```json
{
  "success": true,
  "message": "Users retrieved successfully",
  "user": {
    "id": "user-uuid",
    "email": "user@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "status": "active",
    "notifications_enabled": true,
    "is_system_admin": false,
    "is_staff": true,
    "is_market_user": true,
    "market_id": "market-uuid",
    "is_restaurant_user": false,
    "restaurant_location_id": null,
    "login_country": {
      "code": "IN",
      "name": "India",
      "dial_code": "+91"
    },
    "role": {
      "id": "role-uuid",
      "name": "restaurant_staff",
      "scope": "tenant"
    },
    "panel_type": "dispatcher",
    "warehouse_staff": [],
    "restaurant_staff": {
      "id": "staff-uuid",
      "location_id": "location-uuid",
      "staff_role": "cashier",
      "location": {
        "id": "location-uuid",
        "name": {},
        "code": 1000,
        "tenant_id": "tenant-uuid",
        "active": true
      }
    }
  }
}
```

### Response 403 Inactive Account



```json
{
  "success": false,
  "message": "User account is inactive"
}
```

## POST /users/me/context — Update My Context

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#post-update-my-context)

Stores the authenticated user's market or restaurant context after portal login.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/users/me/context?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code (Optional) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| portal | string |  | `market` or `restaurant` |  |
| market_id | string |  | required when `portal=market` |  |
| restaurant_location_id |  |  | required when `portal=restaurant` |  |



### Request body (raw)

```json
{
  "portal": "market",
  "market_id": "{{market_id}}"
}
```

### Response 200 Success - Market



```json
{
  "success": true,
  "message": "User Updated Successfully",
  "data": {
    "id": "user-uuid",
    "is_market_user": true,
    "market_id": "market-uuid",
    "is_restaurant_user": false,
    "restaurant_location_id": null
  }
}
```

### Response 200 Success - Restaurant



```json
{
  "success": true,
  "message": "User Updated Successfully",
  "data": {
    "id": "user-uuid",
    "is_market_user": true,
    "market_id": "market-uuid",
    "is_restaurant_user": true,
    "restaurant_location_id": "restaurant-location-uuid"
  }
}
```

### Response 404 Error - Market Not Found



```json
{
  "success": false,
  "message": "Market not found",
  "data": null
}
```

## GET /users — Get List Users

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#get-get-list-users)

Lists users with pagination, search, sorting, advanced column filters, and tenant KPI stats. Existing top-level filters continue to work.

- When both top-level `role_id` and `column_filters.role_id` are provided, both filters must match.

### Frontend integration

send the selected role UUID either as top-level `role_id` or as `column_filters.role_id`. Continue using `column_filters.role` only for role names. No navigation behavior changes. Invalid filter types return HTTP 400.

### Response

includes `stats` (`totalUsers`, `activeUsers`, `pendingUsers`, `recentlyJoined`) plus the existing `statusCounts` block.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/users?language=en&page=1&limit=10&status=active&search=&tenant_id=&role_id=&role=driver,manager&is_business=&startDate=&endDate=&show_all=true&include_customer=true&exclude_customers=&is_market_user=&market_id=&is_restaurant_user=&restaurant_location_id=&sort_by=&sort_order=&column_filters=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |
| page | query string | No | Page number for pagination (Optional, default: 1) | 1 |
| limit | query string | No | Number of records per page (Optional, default: 10) | 10 |
| status | query string | No | Filter users by status (Optional, values: active, inactive, pending, blocked) | active |
| search | query string | No | Search term to filter users by email, first name, or last name (Optional) |  |
| tenant_id | query string | No | Filter users by tenant ID (Optional) |  |
| role_id | query string | No | Filter users by role ID (Optional) exact role-ID filter |  |
| startDate | query string | No | Filter users created after this date (ISO format, optional) |  |
| endDate | query string | No | Filter users created before this date (ISO format, optional) |  |
| role | query string | No | Filter users by exact role name. Supports comma-separated values for multiple roles, e.g. `driver,manager` (Optional) exact role-name filter. Supports comma-separated values such as `driver,manager` | driver,manager |
| is_business | query string | No | Filter by business type: 'true' for business users, 'false' for individual users (Optional) |  |
| show_all | query string | No | Show all users regardless of filters (Optional, default: true) | true |
| include_customer | query string | No | Include users without any assigned roles (customers) in the results (Optional, default: true). Set to 'false' to exclude users without roles. | true |
| exclude_customers | query string |  | Alias for excluding customer rows. Set to 'true' to force `include_customer=false`. |  |
| is_market_user | query string | No | Filter users by market-context flag (`true` or `false`) (Optional) |  |
| market_id | query string | No | Filter users by market ID (Optional) |  |
| is_restaurant_user | query string | No | Filter users by restaurant-context flag (`true` or `false`) (Optional) |  |
| restaurant_location_id | query string | No | Filter users by restaurant location ID (Optional) |  |
| sort_by | query string |  | Sort by one of: full_name, email, phone, status, role_name, user_type, created_at, last_login_at |  |
| sort_order | query string |  | Sort order: asc or desc. Defaults to desc when sort_by is provided. `asc` or `desc` (defaults to `desc` when `sort_by` is provided) |  |
| column_filters | query string |  | JSON string with column filters. Supported keys: full_name, email, phone, status, role, role_id, role_name, role_scope, user_type, is_market_user, is_restaurant_user, created_at, last_login_at JSON string with keys `full_name`, `email`, `phone`, `status`, `role`, `role_id`, `role_name`, `role_scope`, `user_type`, `is_market_user`, `is_restaurant_user`, `created_at`, `last_login_at` | {"role_id":"role-uuid"} |
| column_filters.role_id | query string |  | exact role-ID filter, for example `{"role_id":"e103e9a4-8c78-4c95-a27a-893d278ac590"}` |  |
| column_filters.role | query string |  | exact role-name filter with comma-separated values such as `driver,manager` |  |



### Response 200 Success Response



```json
{
  "success": true,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": "user-uuid",
      "email": "user@example.com",
      "first_name": "John",
      "last_name": "Doe",
      "full_name": "John Doe",
      "phone": "+1234567890",
      "avatar_url": "https://example.com/avatar.jpg",
      "status": "active",
      "is_market_user": true,
      "market_id": "market-uuid",
      "is_restaurant_user": false,
      "restaurant_location_id": null,
      "user_type": "business",
      "roles": {
        "id": "role-uuid",
        "name": "admin",
        "scope": "tenant"
      },
      "tenant": {
        "id": "tenant-uuid",
        "name": "Example Tenant"
      },
      "created_at": "2023-01-01T00:00:00Z",
      "last_login_at": "2023-01-15T10:30:00Z"
    }
  ],
  "stats": {
    "totalUsers": 42,
    "activeUsers": 30,
    "pendingUsers": 8,
    "recentlyJoined": [
      {
        "id": "user-uuid",
        "full_name": "John Doe",
        "avatar_url": "https://example.com/avatar.jpg",
        "created_at": "2025-09-20T10:42:09.258391+00:00"
      }
    ]
  },
  "page": 1,
  "limit": 10,
  "total": 42,
  "statusCounts": {
    "active": 30,
    "inactive": 8,
    "pending": 4,
    "total": 42
  }
}
```

### Response 401 Error - Unauthorized



```json
{
  "error": "Unauthorized",
  "message": "Authentication required",
  "status": 401
}
```

### Response 403 Error - Forbidden



```json
{
  "error": "Forbidden",
  "message": "Insufficient permissions",
  "status": 403
}
```

### Response 500 Error - Server Error



```json
{
  "success": false,
  "message": "Error retrieving users"
}
```

## GET /users/:id — Get User by ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#get-get-user-by-id)

Retrieves a specific user by ID after target-scope authorization.

### Authentication and access
- Self-detail remains available.
- manage:user_access:system may read any target.
- manage:user_access:tenant may read only a target with an active membership in the caller's exact tenant and no system-scoped role.
- Missing caller permission returns 403 rbac_forbidden before target lookup.
- Missing, inactive, foreign-tenant, or system-scoped targets return scope-safe 404 user_not_found without contact or role data.

Both success modes include user.can_reset_password. It is true only when the current JWT caller may call PUT /users/:id/reset-password for that returned user. Full-mode stats and enrichment run only after authorization succeeds.

### Frontend integration

open the detail page only on 200. Treat 403 as denied caller access and 404 as unavailable/out-of-scope; do not render cached target data. Show Reset Password only when user.can_reset_password is true.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/users/:id?language=en&response_type=full

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the user to retrieve (Required) | user-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |
| response_type | query string |  | Response detail level: 'full' (default) returns all data including roles, permissions, teams, stats; 'basic' returns only essential profile info (id, name, email, phone, status, avatar, user_type, last_login, created_at) =full (default): roles, permissions, teams, tenant, driver/warehouse context, and stats. =basic: essential profile fields. | full |



### Response 200 Success - Full (default)



```json
{
  "success": true,
  "message": "User retrieved successfully",
  "user": {
    "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
    "avatar_url": "https://example.com/avatars/a1b2c3d4.png",
    "email": "user@example.com",
    "first_name": "John",
    "last_name": "Doe",
    "full_name": "John Doe",
    "phone": "+15551234567",
    "status": "active",
    "user_type": "member",
    "role": {
      "id": "role-uuid-789",
      "name": "tenant_member",
      "scope": "tenant"
    },
    "tenant_info": {
      "id": "tenant-uuid-abc",
      "name": "Example Tenant Inc.",
      "slug": "example-tenant-inc",
      "status": "active",
      "metadata": {
        "industry": "tech"
      },
      "contact_info": {
        "address": "123 Main St"
      }
    },
    "panel_type": "customer",
    "permissions": [
      "view_deliveries",
      "create_deliveries"
    ],
    "teams": [
      {
        "id": "team-uuid",
        "name": "Team A"
      }
    ],
    "all_teams": false,
    "created_at": "2023-01-15T10:30:00Z",
    "updated_at": "2023-10-20T14:45:00Z",
    "last_login_at": "2023-11-01T09:00:00Z",
    "requires_driver_setup": false,
    "can_reset_password": true
  },
  "stats": {
    "wallet_balance": 150.5,
    "total_orders": 42,
    "pending_payouts": 2,
    "active_api_keys": 1
  }
}
```

### Response 200 Success - Basic



```json
{
  "success": true,
  "message": "User retrieved successfully",
  "user": {
    "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
    "email": "user@example.com",
    "phone": "+15551234567",
    "full_name": "John Doe",
    "first_name": "John",
    "last_name": "Doe",
    "status": "active",
    "avatar_url": "https://example.com/avatars/a1b2c3d4.png",
    "user_type": "member",
    "last_login_at": "2023-11-01T09:00:00Z",
    "created_at": "2023-01-15T10:30:00Z",
    "can_reset_password": true
  }
}
```

### Response 401 Error - Unauthorized



```json
{
  "error": "Unauthorized",
  "message": "Authentication token is missing or invalid",
  "status": 401
}
```

### Response 403 Error - Forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 404 Error - Not Found



```json
{
  "success": false,
  "code": "user_not_found",
  "message": "User not found"
}
```

### Response 500 Error - Server Error



```json
{
  "success": false,
  "message": "Internal server error",
  "error": {
    "message": "Details of the internal error if available..."
  }
}
```

## GET /users/:userId/accounting — Get User Accounting

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#get-get-user-accounting)

Returns the accounting summary used by the user-detail Accounting tab. This endpoint is driver-linked: the user must have a driver record in the tenant.

### Includes
- Balance summary for selected period (creditor/debtor)
- Cash collected (COD + order fee)
- Earnings (cash vs online/credit)
- Order costs (cash/paid-by-receiver/card/wallet)
- Company earnings
- Paid withdrawals
- From support buckets

- Self OR tenant permissions (finance/operations/tenant view).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/users/:userId/accounting?tenant_id=&from_date=&to_date=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | User UUID (required) | {{user_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | Tenant ID (optional, defaults to auth context) |  |
| from_date | query string | No | From date (ISO8601, optional) |  |
| to_date | query string | No | To date (ISO8601, optional) |  |
| language | query string | No | Language code (optional) | en |



### Response 200 Success



```json
{
  "success": true,
  "message": "user_accounting_retrieved_successfully",
  "data": {
    "period": {
      "from_date": null,
      "to_date": null
    },
    "currency": "IQD",
    "balance_summary": {
      "amount": 0,
      "type": "creditor",
      "description": "Account is settled."
    },
    "cash_collected": {
      "total": 0,
      "breakdown": {
        "cod": 0,
        "order_fee": 0
      }
    },
    "earnings": {
      "total": 0,
      "breakdown": {
        "cash": 0,
        "online_credit": 0
      }
    },
    "order_costs": {
      "total": 0,
      "breakdown": {
        "cash": 0,
        "paid_by_receiver": 0,
        "card": 0,
        "wallet": 0
      }
    },
    "company_earnings": 0,
    "paid_withdrawals": 0,
    "from_support": {
      "cod_type_creditor": 0,
      "cod_type_debtor": 0,
      "support_type_creditor": 0,
      "support_type_debtor": 0
    }
  }
}
```

## POST /users — Invite User

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#post-invite-user)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

user_access:system, or manage:user_access:tenant for the caller’s current tenant. Broad manage:system, manage:tenant and manage:users:tenant permissions do not substitute. Tenant callers may grant only tenant roles whose complete permission set is a subset of their effective permissions. They cannot change system users or operate across tenants. Role scope must match role_type. System assignments have tenant_id=null. Tenant assignments require tenant_id.

Invite a user by email. Required: email, first_name, last_name. Optional: avatar_url, role_id, role_type, tenant_id, user_type, business_details. For add/invite, omitted `role_id`, `null`, and `""` all mean no role assignment. Non-empty values must be valid role UUIDs and still pass role scope and permission checks. Invalid role values return HTTP 400 with a localized `message` naming `role_id` and field-level `details`; frontend forms can display that message directly. Caller authorization and selected-role validation occur before the invitation email is sent.

role_type defaults to system for manage:user_access:system callers, otherwise tenant. Tenant callers default tenant_id to their own tenant. System callers assigning a tenant role must send role_type=tenant and a valid tenant_id. Authorization and selected-role validation precede Auth work. Existing accounts return 409. Provisioning failures return 500 rbac_provisioning_incomplete; records are retained and disabled, never deleted. Success remains 201 {success:true,message,data}, with data containing the Auth user.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. For add/invite only, handle 500 rbac_provisioning_incomplete as an incomplete account kept disabled for administrator recovery. No navigation action is added.

### Invitation completion

a pending invited account becomes active when its password changes after its email was confirmed. Supabase assigns a temporary password during invite acceptance, so activation must not depend on the previous password being empty. Accepting the link alone does not activate the account. Inactive accounts remain inactive; ordinary sign-ins and manually created pending accounts retain their status. A confirmed pending invitee can finish activation by setting or resetting their password. Frontend: preserve the Auth session on /update-password so pending invitees can finish password setup.

### Contact phone validation

`business_details.contact_person.phone` remains optional (omitted, null or blank is accepted). A supplied value must contain at least two main-number digits and use phone formatting. Local and international formats, Unicode decimal digits, spaces, parentheses, dots, slashes, hyphens and one leading + are accepted. Optional extensions use ext, ext., extension, x, # or ;ext=. Extensions do not count toward the main-number length. No country-specific length or country code is enforced, and the submitted value is preserved. This checks syntax, not whether the number exists or is reachable. Invalid values return HTTP 400 with a plain localized contact-phone message, without an internal field-path prefix. The field identifier remains in structured `details` for clients; display the top-level `message` in the portal toast. Validation runs before invitation email delivery or account writes.

### Invitation delivery errors

Auth failures keep HTTP 400 and return {success:false,code,message}. Codes are `invitation_email_rejected`, `invitation_email_not_authorized`, `invitation_rate_limited`, `user_already_exists`, or `invitation_failed`. Recipient errors also include `details:[{field:"email",message}]`. Messages explain the failed step and the next action; raw provider errors are never returned. Unknown Auth failures cannot reliably identify the SMTP cause, so the fallback asks the user to check the address and supported test address, then asks an administrator to check delivery settings. Existing application-account lookup conflicts remain HTTP 409. For staging invitation tests use a controlled real mailbox or the email provider's supported test recipient, such as `delivered+unique-label@resend.dev`; example.com recipients are rejected by the current provider. Frontend forms can continue showing the localized `message`; no response navigation changes.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/users?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| email | string |  |  |  |
| first_name | string |  |  |  |
| last_name | string |  |  |  |
| avatar_url | string |  |  |  |
| role_id | string |  |  |  |



### Request body (raw)

```json
{
  "email": "new.user@example.com",
  "first_name": "John",
  "last_name": "Doe",
  "avatar_url": "https://example.com/avatar.png",
  "role_id": "role-uuid-optional"
}
```

### Response 201 Created



```json
{
  "success": true,
  "message": "invitation_sent_successfully",
  "data": {
    "user": {
      "id": "new-user-uuid",
      "email": "new.user@example.com"
    }
  }
}
```

### Response 409 Conflict - Already Exists



```json
{
  "success": false,
  "message": "user_already_exists",
  "data": null
}
```

### Response 400 Bad Request - Auth invite failed



```json
{
  "success": false,
  "code": "invitation_failed",
  "message": "The invitation could not be sent. Check the email address and try again. For testing, use an address supported by your email provider. If it still fails, ask an administrator to check email delivery settings."
}
```

### Response 500 Server Error - Config or lookup



```json
{
  "success": false,
  "message": "Internal Server Error",
  "data": null,
  "details": {
    "step": "invite_redirect_config",
    "reason": "ADMIN_WEB_URL secret is missing or empty; cannot build invite redirect URL"
  }
}
```

### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 403 rbac_invalid_scope



```json
{
  "success": false,
  "code": "rbac_invalid_scope",
  "message": "The role, permission, or tenant scope is not allowed."
}
```

### Response 500 rbac_provisioning_incomplete



```json
{
  "success": false,
  "code": "rbac_provisioning_incomplete",
  "message": "Account setup could not be completed. The account has been kept disabled for recovery."
}
```

## POST /users/add — Add User (auto-confirm)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#post-add-user-auto-confirm)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

user_access:system, or manage:user_access:tenant for the caller’s current tenant. Broad manage:system, manage:tenant and manage:users:tenant permissions do not substitute. Tenant callers may grant only tenant roles whose complete permission set is a subset of their effective permissions. They cannot change system users or operate across tenants. Role scope must match role_type. System assignments have tenant_id=null. Tenant assignments require tenant_id.

Create an auto-confirmed user. Required: password and at least one of email/phone. Optional: first_name, last_name, avatar_url, role_id, role_type, tenant_id, is_staff, user_type, business_details. For add/invite, omitted `role_id`, `null`, and `""` all mean no role assignment. Non-empty values must be valid role UUIDs and still pass role scope and permission checks. Invalid role values return HTTP 400 with a localized `message` naming `role_id` and field-level `details`; frontend forms can display that message directly. Only manage:user_access:system permits is_staff=true. Auth creation starts banned; successful membership/role provisioning is followed by unbanning.

role_type defaults to system for manage:user_access:system callers, otherwise tenant. Tenant callers default tenant_id to their own tenant. System callers assigning a tenant role must send role_type=tenant and a valid tenant_id. Authorization and selected-role validation precede Auth work. Existing accounts return 409. Provisioning failures return 500 rbac_provisioning_incomplete; records are retained and disabled, never deleted. Success remains 201 {success:true,message,data}, with data containing the Auth user.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. For add/invite only, handle 500 rbac_provisioning_incomplete as an incomplete account kept disabled for administrator recovery. No navigation action is added.

### Account status

optional `status` accepts `active`, `inactive`, or `pending`; defaults to `pending` when omitted (or null). After successful role/membership provisioning, the selected application status is persisted before authentication is unbanned. Email/phone remain auto-confirmed. Pending/inactive accounts cannot access application APIs until an administrator activates them. Frontend: Add User status selector defaults to Active and explicitly sends the selection. Existing API clients that omit status now create pending accounts.

### Contact phone validation

`business_details.contact_person.phone` remains optional (omitted, null or blank is accepted). A supplied value must contain at least two main-number digits and use phone formatting. Local and international formats, Unicode decimal digits, spaces, parentheses, dots, slashes, hyphens and one leading + are accepted. Optional extensions use ext, ext., extension, x, # or ;ext=. Extensions do not count toward the main-number length. No country-specific length or country code is enforced, and the submitted value is preserved. This checks syntax, not whether the number exists or is reachable. Invalid values return HTTP 400 with a plain localized contact-phone message, without an internal field-path prefix. The field identifier remains in structured `details` for clients; display the top-level `message` in the portal toast. Validation runs before invitation email delivery or account writes.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/users/add?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| email | string |  |  |  |
| phone | string |  |  |  |
| password | string |  |  |  |
| first_name | string |  |  |  |
| last_name | string |  |  |  |
| avatar_url | string |  |  |  |
| is_staff | boolean |  |  |  |
| role_id | string |  |  |  |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "email": "new.user@example.com",
  "phone": "+1234567890",
  "password": "SecurePassword123!",
  "first_name": "New",
  "last_name": "User",
  "avatar_url": "https://example.com/avatar.png",
  "is_staff": false,
  "role_id": "role-uuid-optional",
  "status": "active"
}
```

### Response 201 Created



```json
{
  "success": true,
  "message": "user_created_successfully",
  "data": {
    "user": {
      "id": "new-user-uuid",
      "email": "new.user@example.com",
      "phone": "+1234567890"
    }
  }
}
```

### Response 400 Bad Request - Contact Missing



```json
{
  "success": false,
  "message": "email_or_phone_required"
}
```

### Response 409 Conflict - Already Exists



```json
{
  "success": false,
  "message": "user_already_exists"
}
```

### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 403 rbac_invalid_scope



```json
{
  "success": false,
  "code": "rbac_invalid_scope",
  "message": "The role, permission, or tenant scope is not allowed."
}
```

### Response 500 rbac_provisioning_incomplete



```json
{
  "success": false,
  "code": "rbac_provisioning_incomplete",
  "message": "Account setup could not be completed. The account has been kept disabled for recovery."
}
```

## POST /users/add-customer — Add Customer (auto-confirm phone)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#post-add-customer-auto-confirm-phone)

Creates a customer user with phone and name. Phone is auto-confirmed; email is optional. Password is generated automatically.

### Contact phone validation

`business_details.contact_person.phone` remains optional (omitted, null or blank is accepted). A supplied value must contain at least two main-number digits and use phone formatting. Local and international formats, Unicode decimal digits, spaces, parentheses, dots, slashes, hyphens and one leading + are accepted. Optional extensions use ext, ext., extension, x, # or ;ext=. Extensions do not count toward the main-number length. No country-specific length or country code is enforced, and the submitted value is preserved. This checks syntax, not whether the number exists or is reachable. Invalid values return HTTP 400 with a plain localized contact-phone message, without an internal field-path prefix. The field identifier remains in structured `details` for clients; display the top-level `message` in the portal toast. Validation runs before invitation email delivery or account writes.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/users/add-customer?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| phone | string |  |  |  |
| email | string |  |  |  |



### Request body (raw)

```json
{
  "name": "Jane Customer",
  "phone": "+1234567890",
  "email": "optional@example.com"
}
```

### Response 201 Created



```json
{
  "success": true,
  "message": "user_created_successfully",
  "data": {
    "user": {
      "id": "customer-uuid",
      "phone": "+1234567890",
      "email": "optional@example.com"
    }
  }
}
```

### Response 409 Conflict - Already Exists



```json
{
  "success": false,
  "message": "user_already_exists"
}
```

## POST /users/resend-invitation — Resend User Invitation

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#post-resend-user-invitation)

Resends an invitation email to a user who was previously invited but may not have received or acted on the original invitation.

- Must have appropriate permissions to invite users

### Endpoint

`POST /users/resend-invitation`

### Required Fields
- `email`: Valid email address of the user to resend invitation to

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/users/resend-invitation?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| email | string |  |  |  |



### Request body (raw)

```json
{
  "email": "user@example.com"
}
```

### Response 200 Success Response



```json
{
  "success": true,
  "message": "Invitation resent successfully",
  "data": {
    "user": {
      "id": "user-uuid",
      "email": "user@example.com",
      "invited_at": "2023-01-01T00:00:00Z"
    }
  }
}
```

### Response 400 Error - Bad Request



```json
{
  "success": false,
  "message": "Invitation failed"
}
```

### Response 404 Error - Not Found



```json
{
  "success": false,
  "code": "user_not_found",
  "message": "User not found"
}
```

### Response 409 Error - Already Confirmed



```json
{
  "success": false,
  "message": "User already confirmed"
}
```

### Response 500 Error - Server Error



```json
{
  "success": false,
  "message": "Internal server error"
}
```

### Response 404 Not Found (404 Not Found)



```json
{
  "success": false,
  "message": "User not found"
}
```

## GET /users/customers — List Customers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#get-list-customers)

Lists customers (users without assigned roles) with pagination, search, sorting, column filters, and KPI stats. Legacy top-level `is_business` still works.

- Requires tenant context

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/users/customers?language=en&page=1&limit=10&tenant_id={{tenant_id}}&is_business=&is_market_user=&market_id=&is_restaurant_user=&restaurant_location_id=&search=&sort_by=&sort_order=&column_filters=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |
| page | query string | No | Page number for pagination (Optional, default: 1) | 1 |
| limit | query string | No | Number of records per page (Optional, default: 10) | 10 |
| tenant_id | query string | No | Tenant ID to filter customers (Optional, uses context if not provided) | {{tenant_id}} |
| is_business | query string | No | Filter by customer type: true for business, false for individual (Optional) |  |
| is_market_user | query string | No | Filter customers by market-context flag (`true` or `false`) (Optional) |  |
| market_id | query string | No | Filter customers by market ID (Optional) |  |
| is_restaurant_user | query string | No | Filter customers by restaurant-context flag (`true` or `false`) (Optional) |  |
| restaurant_location_id | query string | No | Filter customers by restaurant location ID (Optional) |  |
| search | query string |  | Toolbar search across first_name, last_name, full_name, email, and phone |  |
| sort_by | query string |  | Sort by one of: name, email, created_at, last_login_at |  |
| sort_order | query string |  | Sort order: asc or desc. Defaults to desc when sort_by is provided. |  |
| column_filters | query string |  | JSON string with keys: full_name, email, phone, status, user_type, is_market_user, is_restaurant_user, created_at, last_login_at JSON string with keys `full_name`, `email`, `phone`, `status`, `user_type`, `is_market_user`, `is_restaurant_user`, `created_at`, `last_login_at` |  |



### Response

```json
{
  "success": true,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": "user-uuid",
      "email": "customer@example.com",
      "first_name": "Jane",
      "last_name": "Doe",
      "full_name": "Jane Doe",
      "user_type": "individual",
      "is_market_user": false,
      "market_id": null,
      "is_restaurant_user": false,
      "restaurant_location_id": null,
      "roles": {
        "id": null,
        "name": "customer",
        "scope": "tenant"
      },
      "tenant": {
        "id": "tenant-uuid",
        "name": "Example Tenant"
      },
      "last_login_at": "2025-09-22T08:30:00Z"
    }
  ],
  "stats": {
    "totalUsers": 25,
    "activeUsers": 20,
    "pendingUsers": 3,
    "recentlyJoined": [
      {
        "id": "user-uuid",
        "first_name": "Jane",
        "last_name": "Doe",
        "email": "customer@example.com",
        "avatar_url": null,
        "created_at": "2025-09-20T10:42:09.258391+00:00"
      }
    ]
  },
  "page": 1,
  "limit": 10,
  "total": 25
}
```

### Response 200 Success Response



```json
{
  "success": true,
  "message": "Users retrieved successfully",
  "users": [
    {
      "id": "user-uuid",
      "email": "customer@example.com",
      "first_name": "Jane",
      "last_name": "Doe",
      "full_name": "Jane Doe",
      "user_type": "individual",
      "is_market_user": false,
      "market_id": null,
      "is_restaurant_user": false,
      "restaurant_location_id": null,
      "roles": {
        "id": null,
        "name": "customer",
        "scope": "tenant"
      },
      "tenant": {
        "id": "tenant-uuid",
        "name": "Example Tenant"
      }
    }
  ],
  "stats": {
    "totalUsers": 25,
    "activeUsers": 20,
    "pendingUsers": 3,
    "recentlyJoined": [
      {
        "id": "user-uuid",
        "first_name": "Jane",
        "last_name": "Doe",
        "email": "customer@example.com",
        "avatar_url": null,
        "created_at": "2025-09-20T10:42:09.258391+00:00"
      }
    ]
  },
  "page": 1,
  "limit": 10,
  "total": 25
}
```

## PUT /users/:id — Update User

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#put-update-user)

Updates a user's information. Users can update their own information, while system administrators can update any user.

- Users can update their own profile (including `status`)
- System administrators / tenant administrators can update any user profile (email change is self-only)

### Updatable Fields
- `email`: Change login email (self-only). Triggers a confirmation email and redirects to sign-in after confirmation.
- `first_name`: User's first name
- `last_name`: User's last name
- `status`: User account status (self or admin)
- `phone`: User's phone number
- `avatar_url`: URL to user's profile image
- `profile_data`: Object containing additional profile information
- `customer_delivery_preferences`: Object containing delivery preferences

### Notes
- Only provided fields will be updated. Fields not included in the request will remain unchanged.
- Email change sends a confirmation email via Supabase Auth and is allowed only when updating your own user.
- When email is updated, the API returns immediately with `requires_email_confirmation: true` and `new_email`. The frontend should display the message instructing the user to check their new email inbox to confirm.

### Response

### Status editing

pending users may now be edited, activated, or deactivated through this endpoint. A supplied `status` must be `active` or `inactive`; omit it to preserve the existing status, including pending. Existing authorization and administrator deactivation protections remain in effect. Status affects the global application account, not an individual tenant membership, and does not confirm email or set a password. Frontend: show the current status, including Pending as a display-only option, submit status only when changed, keep the modal open on failure, and refresh the user on success. An unsupported status returns HTTP 400.

### Contact phone validation

`business_details.contact_person.phone` remains optional (omitted, null or blank is accepted). A supplied value must contain at least two main-number digits and use phone formatting. Local and international formats, Unicode decimal digits, spaces, parentheses, dots, slashes, hyphens and one leading + are accepted. Optional extensions use ext, ext., extension, x, # or ;ext=. Extensions do not count toward the main-number length. No country-specific length or country code is enforced, and the submitted value is preserved. This checks syntax, not whether the number exists or is reachable. Invalid values return HTTP 400 with a plain localized contact-phone message, without an internal field-path prefix. The field identifier remains in structured `details` for clients; display the top-level `message` in the portal toast. Validation runs before invitation email delivery or account writes.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/users/:id?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the user to update (Required) | user-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| email | string |  |  |  |
| first_name | string |  |  |  |
| last_name | string |  |  |  |
| status | string |  |  |  |
| phone | string |  |  |  |
| avatar_url | string |  |  |  |
| profile_data | object |  |  |  |
| profile_data.bio | string |  |  |  |
| profile_data.preferences | object |  |  |  |
| profile_data.preferences.theme | string |  |  |  |
| profile_data.preferences.notifications | boolean |  |  |  |
| customer_delivery_preferences | object |  |  |  |
| customer_delivery_preferences.preferred_time | string |  |  |  |
| customer_delivery_preferences.special_instructions | string |  |  |  |



### Request body (raw)

```json
{
  "email": "new.email@example.com",
  "first_name": "Updated",
  "last_name": "Name",
  "status": "inactive",
  "phone": "+9876543210",
  "avatar_url": "https://example.com/avatar.jpg",
  "profile_data": {
    "bio": "Updated user bio",
    "preferences": {
      "theme": "dark",
      "notifications": true
    }
  },
  "customer_delivery_preferences": {
    "preferred_time": "evening",
    "special_instructions": "Leave at the door"
  }
}
```

### Response 200 Success Response - Regular Update



```json
{
  "success": true,
  "message": "User updated successfully",
  "user": {
    "id": "user-uuid",
    "email": "user@example.com",
    "first_name": "Updated",
    "last_name": "Name",
    "status": "inactive",
    "phone": "+9876543210",
    "avatar_url": "https://example.com/avatar.jpg",
    "profile_data": {
      "bio": "Updated user bio",
      "preferences": {
        "theme": "dark",
        "notifications": true
      }
    },
    "customer_delivery_preferences": {
      "preferred_time": "evening",
      "special_instructions": "Leave at the door"
    },
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-01-01T00:00:00Z"
  }
}
```

### Response 200 Success Response - Email Change (requires confirmation)



```json
{
  "success": true,
  "message": "Please check your inbox at new.email@example.com to confirm the email change",
  "requires_email_confirmation": true,
  "new_email": "new.email@example.com"
}
```

### Response 401 Error - Unauthorized



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 400 Error - Bad Request



```json
{
  "success": false,
  "message": "Failed to update user"
}
```

### Response 200 Success (200 OK) - Regular update



```json
{
  "success": true,
  "message": "User updated successfully",
  "user": { ... }
}
```

### Response 401 Unauthorized (401 Unauthorized)



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## PUT /users/me/password — Update My Password

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#put-update-my-password)

Updates the current authenticated user's password.

- Requires valid authenticated user bearer token
- API key authentication is not allowed

### Required Fields
- `new_password`: New password (min: 8, max: 128 characters)

### Optional Fields
- `current_password`: Required when the user already has a password. Omit it only when setting a password for the first time after OTP or magic link sign-in.

### Notes
- The authenticated user is always the target user
- Users with an existing password must provide the correct current password
- Users without an existing password can set one by providing only `new_password`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/users/me/password?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| current_password | string |  |  |  |
| new_password | string |  |  |  |



### Request body (raw)

```json
{
  "current_password": "CurrentSecurePassword123!",
  "new_password": "NewSecurePassword123!"
}
```

### Response 200 Success Response



```json
{
  "success": true,
  "message": "Password updated successfully",
  "data": {
    "user_id": "user-uuid"
  }
}
```

### Response 400 Error - Bad Request



```json
{
  "success": false,
  "message": "Current password is required"
}
```

### Response 403 Error - Unauthorized



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 404 Error - Not Found



```json
{
  "success": false,
  "message": "User not found"
}
```

### Response 500 Error - Server Error



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## PUT /users/me/notifications — Update My Notification Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#put-update-my-notification-settings)

Enable or disable in-app/push/email/SMS notifications for the current user.

When `notifications_enabled` is `false`, new rows in `notifications` are stored with status `skipped_disabled_notification` and are not delivered; they do not appear in GET `/messaging/notifications` (which only returns `status: sent`).

Bearer token only (API key not supported).

### Response (200)

`{ success, message, data: { user_id, notifications_enabled } }`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/users/me/notifications?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| notifications_enabled | boolean | Yes |  |  |



### Request body (raw)

```json
{
  "notifications_enabled": true
}
```

### Response 200 Success Response



```json
{
  "success": true,
  "message": "Notification settings updated successfully",
  "data": {
    "user_id": "user-uuid",
    "notifications_enabled": true
  }
}
```

### Response 400 Error - Bad Request



```json
{
  "success": false,
  "message": "field_required"
}
```

### Response 401 Error - Unauthorized



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## PUT /users/:id/reset-password — Reset User Password by ID

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#put-reset-user-password-by-id)

Resets a user's password by ID after permission and target-scope authorization.

- JWT only; API keys return 403 rbac_forbidden.
- manage:user_access:system may reset any target.
- manage:user_access:tenant may reset only a target with an active membership in the caller's exact tenant and no system-scoped role.
- Role names and the deprecated is_system_admin flag do not authorize this operation.

The complete target boundary is checked in one indexed query before auth.admin.updateUserById. Missing caller permission returns 403 rbac_forbidden. Missing, inactive, foreign-tenant, or system-scoped targets return scope-safe 404 user_not_found and the password is unchanged.

{"password":"NewSecurePassword123!"}; password length is 8-128 characters. Success remains 200 with data.user_id, email, and phone.

### Frontend integration

send the returned user ID only when user.can_reset_password from GET /users/:id is true. Handle 403 as denied caller access and 404 as unavailable/out-of-scope. The backend check remains authoritative.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/users/:id/reset-password?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the user whose password to reset (Required) | user-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| password | string |  |  |  |



### Request body (raw)

```json
{
  "password": "NewSecurePassword123!"
}
```

### Response 200 Success Response



```json
{
  "success": true,
  "message": "User updated successfully",
  "data": {
    "user_id": "user-uuid",
    "email": "user@example.com",
    "phone": "+1234567890"
  }
}
```

### Response 400 Error - Bad Request



```json
{
  "success": false,
  "message": "Validation error"
}
```

### Response 403 Error - Unauthorized



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 404 Error - Not Found



```json
{
  "success": false,
  "message": "User not found"
}
```

### Response 500 Error - Server Error



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## DELETE /users/:id — Delete User

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#delete-delete-user)

Delete a user. This operation may be permanent or may mark the user as deleted depending on implementation.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/users/:id?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the user to delete (Required) | user-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Response 204 Success Response



### Response 404 Error - Not Found



```json
{
  "error": "Not Found",
  "message": "User not found",
  "status": 404
}
```

### Response 403 Error - Forbidden



```json
{
  "error": "Forbidden",
  "message": "Cannot delete own account",
  "status": 403
}
```

## POST /users/:userId/roles — Assign User Role

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#post-assign-user-role)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

user_access:system, or manage:user_access:tenant for the caller’s current tenant. Broad manage:system, manage:tenant and manage:users:tenant permissions do not substitute. Tenant callers may grant only tenant roles whose complete permission set is a subset of their effective permissions. They cannot change system users or operate across tenants. Role scope must match role_type. System assignments have tenant_id=null. Tenant assignments require tenant_id.

### Required

role_id. System user-access callers must explicitly send role_type. Tenant callers default role_type=tenant and tenant_id to their current tenant. Optional tenant_id for system callers assigning tenant roles. Malformed JSON, scalar bodies and malformed UUIDs return 400. Self changes return 403; changing the last active system_admin or tenant_admin returns 409. A business user without an existing role cannot be assigned one (400). Authorization uses the permissions already loaded by authentication middleware. Membership and role assignment use separate database writes; failures stop the remaining steps but earlier writes may persist. Existing assignments are updated in place. Audit logging uses the existing request middleware. Success remains 201 for a new assignment, 200 for an existing assignment, {success:true,message}.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. For add/invite only, handle 500 rbac_provisioning_incomplete as an incomplete account kept disabled for administrator recovery. No navigation action is added.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/users/:userId/roles?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | The unique identifier of the user to assign the role to |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Language code for response messages (default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| role_id | string |  |  |  |
| role_type | string |  |  |  |
| tenant_id | string |  |  |  |



### Request body (raw)

```json
{
  "role_id": "role-uuid",
  "role_type": "tenant",
  "tenant_id": "tenant-uuid"
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Role assigned successfully"
}
```

### Response 400 Error - Invalid Request Body

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request body"
}
```

### Response 400 Error - User Already Has Role

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "User already has role"
}
```

### Response 400 Error - Invalid Role Type

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "System role cannot be assigned to tenant"
}
```

### Response 403 Error - Cannot Change Own Role

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "You cannot change your own role"
}
```

### Response 409 Error - Last Admin Quorum Violation

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot remove the last admin of this tenant. Assign another admin first."
}
```

### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 403 rbac_invalid_scope



```json
{
  "success": false,
  "code": "rbac_invalid_scope",
  "message": "The role, permission, or tenant scope is not allowed."
}
```

## GET /users/:userId/roles — Get User Role

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#get-get-user-role)

Retrieves roles assigned to a user. This endpoint returns all roles associated with the specified user, including role name, scope, role type, and tenant ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/users/:userId/roles?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | ID of the user to get roles for |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Role retrieved successfully",
  "roles": [
    {
      "id": "role-123",
      "name": "Admin",
      "scope": "global",
      "role_type": "admin",
      "tenant_id": "tenant-456"
    },
    {
      "id": "role-789",
      "name": "Editor",
      "scope": "tenant",
      "role_type": "user",
      "tenant_id": "tenant-456"
    }
  ]
}
```

### Response 400 Error Response

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Failed to get role"
}
```

## DELETE /users/:userId/roles/:roleId — Remove User Role

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#delete-remove-user-role)

authenticated JWT or API key. API keys inherit their owner’s permissions; the key’s system/tenant scope remains a ceiling. The api_keys.permissions=[] field is not reinterpreted. The deprecated is_system_admin flag and role names do not authorize these operations.

Errors use {success:false, code, message}; message is localized (en/ar/fa/ku). 403: rbac_forbidden or rbac_invalid_scope. 409: rbac_preservation_conflict, role_already_exists, or the existing last-administrator protection where applicable. Unexpected database errors return 500 with a safe localized message.

### Requires manage

user_access:system, or manage:user_access:tenant for the caller’s current tenant. Broad manage:system, manage:tenant and manage:users:tenant permissions do not substitute. Tenant callers may grant only tenant roles whose complete permission set is a subset of their effective permissions. They cannot change system users or operate across tenants. Role scope must match role_type. System assignments have tenant_id=null. Tenant assignments require tenant_id.

Record removal is blocked by the preservation policy. Unauthorized callers receive 403. Authorized callers receive 409 rbac_preservation_conflict. No role, permission assignment, user assignment, or dependent record is deleted. There is no success response while this policy is active.

### Frontend integration

gate controls using the documented permission names and scope. Display localized message for failures; branch on code/status, never message. Handle 403 as denied access and 409 as a conflict requiring correction. For add/invite only, handle 500 rbac_provisioning_incomplete as an incomplete account kept disabled for administrator recovery. No navigation action is added.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/users/:userId/roles/:roleId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | ID of the user to remove the role from |  |
| roleId | path string | Yes | ID of the role to be removed from the user |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Response 403 rbac_forbidden



```json
{
  "success": false,
  "code": "rbac_forbidden",
  "message": "You do not have permission to perform this action."
}
```

### Response 409 rbac_preservation_conflict



```json
{
  "success": false,
  "code": "rbac_preservation_conflict",
  "message": "This operation would remove existing access records and is not permitted."
}
```

## GET /users/:userId/addresses — Get User Addresses

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/overview#get-get-user-addresses)

Lists addresses belonging to a specific user with pagination. Users can only access their own addresses, while system administrators can access any user's addresses.

- Users can only access their own addresses
- System administrators can access any user's addresses

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/users/:userId/addresses?language=en&page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | UUID of the user whose addresses to retrieve (Required) | user-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (e.g., 'en') | en |
| page | query string | No | Page number for pagination (Optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of records per page (Optional, default: 10) Number of records per page (default: 10) | 10 |



### Response 200 Success Response



```json
{
  "success": true,
  "message": "Addresses retrieved successfully",
  "addresses": [
    {
      "id": "address-uuid",
      "user_id": "user-uuid",
      "title": "Home",
      "address_line1": "123 Main St",
      "address_line2": "Apt 4B",
      "city": "New York",
      "state": "NY",
      "postal_code": "10001",
      "country": "US",
      "is_primary": true,
      "is_active": true,
      "latitude": 40.7128,
      "longitude": -74.006,
      "created_at": "2023-01-01T00:00:00Z",
      "updated_at": "2023-01-01T00:00:00Z"
    }
  ],
  "total": 5,
  "page": 1,
  "limit": 10
}
```

### Response 403 Error - Unauthorized



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 400 Error - Bad Request



```json
{
  "success": false,
  "message": "Error retrieving addresses"
}
```

## POST /users/:userId/wallet/manual-transaction — Manual Customer Wallet Transaction

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/wallets#post-manual-customer-wallet-transaction)

Creates a manual wallet credit or debit for an individual or business customer in the authenticated tenant. Requires a system administrator, the exact `tenant_admin` role, or an explicit `manage:wallets` permission. Finance must be granted `manage:wallets`; `manage:tenant` alone does not authorize adjustments, including for dispatchers. Wallet ownership alone does not grant access. Staff roles and drivers cannot be targeted by this customer endpoint.

type is credit or debit; amount is positive, below 1,000,000,000,000,000, with at most four decimal places; reason is required, trimmed, and at most 500 characters. Optional reference (at most 200 characters) and notes (at most 500 characters) must be strings or null. They are trimmed; omitted, null, empty, and whitespace-only values become null. Length limits apply after trimming. Actor identity and tenant come from authenticated context. Currency comes from tenant settings.

One database commit locks the wallet, checks the customer minimum available balance (draft reservations are already reflected in wallet balance), and writes the ledger record and audit event. The limit comes from `customer_wallet_minimum_balance.settings.minimum_balance`; `wallet_minimum_balance` is for drivers. Credits can restore a wallet already below its limit. Credit ledger rows use `type=earning` and positive amount; debit rows use `type=payment` and negative amount. This endpoint response keeps the requested positive amount and credit/debit type, and returns saved `reference` and `notes` as string or null. Both optional fields are stored in the existing transaction metadata and audit event. Nonempty notes append ` - {notes}` to the description; without notes, existing description behavior is unchanged.

`Idempotency-Key` is a required UUID. Repeating the same key and payload returns the original transaction and balance with `replayed=true`, without another ledger/audit write. Reusing a key with a different tenant, customer, actor, type, amount, reason, normalized reference, or normalized notes returns HTTP 409 `idempotency_conflict`.

### Frontend integration

display Credit/Debit, positive amount, required reason, optional Reference (200 characters), and optional Notes (500 characters), then confirm before POST. Include the optional values in both the confirmation and payload; preserve the same complete payload for an uncertain retry. Generate one UUID per confirmed adjustment and retain it for uncertain retries. After HTTP 200, refresh the existing wallet summary and ledger; stay on the customer's Wallet tab. Read ledger `manual_transaction.type`, `.reason`, `.reference`, `.notes`, and `.actor.{id,name}` for action, reason, and administrator. Do not infer adjustments from description text. Show the button to system administrators, users with the exact `tenant_admin` role, or users granted `manage:wallets`; the backend independently enforces the same rule.

Errors use localized `message` and stable `code`: 400 `reference_too_long`, `notes_too_long`, `invalid_amount`, `invalid_transaction_type`, `reason_required`, `reason_too_long`, `idempotency_key_required`, `tenant_id_required`, `invalid_request_body`, or `insufficient_wallet_balance`; 403 `insufficient_permissions`; 404 `customer_not_found` or `wallet_not_found`; 409 `idempotency_conflict`; 500 `internal_server_error`. Invalid optional-field types or lengths use the existing HTTP 400 validation response with localized `message` and `details` entries identifying `reference` or `notes`. Invalid user, tenant, or idempotency UUIDs use the same validation format. Allow correction on 400, deny the action on 403, and start a fresh adjustment on 409.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/users/:userId/wallet/manual-transaction?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| Idempotency-Key | header string | Yes | Required UUID generated once per confirmed adjustment; reuse for retries. | {{wallet_adjustment_key}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | Customer user UUID, not tenant_users ID or driver ID. | {{customer_user_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Selected tenant; authentication must resolve this tenant. | {{tenant_id}} |
| language | query string |  | Response language: en, ar, fa, or ku. | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| type | string |  |  |  |
| amount | number |  |  |  |
| reason | string |  |  |  |
| reference | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "type": "credit",
  "amount": 10000,
  "reason": "Customer cash payment received",
  "reference": "CASH-2026-001",
  "notes": "Received at the front desk."
}
```

### Response 200 Success



```json
{
  "success": true,
  "message": "Customer wallet transaction saved successfully.",
  "transaction": {
    "transaction_id": "550e8400-e29b-41d4-a716-446655440000",
    "new_balance": 15000,
    "amount": 10000,
    "type": "credit",
    "reason": "Customer cash payment received",
    "created_at": "2026-10-14T12:00:00Z",
    "actor": {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "name": "Tenant Admin"
    },
    "replayed": false,
    "reference": "CASH-2026-001",
    "notes": "Received at the front desk."
  }
}
```

### Response 200 Idempotent Replay



```json
{
  "success": true,
  "message": "Customer wallet transaction saved successfully.",
  "transaction": {
    "transaction_id": "550e8400-e29b-41d4-a716-446655440000",
    "new_balance": 15000,
    "amount": 10000,
    "type": "credit",
    "reason": "Customer cash payment received",
    "created_at": "2026-10-14T12:00:00Z",
    "actor": {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "name": "Tenant Admin"
    },
    "replayed": true,
    "reference": "CASH-2026-001",
    "notes": "Received at the front desk."
  }
}
```

### Response 400 Minimum Balance Rejected



```json
{
  "success": false,
  "code": "insufficient_wallet_balance",
  "message": "This debit would take the available customer wallet balance below its minimum limit."
}
```

### Response 403 Permission Denied



```json
{
  "success": false,
  "code": "insufficient_permissions",
  "message": "Insufficient permissions"
}
```

### Response 404 Customer Not Found In Tenant



```json
{
  "success": false,
  "code": "customer_not_found",
  "message": "Customer not found"
}
```

### Response 409 Different Payload For Request Key



```json
{
  "success": false,
  "code": "idempotency_conflict",
  "message": "This request key was already used for a different wallet adjustment. Start a new adjustment."
}
```

## POST /users/:userId/wallet/topup — Top-up Wallet

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/wallets#post-top-up-wallet)

### Top-up User Wallet

Adds funds to a user's wallet. The wallet is automatically identified by the user ID.

- Requires Bearer Token authentication
- User can only top-up their own wallet
- Tenant admins can top-up any user's wallet

### Endpoint

`POST /users/{userId}/wallet/topup`

### Features
- Automatic wallet lookup by user ID
- Currency validation
- Transaction record creation
- Balance update via database trigger
- Tenant isolation

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/users/:userId/wallet/topup?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | UUID of the user whose wallet to top up | {{user_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| amount | number |  |  |  |
| currency | string |  |  |  |
| description | string |  |  |  |



### Request body (raw)

```json
{
  "amount": 100,
  "currency": "USD",
  "description": "Adding funds to wallet"
}
```

### Request example 1

```json
{
  "amount": 100.00,     // required - Amount to add (min: 0.01)
  "currency": "USD",   // optional - Currency code (default: USD)
  "description": "Adding funds to wallet" // optional - Description
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "wallet": {
    "transaction_id": "550e8400-e29b-41d4-a716-446655440000",
    "new_balance": 1000,
    "amount": 100,
    "currency": "USD",
    "timestamp": "2023-01-01T00:00:00Z"
  }
}
```

## POST /users/:userId/wallet/transfer — Transfer Funds

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/wallets#post-transfer-funds)

### Transfer Funds Between Users

Transfers funds from one user's wallet to another user within the same tenant.

- Requires Bearer Token authentication
- User can only transfer from their own wallet

### Endpoint

`POST /users/{userId}/wallet/transfer`

### Features
- Automatic sender wallet lookup by user ID
- Recipient lookup by email or user_id
- Balance validation
- Atomic transaction
- Tenant isolation
- Transfer history tracking

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/users/:userId/wallet/transfer?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | UUID of the sender user | {{user_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| amount | number |  |  |  |
| recipient_identifier | string |  |  |  |
| identifier_type | string |  |  |  |
| description | string |  |  |  |



### Request body (raw)

```json
{
  "amount": 50,
  "recipient_identifier": "recipient@example.com",
  "identifier_type": "email",
  "description": "Payment for services"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "transfer": {
    "transfer_id": "550e8400-e29b-41d4-a716-446655440000",
    "new_balance": 950,
    "amount": 50,
    "currency": "USD",
    "timestamp": "2023-01-01T00:00:00Z"
  }
}
```

## GET /users/:userId/wallet/transactions — List Wallet Transactions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/users/wallets#get-list-wallet-transactions)

### List User Wallet Transactions

Retrieves a paginated list of wallet transactions for a specific user with filtering options.

- Requires Bearer Token authentication
- User can only access their own transactions
- System administrators or staff with `manage:tenant` or `manage:wallets` can access wallets of users belonging to their authenticated tenant

### Endpoint

`GET /users/{userId}/wallet/transactions`

### Summary Rules
- `summary.total_credited` is the sum of positive `wallet_transactions.amount` values over the full filtered result set.
- `summary.total_debited` is the sum of absolute negative `wallet_transactions.amount` values over the full filtered result set.
- Summary totals respect `type`, `search`, `from_date`, `to_date`, `source`, and `actor_name`, but never `page` or `limit`.
- Cash/COD collections that do not create wallet transaction rows are not included in wallet credited/debited totals.

### Features
- Automatic wallet lookup by user ID
- Comprehensive pagination metadata
- Type and date range filtering
- Search on amount and description
- Transfer details enhancement
- Chronological ordering
- Wallet balance and filtered wallet summary included

### Frontend integration contract

Use `summary.total_credited` and `summary.total_debited` for wallet stat cards. Do not reduce the current page of `transactions` to calculate these totals.

### Manual customer adjustments

New manual entries include `manual_transaction: {type: "credit"|"debit", reason: string, reference: string | null, notes: string | null, actor: {id: string, name: string}}`. These values are saved with the transaction; no user lookup is required. Reference and Notes are null for older manual entries that did not store them. Display the saved values rather than parsing the description. The `manual_transaction` field remains absent for non-manual entries. Manual credit rows retain ledger `type=earning` and debit rows retain `type=payment`; signed `amount` remains the source for summary totals. Include `tenant_id` in staff requests so authentication resolves the intended tenant.

### Source and Recorded By columns

Every row includes `source: string | null` and `recorded_by: { id: string | null, name: string | null } | null`, projected only from saved metadata. Source is the literal saved source (for example `customer_manual_adjustment` or `delivery_fee_refund`), not a Manual/Other category. Recorded By uses the saved `actor_user_id` and `actor_name`; missing or empty values are null. No user lookup is performed, and older rows without a saved actor name must not be given an invented name. The existing manual transaction object remains available.

Use a dropdown for Source, populated from top-level `source_options: string[]`. Options are the distinct nonempty saved sources from the full wallet history, sorted and independent of all filters and pagination, including when the current filtered result is empty. Display readable localized labels with a humanized fallback, but send the exact selected raw value as `source`. Source matches the saved value exactly and case-sensitively; All means omit `source`. Recorded By remains a text filter sent as `actor_name`, with case-insensitive literal contains matching. Both filter inputs are trimmed; empty inputs are ignored. Asterisk, percent, underscore, backslash and other pattern characters are ordinary text. All active filters are combined with AND before pagination and are applied identically to `total`, `summary.total_credited`, and `summary.total_debited`; current wallet balance remains unfiltered. Reset the page to 1 when changing column filters.

### Example

`GET /users/:userId/wallet/transactions?tenant_id={{tenant_id}}&source=customer_manual_adjustment&actor_name=Amit&page=1&limit=20`. Authentication, access-denied responses and other error behavior are unchanged.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/users/:userId/wallet/transactions?page=1&limit=20&type=&search=&source=&actor_name=&from_date=&to_date=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| userId | path string | Yes | UUID of the user whose transactions to retrieve | {{user_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| source | query string | No | Exact, case-sensitive saved source value selected from source_options; omit for All. (string, optional): Exact, case-sensitive match on the selected saved source value |  |
| actor_name | query string | No | Trimmed, case-insensitive literal contains match on saved actor name; rows without a name do not match. (string, optional): Case-insensitive literal contains match on the saved recorded-by name |  |
| page | query string | No | Page number for pagination (default: 1) (number, optional): Page number (default: 1) | 1 |
| limit | query string | No | Number of items per page (default: 20, max: 100) (number, optional): Items per page (default: 20, max: 100) | 20 |
| type | query string | No | Filter by transaction type: topup, transfer, payment, earning (string, optional): Filter by type (topup, transfer, payment, earning) |  |
| search | query string | No | Search by description (partial match) or amount (exact match). If numeric, matches amount; always matches description. (string, optional): Search by description (partial match) or amount (exact match) |  |
| from_date | query string | No | Filter from date (ISO format: 2024-01-01T00:00:00Z) (string, optional): Filter from date (ISO format) |  |
| to_date | query string | No | Filter to date (ISO format: 2024-12-31T23:59:59Z) (string, optional): Filter to date (ISO format) |  |
| language | query string | No | Language code for response localization (Optional, default: en) | en |



### Response 200 Success - Source and Recorded By filters



```json
{
  "success": true,
  "message": "Wallet transactions retrieved successfully",
  "wallet": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "balance": 15000,
    "reserved": 0
  },
  "summary": {
    "current_balance": 15000,
    "total_credited": 10000,
    "total_debited": 0
  },
  "transactions": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "type": "earning",
      "amount": 10000,
      "description": "Manual credit: Customer payment - Received at the front desk.",
      "reference_id": null,
      "reference_type": null,
      "created_at": "2026-10-14T12:00:00Z",
      "delivery": null,
      "source": "customer_manual_adjustment",
      "recorded_by": {
        "id": "550e8400-e29b-41d4-a716-446655440002",
        "name": "Amit"
      },
      "manual_transaction": {
        "type": "credit",
        "reason": "Customer payment",
        "actor": {
          "id": "550e8400-e29b-41d4-a716-446655440002",
          "name": "Amit"
        },
        "reference": "CASH-2026-001",
        "notes": "Received at the front desk."
      }
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1,
  "source_options": [
    "customer_manual_adjustment",
    "delivery_fee_refund",
    "wallet_delivery_fee"
  ]
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "wallet_transactions_retrieved_successfully",
  "wallet": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "balance": 1500.75,
    "reserved": 25.5
  },
  "summary": {
    "current_balance": 1500.75,
    "total_credited": 100,
    "total_debited": 50
  },
  "transactions": [
    {
      "id": "txn-550e8400-e29b-41d4-a716-446655440001",
      "type": "transfer",
      "amount": -50,
      "description": "Payment for services",
      "reference_id": "transfer-550e8400-e29b-41d4-a716-446655440002",
      "reference_type": "transfer",
      "created_at": "2024-01-02T14:30:00Z",
      "transfer_details": {
        "transfer_id": "transfer-550e8400-e29b-41d4-a716-446655440002",
        "from_wallet_id": "550e8400-e29b-41d4-a716-446655440000",
        "to_wallet_id": "wallet-550e8400-e29b-41d4-a716-446655440003",
        "recipient_info": {
          "email": "recipient@example.com",
          "first_name": "John",
          "last_name": "Doe"
        }
      }
    },
    {
      "id": "txn-550e8400-e29b-41d4-a716-446655440004",
      "type": "topup",
      "amount": 100,
      "description": "Wallet top-up",
      "reference_id": null,
      "reference_type": null,
      "created_at": "2024-01-01T10:15:00Z"
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 45
}
```

## POST /wallets/topup — Top-up Wallet

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/wallets/top-up#post-top-up-wallet-2)

Initiates a wallet top-up payment. The API does not accept or return a currency field; the frontend displays currency from tenant configuration.

- Requires Bearer Token authentication: `Authorization: Bearer {{access_token}}`

### Field Descriptions
- `amount` (number, required): Amount to add. Must be a positive number with min value 0.01.
- `payment_method_type_id` (string, required): Payment method type id.
- `back_ref` (string, optional): Return URL/reference for gateway flow.
- `description` (string, optional): Description metadata.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/wallets/topup?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) (string, optional): Language code for localized response messages (default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| amount | number |  |  |  |
| payment_method_type_id | string |  |  |  |
| back_ref | string |  |  |  |
| description | string |  |  |  |



### Request body (raw)

```json
{
  "amount": 100,
  "payment_method_type_id": "uuid",
  "back_ref": "https://your-site/return",
  "description": "Adding funds to wallet"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | no-store |



```json
{
  "success": true,
  "payment": {
    "payment_id": "550e8400-e29b-41d4-a716-446655440000",
    "status": "PENDING",
    "payment_url": "https://gateway.example/redirect"
  }
}
```

### Response 400 Error - Invalid Amount

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid amount"
}
```

### Response 404 Error - Wallet Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Wallet not found"
}
```

### Response 403 Error - Unauthorized Access

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized wallet access"
}
```

### Response 400 Error - Validation Error

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation error"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 200 200 OK - Success



```json
{
  "success": true,
  "payment": { ... }
}
```

## POST /wallets/transfer — Transfer Funds

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/wallets/transfer#post-transfer-funds-2)

Transfers funds from the sender wallet resolved from the auth context to a recipient wallet within the same tenant. The response does not return currency; the frontend displays currency from tenant configuration.

- Requires Bearer Token authentication: `Authorization: Bearer {{access_token}}`

### Response
- Returns `transfer_id`, `new_balance`, `amount`, `timestamp`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/wallets/transfer?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) (string, optional): Language code for localized response messages (default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| amount | number |  |  |  |
| recipient_identifier | string |  |  |  |
| identifier_type | string |  |  |  |
| description | string |  |  |  |



### Request body (raw)

```json
{
  "amount": 100,
  "recipient_identifier": "recipient@example.com",
  "identifier_type": "email",
  "description": "Transfer funds"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | no-store |



```json
{
  "success": true,
  "transfer": {
    "transfer_id": "550e8400-e29b-41d4-a716-446655440000",
    "new_balance": 900,
    "amount": 100,
    "timestamp": "2023-01-01T00:00:00Z"
  }
}
```

### Response 400 Error - Invalid Amount

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid amount"
}
```

### Response 400 Error - Insufficient Balance

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient balance"
}
```

### Response 400 Error - Transfer to Self

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot transfer to self"
}
```

### Response 404 Error - Recipient Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Recipient not found"
}
```

### Response 404 Error - Recipient Wallet Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Recipient wallet not found"
}
```

### Response 403 Error - Unauthorized Access

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized wallet access"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## GET /wallets/transactions — List Transactions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/wallets/transactions#get-list-transactions)

### List Wallet Transactions

Retrieves a paginated list of wallet transactions with full-ledger summary. `data.wallet.balance` is currently usable funds and `data.wallet.reserved` is the total held for draft deliveries. Reserving or releasing does not create a ledger transaction; capture on Execute creates the delivery-fee transaction. Currency is not returned; the frontend displays it from tenant configuration.

- Requires Bearer Token authentication: `Authorization: Bearer {{access_token}}`

### Response Summary
- `data.summary.total_count` — Total filtered transactions across all pages.
- `data.summary.total_credited` — Sum of all positive amounts (full filtered ledger).
- `data.summary.total_debited` — Sum of absolute negative amounts (full filtered ledger).

### Stored transaction context

Each transaction also includes `source: string | null` and `recorded_by: { id: string | null, name: string | null } | null`. These values come only from saved transaction metadata (`source`, `actor_user_id`, `actor_name`), with empty or missing values returned as null. Historical names are not looked up or invented. Customer manual adjustment rows also include `manual_transaction` with saved `type`, `reason`, `reference: string | null`, `notes: string | null`, and `actor: {id, name}`. Reference and Notes are null for older records without those fields; non-manual rows omit this object. Existing permissions and query filters for this endpoint are unchanged. Source/Recorded By column filtering is available on `GET /users/:userId/wallet/transactions` using `source` and `actor_name`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/wallets/transactions?page=1&limit=20&type=&from_date=&to_date=&search=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| walletId | path string | Yes | UUID of the wallet to retrieve transactions for | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination (default: 1) | 1 |
| limit | query string |  | Number of items per page (default: 20, max: 100) | 20 |
| type | query string |  | Filter by transaction type: topup, transfer, payment, earning |  |
| from_date | query string |  | Filter from date (ISO format: 2024-01-01T00:00:00Z) |  |
| to_date | query string |  | Filter to date (ISO format: 2024-12-31T23:59:59Z) |  |
| search | query string |  | Search in description/reference fields and by exact amount when numeric |  |
| language | query string | No | Language code for response localization (Optional, default: en) . | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | no-store |



```json
{
  "success": true,
  "data": {
    "wallet": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "balance": 1500.75,
      "reserved": 25.5
    },
    "transactions": [
      {
        "id": "txn-550e8400-e29b-41d4-a716-446655440001",
        "type": "transfer",
        "amount": -50,
        "description": "Payment for services",
        "reference_id": "transfer-550e8400-e29b-41d4-a716-446655440002",
        "reference_type": "transfer",
        "created_at": "2024-01-02T14:30:00Z",
        "balance_snapshot": 1450.75,
        "transfer_details": {
          "transfer_id": "transfer-550e8400-e29b-41d4-a716-446655440002",
          "from_wallet_id": "550e8400-e29b-41d4-a716-446655440000",
          "to_wallet_id": "wallet-550e8400-e29b-41d4-a716-446655440003",
          "recipient_info": {
            "email": "recipient@example.com",
            "first_name": "John",
            "last_name": "Doe"
          }
        }
      },
      {
        "id": "txn-550e8400-e29b-41d4-a716-446655440004",
        "type": "topup",
        "amount": 100,
        "description": "Wallet top-up",
        "reference_id": null,
        "reference_type": null,
        "created_at": "2024-01-01T10:15:00Z",
        "balance_snapshot": 1500.75
      },
      {
        "id": "txn-550e8400-e29b-41d4-a716-446655440005",
        "type": "earning",
        "amount": 25.5,
        "description": "Delivery completion payment",
        "reference_id": "delivery-550e8400-e29b-41d4-a716-446655440006",
        "reference_type": "delivery",
        "created_at": "2023-12-30T16:45:00Z",
        "balance_snapshot": 1400.75
      }
    ],
    "summary": {
      "total_count": 45,
      "total_credited": 125.5,
      "total_debited": 50
    },
    "pagination": {
      "current_page": 1,
      "per_page": 20,
      "total_items": 45,
      "total_pages": 3,
      "has_next_page": true,
      "has_previous_page": false,
      "next_page": 2,
      "previous_page": null
    }
  }
}
```

### Response 200 Success Response - Filtered by Type

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Cache-Control | header string |  |  | no-store |



```json
{
  "success": true,
  "data": {
    "wallet": {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "balance": 1500.75,
      "reserved": 25.5
    },
    "transactions": [
      {
        "id": "txn-550e8400-e29b-41d4-a716-446655440001",
        "type": "transfer",
        "amount": -50,
        "description": "Payment for services",
        "reference_id": "transfer-550e8400-e29b-41d4-a716-446655440002",
        "reference_type": "transfer",
        "created_at": "2024-01-02T14:30:00Z",
        "transfer_details": {
          "transfer_id": "transfer-550e8400-e29b-41d4-a716-446655440002",
          "from_wallet_id": "550e8400-e29b-41d4-a716-446655440000",
          "to_wallet_id": "wallet-550e8400-e29b-41d4-a716-446655440003",
          "recipient_info": {
            "email": "recipient@example.com",
            "first_name": "John",
            "last_name": "Doe"
          }
        }
      }
    ],
    "summary": {
      "total_count": 8,
      "total_credited": 0,
      "total_debited": 50
    },
    "pagination": {
      "current_page": 1,
      "per_page": 10,
      "total_items": 8,
      "total_pages": 1,
      "has_next_page": false,
      "has_previous_page": false,
      "next_page": null,
      "previous_page": null
    }
  }
}
```

### Response 400 Error - Validation Error

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation error: limit must be between 1 and 100"
}
```

### Response 400 Error - Invalid Transaction Type

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Validation error: type must be one of topup, transfer, payment, earning"
}
```

### Response 404 Error - Wallet Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Wallet not found"
}
```

### Response 403 Error - Unauthorized Access

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized wallet access"
}
```

### Response 401 Error - Unauthorized

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

### Response 500 Error - Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /wallets/transactions-export/{{user_id}} — Export Transactions (CSV)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/wallets/transactions#get-export-transactions-csv)

### Export Wallet Transactions (CSV)

Streams a CSV export of wallet transactions for the user in the URL.

- Requires Bearer Token authentication

### Authorization
- If you have `tenant:wallet:management`, you can export any tenant user's transactions
- Otherwise you can export **only your own** transactions (the `userId` must match the authenticated user)

### Response
- `Content-Type`: `text/csv`
- `Content-Disposition`: attachment filename
- `X-Total-Rows`: total export rows
- `X-Export-Tenant-Id`, `X-Export-Actor-User-Id`, `X-Export-Target-User-Id`

CSV stream.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/wallets/transactions-export/{{user_id}}?type=&from_date=&to_date=&search=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Accept | header string |  |  | text/csv |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_id | path string | Yes | Variable used inside the request path. |  |
| userId | path string | Yes | (required) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| type | query string |  | Filter by transaction type: topup, transfer, payment, earning |  |
| from_date | query string |  | Filter from date (ISO format or YYYY-MM-DD) |  |
| to_date | query string |  | Filter to date (ISO format or YYYY-MM-DD) |  |
| search | query string | No | Search in description/reference fields and by exact amount when numeric (optional) |  |
| language | query string | No | Language code for response localization (Optional, default: en) (optional) | en |



## GET /wallets/user-wallet — Get User Wallet

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/wallets/admin#get-get-user-wallet)

### Get User Wallet (Admin)

Returns wallet + transaction history for a specific user (same tenant). `data.wallet.balance` is usable funds and `data.wallet.reserved` is the total held for draft deliveries. Currency is not returned; the frontend displays it from tenant configuration.

- Requires Bearer Token authentication: `Authorization: Bearer {{access_token}}` (or API key)
- Requires permission: `tenant:wallet:management`

### Response
- Returns `data.user`, `data.wallet`, `data.transactions`, `data.summary`, `data.pagination`.
- `data.summary.total_count` — Total filtered transactions across all pages.
- `data.summary.total_credited` — Sum of all positive amounts (full filtered ledger).
- `data.summary.total_debited` — Sum of absolute negative amounts (full filtered ledger).

### Stored transaction context

Each transaction also includes `source: string | null` and `recorded_by: { id: string | null, name: string | null } | null`. These values come only from saved transaction metadata (`source`, `actor_user_id`, `actor_name`), with empty or missing values returned as null. Historical names are not looked up or invented. Customer manual adjustment rows also include `manual_transaction` with saved `type`, `reason`, `reference: string | null`, `notes: string | null`, and `actor: {id, name}`. Reference and Notes are null for older records without those fields; non-manual rows omit this object. Existing permissions and query filters for this endpoint are unchanged. Source/Recorded By column filtering is available on `GET /users/:userId/wallet/transactions` using `source` and `actor_name`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/wallets/user-wallet?user_id={{user_id}}&page=1&limit=20&type=&from_date=&to_date=&search=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_id | query string | Yes | Target user UUID (required) | {{user_id}} |
| page | query string |  | Page number for pagination (default: 1) | 1 |
| limit | query string |  | Number of items per page (default: 20, max: 100) | 20 |
| type | query string |  | Filter by transaction type: topup, transfer, payment, earning |  |
| from_date | query string |  | Filter from date (ISO format: 2024-01-01T00:00:00Z) |  |
| to_date | query string |  | Filter to date (ISO format: 2024-12-31T23:59:59Z) |  |
| search | query string |  | Search in description/reference fields and by exact amount when numeric |  |
| language | query string | No | Language code for response localization (Optional, default: en) . | en |



### Response 403 Error - Unauthorized

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized"
}
```

## GET /wallets/tenant-transactions — List Tenant Wallet Transactions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/wallets/admin#get-list-tenant-wallet-transactions)

Read-only, tenant-wide wallet ledger for the Financial Dashboard's Transactions and Adjustments tabs. The primary entity is one persisted `wallet_transactions` entry. This does not return cash COD collections, payment attempts, or reserved draft funds that have not become ledger entries.

### Authentication and authorization

Requires Bearer token authentication (`Authorization: Bearer {{access_token}}`) or an authorized `X-API-Key`. The resolved user must be a system administrator, have the exact `tenant_admin` role, or hold at least one of `manage:operations:tenant`, `view:finance:tenant`, `view:financial_reports`, or `manage:wallets`. Ordinary viewers use their authenticated tenant context; a supplied `tenant_id` must match it or the request returns 403. System administrators authenticated by JWT or a system-scoped API key may select a tenant with `tenant_id`; tenant-scoped API keys remain bound to their tenant even when their owner is a system administrator. Every supplied tenant ID is validated as a UUID.

### Column filters
| Key | Value | Meaning |
| --- | --- | --- |
| `account_name` | string | Literal case-insensitive name substring |
| `account_type` | `driver`, `customer`, `company`, `staff` | Wallet account category |
| `customer_type` | `individual`, `business`, `institution` | Customer subtype; implies `account_type=customer` |
| `direction` | `credit`, `debit` | Wallet movement direction |
| `type` | string | Exact stored ledger type selected from `data.type_options` |
| `amount` | `{"min": 10, "max": 100}` | Inclusive range of absolute transaction amounts; either bound is optional and must be nonnegative and less than 1,000,000,000,000,000 |
| `description` | string | Literal substring in reason or description |
| `reason` | string | Literal reason substring |
| `actor_name` | string | Literal substring of the returned administrator name |
| `created_at` | `{"from": "2026-10-01", "to": "2026-10-17"}` | Inclusive UTC dates; either bound is optional. Full ISO timestamps mean exact instants |
| `source` | string | Exact stored source selected from `data.source_options` |
| `reference` | string | Literal saved-reference substring |
| `notes` | string | Literal notes substring |
| `currency_code` | three-letter string | Exact saved currency code; input is normalized to uppercase |
| `delivery_code` | string | Literal linked-delivery code substring |
| `external_reference_id` | string | Literal linked-delivery external-reference substring |

Example `column_filters`:

A customer subtype combined with an explicit non-customer account type is invalid. Unknown column/range keys, malformed JSON, unsupported enum values, reversed ranges, invalid dates, and oversized text values return HTTP 400. Text/search values are limited to 500 characters. Text filter characters such as `%` and `_` are ordinary text rather than wildcard instructions. An omitted or cleared filter does not restrict results.

### Response and business rules
- `data.transactions` contains the requested page; `pagination.total` and `total_pages` describe the same complete filtered population.
- `account` contains `user_id`, `name`, `account_type`, `customer_type`, and `driver_id`. Company/staff accounts are included in Transactions when present; customer subtype comes from the current tenant membership and is null for non-customers.
- `type` is the raw persisted ledger type. `amount` is the signed saved amount. `direction` is `credit` or `debit`; zero amounts and `currency_adjustment`, `frozen`, and `unfrozen` rows have null direction. The amount-range filter uses the absolute magnitude.
- `currency_code` is the saved transaction currency, or null when unavailable. It is not reconstructed from the tenant's current currency setting.
- `description` is the stored description. `reason`, `reference`, and `notes` are separate saved metadata values, null when absent. Manual reference is distinct from `delivery.external_reference_id`.
- `is_manual` is true only for saved sources `manual_credit`, `manual_debit`, and `customer_manual_adjustment`, including manual rows with a linked delivery. `manual_only=true` fixes that scope regardless of other filters.
- `recorded_by` is `{id, name}` when saved actor context exists, otherwise null. A saved actor-name snapshot is preferred; older rows with only a saved actor ID can use that user's current profile name. Saved `processed_by_user_id` is used when `actor_user_id` is absent (for example, payouts). Missing actors are not inferred from the wallet owner or labelled as System.
- `source_options`, `type_options`, and `currency_options` contain sorted distinct saved values from the full current tenant/tab scope, independently of column filters, search, and pagination, so options remain available for an empty filtered result. Source/type filters are exact, case-sensitive raw strings, not a closed enum.
- `delivery` is null or `{id, delivery_code, barcode, external_reference_id}`. `balance_snapshot` is the saved balance after that entry, or null.
- This endpoint does not create adjustments or alter wallets. Existing driver/customer manual-transaction actions remain on their detail pages.

### Frontend integration contract

Both tabs call this endpoint. Transactions sends `manual_only=false`; Adjustments always sends `manual_only=true`. Use the existing grid's column filters and reset to page 1 when filters, search, or page size change. Selecting a customer subtype selects Customer; switching to another account type clears the customer subtype. Render stored currency alongside the amount and preserve reference/notes in the table or transaction details. Link Driver accounts with `account.driver_id`, Customer accounts with `account.user_id`, and linked deliveries with `delivery.id`; do not send Company/Staff accounts to a customer detail page. No transaction action values are returned by this read endpoint.

### Errors
- `400 invalid_query`: malformed tenant UUID or invalid query/column filters. The localized `message` summarizes the first issue; `details` contains `{field, message}` for each issue. For example, `column_filters.customer_type` conflicts with an explicit Driver account filter.
- `400 tenant_id_required`: no tenant was resolved from the authorized context.
- `401`: missing authentication; the authentication middleware returns a localized message. Invalid or inactive authentication can return `403`.
- `403 insufficient_permissions`: the authenticated user lacks finance access or attempted a tenant selection outside the allowed context.
- `405 method_not_allowed`: this route accepts GET only; `Allow: GET` is returned.
- `500 internal_server_error`: the ledger query failed; no partial list is returned.
Frontend clients should show the localized message and use the stable `code` and `details[].field` values for handling; do not branch on translated message text. Do not render a read failure as an empty successful table.

### Performance

The handler uses one read-only RPC for the ledger result. Ownership, filters, matching-row count, sorting, page selection, and source options run in the database, with no per-row API lookups or frontend filtering of a partial page. Pagination and its total share one database snapshot.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/wallets/tenant-transactions?tenant_id={{tenant_id}}&manual_only=false&page=1&limit=25&sort_by=created_at&sort_order=desc&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | Optional tenant UUID; ordinary viewers and tenant-scoped API keys must match authenticated tenant context. Authorized system admins may select a tenant. optional tenant UUID. Defaults to the authenticated tenant. Required when an authorized system administrator has no tenant context; otherwise it must follow the tenant-selection rules above. | {{tenant_id}} |
| manual_only | query string |  | false: all wallet entries; true: saved manual driver/customer adjustments only. `false` by default for Transactions; `true` for Adjustments. | false |
| page | query string |  | Positive page number. Default 1. positive integer, default `1`. | 1 |
| limit | query string |  | Rows per page. Default 25, maximum 100. positive integer, default `25`, maximum `100`. | 25 |
| sort_by | query string |  | created_at, amount, or account_name. `created_at` (default), `amount` (absolute magnitude), or `account_name` (case-insensitive). Ties use `created_at DESC`, then `id DESC`. | created_at |
| sort_order | query string |  | asc or desc. `desc` (default) or `asc`. | desc |
| search | query string | No | Literal case-insensitive text search. optional literal, case-insensitive text search across account name, description, reason, actor name, reference, notes, delivery code, and delivery external reference. |  |
| column_filters | query string | No | JSON column filters; see the documented schema. Apply before pagination. optional JSON object; URL-encode it once through `URLSearchParams` or the equivalent client query builder. Every populated filter is combined with AND and applied before pagination. | {"account_type":"customer","customer_type":"business","direction":"credit","amount":{"min":10,"max":100}} |
| language | query string |  | Response language: en, ar, fa, ku. response localization language (`en`, `ar`, `fa`, `ku`). | en |



### Example column_filters

```json
{
  "account_type": "customer",
  "customer_type": "business",
  "direction": "credit",
  "amount": {
    "min": 10,
    "max": 100
  },
  "created_at": {
    "from": "2026-10-01",
    "to": "2026-10-17"
  }
}
```

### Response 200 Success - Manual adjustment with reference and notes

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Wallet transactions retrieved successfully",
  "data": {
    "transactions": [
      {
        "id": "10000000-0000-4000-8000-000000000001",
        "wallet_id": "20000000-0000-4000-8000-000000000001",
        "account": {
          "user_id": "30000000-0000-4000-8000-000000000001",
          "name": "Example Driver",
          "account_type": "driver",
          "customer_type": null,
          "driver_id": "40000000-0000-4000-8000-000000000001"
        },
        "type": "earning",
        "direction": "credit",
        "amount": 50,
        "currency_code": "USD",
        "description": "Manual credit: Performance bonus - Approved for September",
        "reason": "Performance bonus",
        "reference": "BONUS-2026-09",
        "notes": "Approved for September",
        "source": "manual_credit",
        "is_manual": true,
        "recorded_by": {
          "id": "50000000-0000-4000-8000-000000000001",
          "name": "Example Administrator"
        },
        "created_at": "2026-10-17T10:00:00Z",
        "balance_snapshot": 250,
        "delivery": null
      }
    ],
    "source_options": [
      "manual_credit"
    ],
    "pagination": {
      "page": 1,
      "limit": 25,
      "total": 1,
      "total_pages": 1
    },
    "type_options": [
      "earning"
    ],
    "currency_options": [
      "USD"
    ]
  }
}
```

### Response 400 Error - Conflicting customer-type column filter

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "invalid_query",
  "message": "Select Customers when filtering by customer type.",
  "details": [
    {
      "field": "column_filters.customer_type",
      "message": "Select Customers when filtering by customer type."
    }
  ]
}
```

### Response 400 Error - Missing tenant context

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "tenant_id_required",
  "message": "Tenant ID is required"
}
```

### Response 401 Error - Missing authentication

Unauthorized

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unauthorized",
  "data": null
}
```

### Response 403 Error - Insufficient finance permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_permissions",
  "message": "Insufficient Permissions"
}
```

### Response 405 Error - Method not allowed

Method Not Allowed

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| Allow | header string |  |  | GET |



```json
{
  "success": false,
  "code": "method_not_allowed",
  "message": "Method not allowed"
}
```

### Response 500 Error - Ledger read failed

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "internal_server_error",
  "message": "Internal Server Error"
}
```

### Response 403 Error - Tenant selection outside authorized context

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_permissions",
  "message": "Insufficient Permissions"
}
```

## GET /warehousing/inventory-audits — List Inventory Audits

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-audits#get-list-inventory-audits)

Retrieves inventory audit records with comprehensive filtering options. Returns optimized list without audit_items and discrepancies for performance. Use Get Single Inventory Audit for complete details.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Business Rules
- `statusCounts` and `stats` use the same tenant, warehouse, audit type, date, search, notes, creator, and completer filters as the list.
- Only the selected `status` filter is excluded from counters, so all tab counts remain available.
- Counters are grouped in PostgreSQL and are not calculated by downloading all audit rows into the Edge Function.

### Frontend integration contract
- Continue sending the selected warehouse ID as `warehouse_id`.
- Use `statusCounts.total`, `scheduled`, `in_progress`, `completed`, and `cancelled` directly for the tabs.

### Response
- Returns a paginated list of inventory audit records with basic information
- Excludes audit_items and discrepancies for performance optimization
- Includes user information for audit creation and completion
- Provides pagination information (total count, current page, limit)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-audits?tenant_id={{tenant_id}}&warehouse_id=&audit_type=&status=&scheduled_date=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| warehouse_id | query string | No | Filter by warehouse (optional) |  |
| audit_type | query string | No | Filter by audit type (cycle_count, full_count, spot_check) (optional) |  |
| status | query string | No | Filter by audit status (scheduled, in_progress, completed, cancelled) (optional) |  |
| scheduled_date | query string | No | Filter by scheduled date (YYYY-MM-DD) (optional) |  |
| page | query string | No | Page number for pagination (optional) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional) Number of items per page (default: 20) | 20 |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory audits retrieved successfully",
  "audits": [
    {
      "id": "audit-uuid-1",
      "created_by_user": {
        "id": "user-uuid-1",
        "full_name": "John Doe"
      },
      "audit_type": "cycle_count",
      "status": "scheduled",
      "created_at": "2023-07-01T10:00:00Z",
      "discrepancy_count": 0,
      "notes": "Monthly cycle count for electronics section",
      "scheduled_date": "2023-07-15",
      "completed_by_user": null,
      "completion_date": null,
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Main Warehouse",
        "code": "WH001"
      }
    },
    {
      "id": "audit-uuid-2",
      "created_by_user": {
        "id": "user-uuid-1",
        "full_name": "John Doe"
      },
      "audit_type": "spot_check",
      "status": "completed",
      "created_at": "2023-06-19T14:30:00Z",
      "discrepancy_count": 1,
      "notes": "Spot check due to reported discrepancy",
      "scheduled_date": "2023-06-20",
      "completed_by_user": {
        "id": "user-uuid-2",
        "full_name": "Jane Smith"
      },
      "completion_date": "2023-06-20",
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Main Warehouse",
        "code": "WH001"
      }
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 2,
  "stats": {
    "total": 2,
    "scheduled": 1,
    "in_progress": 0,
    "completed": 1,
    "cancelled": 0
  },
  "statusCounts": {
    "total": 2,
    "scheduled": 1,
    "in_progress": 0,
    "completed": 1,
    "cancelled": 0
  }
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /warehousing/inventory-audits/{{audit_id}} — Get Single Inventory Audit

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-audits#get-get-single-inventory-audit)

Retrieves a single inventory audit record with complete details including audit_items and discrepancies. Essential for Conduct Audit Screen and Audit Summary Screen.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns complete audit details including all audit items and discrepancies
- Every line includes an `item` object with `name` and `sku`, plus a `bin` object with `name` and `code`; IDs remain machine identifiers only
- Includes `progress` with counted, uncounted, no-difference, shortage, overage, write-off, and marker-only totals
- Includes detailed user information for audit creation and completion
- Includes full warehouse information

### Large audits and read failures
- Item, bin, and exception details cover the complete audit scope, including audits above 1,000 entries; lookups do not use an ID list in the URL or a capped table response.
- If the reference-data lookup fails, GET returns HTTP 500 with the existing localized internal-server-error message instead of a successful response with unloaded details.
- Frontend integration is unchanged: use each line's `item.name`, `item.sku`, `bin.name`, and `bin.code` for audit-scoped display and search.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-audits/{{audit_id}}?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| audit_id | path string | Yes | UUID of the audit record |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory audit retrieved successfully",
  "audit": {
    "id": "audit-uuid-1",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "audit_type": "cycle_count",
    "status": "in_progress",
    "scheduled_date": "2023-07-15",
    "completed_date": null,
    "created_by": "user-uuid-1",
    "completed_by": null,
    "audit_items": [
      {
        "item_id": "item-uuid-1",
        "bin_id": "bin-uuid-1",
        "expected_quantity": 100,
        "actual_quantity": 98,
        "discrepancy": -2,
        "notes": "Found 2 items missing"
      },
      {
        "item_id": "item-uuid-2",
        "bin_id": "bin-uuid-2",
        "expected_quantity": 50,
        "actual_quantity": null,
        "discrepancy": null,
        "notes": null
      }
    ],
    "discrepancies": [
      {
        "item_id": "item-uuid-1",
        "bin_id": "bin-uuid-1",
        "expected_quantity": 100,
        "actual_quantity": 98,
        "discrepancy": -2,
        "notes": "Found 2 items missing"
      }
    ],
    "discrepancy_count": 1,
    "notes": "Monthly cycle count for electronics section",
    "created_at": "2023-07-01T10:00:00Z",
    "updated_at": "2023-07-15T09:30:00Z",
    "created_by_user": {
      "id": "user-uuid-1",
      "full_name": "John Doe",
      "email": "john.doe@example.com"
    },
    "completed_by_user": null,
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001",
      "address": "123 Warehouse St, Industrial District"
    }
  }
}
```

### Response 404 Error - Audit Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Audit not found"
}
```

### Response 409 Error - Audit ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Audit ID required"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## PUT /warehousing/inventory-audits/{{audit_id}}/record-count — Record Item Count in Audit

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-audits#put-record-item-count-in-audit)

Atomically records one or many item counts. This endpoint is the Save Count and Continue write boundary.

- Requires valid authentication token and warehouse mutation access.

### Business Rules
- `actual_quantity` is a non-negative whole number.
- The server locks the audit and submitted stock scopes and compares the count with current book stock.
- A shortage requires `exception_type`; use `GET /warehousing/exceptions/available` and show every returned tenant-defined type.
- A type with `write_off_stock=true` writes off the shortage through the warehouse-exception owner, clamps invalid reservations, and creates `stock_adjustment` transactions linked to this audit.
- A type with `write_off_stock=false` creates a marker-only exception and leaves stock and reservations unchanged.
- No exception type is accepted when the result is not a shortage. Positive discrepancies are recorded without changing stock.
- Every counted stock row is stamped with `last_counted_at`. Exact retries are idempotent. Correcting a saved shortage resolves its prior audit-linked exception and creates a replacement only if the corrected count remains short.
- The full batch is all-or-nothing and the same item/bin may appear only once.

### Response
- Returns item and bin objects with names/codes, discrepancy exception fields, and `progress` counts for counted, uncounted, shortage, overage, write-off, and marker-only lines.

### Important errors
- `inventory_audit_exception_type_required` (400)
- `inventory_audit_exception_type_invalid` (400)
- `warehouse_write_off_stock_not_available` (409)
- `inventory_audit_item_ambiguous` (409)
- A reference-data lookup failure after the mutation has committed does not turn the successful write into an error. Count state and progress remain authoritative; a subsequent detail GET can reload display details.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/inventory-audits/{{audit_id}}/record-count?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| audit_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| item_counts | array<object> |  |  |  |
| item_counts.item_id | string |  |  |  |
| item_counts.bin_id | string |  |  |  |
| item_counts.actual_quantity | number |  |  |  |
| item_counts.exception_type | string |  |  |  |
| item_counts.notes | string |  |  |  |



### Request body (raw)

```json
{
  "item_counts": [
    {
      "item_id": "{{item_id}}",
      "bin_id": "{{bin_id}}",
      "actual_quantity": 0,
      "exception_type": "missing",
      "notes": "Expected parcel is missing"
    },
    {
      "item_id": "item-uuid-2",
      "bin_id": "bin-uuid-2",
      "actual_quantity": 52,
      "notes": "Found 2 extra items"
    }
  ]
}
```

### Request example 1

```json
{
  "item_counts": [
    {
      "item_id": "uuid",
      "bin_id": "uuid",
      "actual_quantity": 0,
      "exception_type": "missing",
      "notes": "Optional note"
    }
  ]
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Item counts recorded successfully",
  "audit": {
    "id": "audit-uuid-1",
    "status": "in_progress",
    "audit_items": [
      {
        "item_id": "item-uuid-1",
        "bin_id": "bin-uuid-1",
        "expected_quantity": 1,
        "book_quantity_at_count": 1,
        "actual_quantity": 0,
        "discrepancy": -1,
        "exception_type": "missing",
        "warehouse_exception_id": "exception-uuid",
        "stock_written_off": true,
        "write_off_status": "written_off",
        "counted_by": "user-uuid",
        "counted_at": "2026-09-10T07:00:00Z",
        "item": {
          "id": "item-uuid-1",
          "name": "Ministry of Health",
          "sku": "STAGE-3071-1"
        },
        "bin": {
          "id": "bin-uuid-1",
          "name": "Bin A1",
          "code": "A1"
        }
      }
    ],
    "progress": {
      "total_items": 2,
      "counted_items": 2,
      "uncounted_items": 0,
      "shortage_items": 1,
      "overage_items": 1,
      "write_off_items": 1,
      "marker_only_items": 0
    }
  }
}
```

### Response 400 Error - Item Counts Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: item_counts"
}
```

### Response 400 Error - Invalid Item Count Structure



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid item count structure"
}
```

### Response 400 Error - Shortage Requires Exception Type



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "inventory_audit_exception_type_required",
  "message": "Select a warehouse exception type for every shortage"
}
```

### Response 404 Error - Item Not Found In Audit



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found in audit (item_id: item-uuid, bin_id: bin-uuid)"
}
```

### Response 400 Error - Cannot Record Counts For Completed Audit



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Cannot record counts for completed audit"
}
```

### Response 404 Error - Audit Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Audit not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## PUT /warehousing/inventory-audits/{{audit_id}}/complete — Complete Inventory Audit

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-audits#put-complete-inventory-audit)

Finalizes an inventory audit. Completion is a validation-only state transition and never adjusts stock.

- Requires valid authentication token and warehouse mutation access.

The legacy `apply_stock_adjustments` field is accepted and ignored, regardless of its value. Omitted, true, and false all use the same completion behavior. Older mobile clients do not need to change their request; updated clients should omit the field and remove the checkbox. Stock behavior was already determined by each shortage's selected warehouse exception type during Record Count.

### Business Rules
- Every audit item must have `actual_quantity`.
- Every shortage must have `warehouse_exception_id`.
- If items remain, HTTP 409 returns code `inventory_audit_has_uncounted_items` and `remaining_count`. The audit stays resumable.
- `inventory_audit_shortage_missing_exception` is returned if a shortage has no exception.
- Sets status, `completion_date`, and `completed_by` only after validation succeeds.
- The response keeps item/bin names and returns `progress`; marker-only shortages are not described as stock adjustments.
- A reference-data lookup failure after the mutation has committed does not turn the successful write into an error. Count state and progress remain authoritative; a subsequent detail GET can reload display details.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/inventory-audits/{{audit_id}}/complete?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| audit_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| completion_notes | string |  |  |  |



### Request body (raw)

```json
{
  "completion_notes": "All items counted and reviewed."
}
```

### Request example 1

```json
{
  "completion_notes": "string"
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory audit completed successfully",
  "audit": {
    "id": "audit-uuid-1",
    "status": "completed",
    "completed_by": "user-uuid-2",
    "completion_date": "2026-09-10T07:30:00Z",
    "progress": {
      "total_items": 403,
      "counted_items": 403,
      "uncounted_items": 0,
      "shortage_items": 2,
      "overage_items": 0,
      "write_off_items": 1,
      "marker_only_items": 1
    }
  }
}
```

### Response 409 Error - Audit Already Completed



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "inventory_audit_already_completed",
  "message": "Audit already completed"
}
```

### Response 409 Error - Items Remain Uncounted



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "inventory_audit_has_uncounted_items",
  "message": "Count all remaining items before completing the audit",
  "remaining_count": 402
}
```

### Response 409 Error - Cannot Complete Cancelled Audit



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "inventory_audit_cancelled",
  "message": "Cannot complete cancelled audit"
}
```

### Response 404 Error - Audit Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Audit not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## POST /warehousing/inventory-audits — Create Inventory Audit

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-audits#post-create-inventory-audit)

Creates a new inventory audit, which is a planned verification of actual stock against recorded quantities. Supports different audit types with varying scopes and item selection methods.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Warehouse must exist and belong to the specified tenant
- Audit type must be one of: cycle_count, full_count, or spot_check
- Scheduled date must be a valid date
- Items behavior depends on audit type:
  - cycle_count: Automatically selects 20% of inventory items randomly
  - full_count: Includes all inventory items in the warehouse
  - spot_check: Uses only the specific items provided in the request
- For each audit item:
  - Item must exist in inventory
  - Bin must exist in the specified warehouse
  - Bin must be of location_type "bin"
  - Expected quantity defaults to current stock level if not provided
- New audits are created with status "scheduled"

### Large audit scope
- Automatic full counts read every stock row in the tenant/warehouse before grouping by item and bin; cycle counts sample from that complete grouped scope.
- Explicit item/bin requests validate all submitted IDs and include every matching stock row when calculating expected quantities, including scopes above 1,000 rows.
- Existing request and response fields, expected-quantity overrides, and stock behavior are unchanged.
- A reference-data lookup failure after the mutation has committed does not turn the successful write into an error. Count state and progress remain authoritative; a subsequent detail GET can reload display details.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/inventory-audits?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| audit_type | string |  |  |  |
| scheduled_date | string |  |  |  |
| audit_items | array<object> |  |  |  |
| audit_items.item_id | string |  |  |  |
| audit_items.bin_id | string |  |  |  |
| audit_items.expected_quantity | number |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "audit_type": "spot_check",
  "scheduled_date": "2023-07-15",
  "audit_items": [
    {
      "item_id": "{{item_id}}",
      "bin_id": "{{bin_id}}",
      "expected_quantity": 100
    }
  ],
  "notes": "Spot check for high-value electronics"
}
```

### Request example 1

```json
{
  "warehouse_id": "uuid", // required
  "audit_type": "string", // required (one of: "cycle_count", "full_count", "spot_check")
  "scheduled_date": "string", // required, date in YYYY-MM-DD format
  "audit_items": [ // optional for cycle_count and full_count, required for spot_check
    {
      "item_id": "uuid", // required
      "bin_id": "uuid", // required
      "expected_quantity": number // optional, defaults to current stock quantity
    }
  ],
  "notes": "string" // optional
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory audit created successfully",
  "audit": {
    "id": "audit-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "audit_type": "spot_check",
    "status": "scheduled",
    "scheduled_date": "2023-07-15",
    "completed_date": null,
    "created_by": "user-uuid",
    "completed_by": null,
    "audit_items": [
      {
        "item_id": "item-uuid",
        "bin_id": "bin-uuid",
        "expected_quantity": 100,
        "actual_quantity": null,
        "discrepancy": null,
        "notes": null
      }
    ],
    "discrepancies": [],
    "discrepancy_count": 0,
    "notes": "Spot check for high-value electronics",
    "created_at": "2023-07-01T10:00:00Z",
    "updated_at": "2023-07-01T10:00:00Z"
  }
}
```

### Response 400 Error - Field Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: warehouse_id"
}
```

### Response 400 Error - Invalid Audit Type



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid audit type"
}
```

### Response 400 Error - Invalid Scheduled Date



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid scheduled date"
}
```

### Response 400 Error - Audit Items Must Be Array



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Audit items must be array"
}
```

### Response 400 Error - Audit Item Missing Fields



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Audit item must have item_id and bin_id"
}
```

### Response 400 Error - Spot Check Requires Audit Items



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Spot check requires audit items"
}
```

### Response 404 Error - Item Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found (item_id: item-uuid)"
}
```

### Response 404 Error - Bin Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Bin not found (bin_id: bin-uuid)"
}
```

### Response 400 Error - Location Not A Bin



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Location not a bin (bin_id: bin-uuid)"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 500 Error - Error Fetching Stock Items



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Error fetching stock items"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/handoffs — List Package Handoffs

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/package-handoffs-management#get-list-package-handoffs)

Retrieves package handoffs based on specified filter criteria. Supports pagination and filtering by warehouse, delivery ID, driver ID, warehouse staff ID, and status.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns a paginated list of package handoffs
- Includes related entities such as warehouse, picking order, and warehouse staff information
- Includes a `status_matrix` summary for the current non-status filters
- Provides pagination information (total count, current page, limit)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/handoffs?tenant_id={{tenant_id}}&warehouse_id=&delivery_id=&driver_id=&warehouse_staff_id=&status=&search=&page=1&limit=10

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| warehouse_id | query string | No | Filter by warehouse ID (optional) |  |
| delivery_id | query string | No | Filter by delivery ID (optional) |  |
| driver_id | query string | No | Filter by driver ID (optional) |  |
| warehouse_staff_id | query string | No | Filter by warehouse staff ID (optional) |  |
| status | query string | No | Filter by handoff status: pending, in_progress, completed (optional) Filter by handoff status (pending, in_progress, completed) |  |
| search | query string | No | Search term to filter by status (optional) |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 10) Number of items per page (default: 10) | 10 |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Handoffs retrieved successfully",
  "handoffs": [
    {
      "id": "12345678-1234-1234-1234-123456789012",
      "tenant_id": "tenant-uuid",
      "warehouse_id": "warehouse-uuid",
      "delivery_id": "delivery-uuid",
      "picking_order_id": "picking-order-uuid",
      "warehouse_staff_id": "staff-uuid",
      "driver_id": "driver-uuid",
      "status": "completed",
      "signature_url": "https://example.com/signatures/sig123.png",
      "notes": "Handoff completed without issues",
      "initiated_at": "2023-05-15T14:30:00Z",
      "completed_at": "2023-05-15T14:45:00Z",
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Main Warehouse",
        "code": "WH001"
      },
      "picking_order": {
        "id": "picking-order-uuid",
        "reference_number": "PO-12345"
      },
      "warehouse_staff": {
        "id": "staff-uuid",
        "user_id": {
          "id": "user-uuid",
          "full_name": "John Smith"
        }
      }
    }
  ],
  "status_matrix": {
    "pending": 4,
    "in_progress": 2,
    "completed": 8,
    "total": 14
  },
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /warehousing/handoffs — Create Package Handoff

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/package-handoffs-management#post-create-package-handoff)

Assignment owns prepared P&D handoff creation. If an eligible P&D assignment already created the pending handoff, matching warehouse and picking-order input returns that same handoff with HTTP 200 and may fill its previously empty warehouse staff. This idempotent result also applies while driver acceptance is `pending`. Non-P&D legacy creation keeps its existing rules and returns HTTP 201 for a new record.

Records or returns a package handoff from warehouse staff to a delivery driver.

- User must have `manage:operations:tenant` permission

### Business Rules
- `warehouse_id`, `delivery_id`, `picking_order_id`, and `warehouse_staff_id` are required and tenant-scoped.
- The picking order must be `completed`.
- For prepared P&D/operational-return work, assignment is the handoff owner. A matching request returns the existing pending handoff with HTTP 200, including when the assignment is still `pending`; warehouse, picking order, assignment, and driver must match current preparation.
- For non-P&D legacy creation, the delivery assignment must be `assigned`, `accepted`, or `en_route_to_pickup`, no active handoff may already exist, and a new pending record returns HTTP 201.
- `driver_id` is resolved from the current active assignment.
- Pickup or later states are outside the creation window.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/handoffs?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| delivery_id | string |  |  |  |
| picking_order_id | string |  |  |  |
| warehouse_staff_id | string |  |  |  |
| driver_id | string |  |  |  |
| signature_url | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "warehouse-uuid",
  "delivery_id": "delivery-uuid",
  "picking_order_id": "picking-order-uuid",
  "warehouse_staff_id": "staff-uuid",
  "driver_id": "driver-uuid",
  "signature_url": "https://example.com/signatures/sig123.png",
  "notes": "Package ready for handoff"
}
```

### Request example 1

```json
{
  "warehouse_id": "uuid",
  "delivery_id": "uuid",
  "picking_order_id": "uuid",
  "warehouse_staff_id": "uuid",
  "driver_id": "uuid",
  "signature_url": "string",
  "notes": "string"
}
```

### Response 200 Success - Existing P&D Handoff

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Handoff created successfully",
  "handoff": {
    "id": "existing-handoff-uuid",
    "delivery_id": "delivery-uuid",
    "picking_order_id": "picking-order-uuid",
    "assignment_id": "assignment-uuid",
    "driver_id": "driver-uuid",
    "warehouse_staff_id": "staff-uuid",
    "status": "pending"
  }
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Handoff created successfully",
  "handoff": {
    "id": "12345678-1234-1234-1234-123456789012",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "delivery_id": "delivery-uuid",
    "picking_order_id": "picking-order-uuid",
    "warehouse_staff_id": "staff-uuid",
    "driver_id": "driver-uuid",
    "status": "pending",
    "signature_url": null,
    "notes": "Package ready for handoff",
    "initiated_at": "2023-05-15T14:30:00Z",
    "completed_at": null
  }
}
```

### Response 400 Error - Missing Required Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: warehouse_id"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Delivery Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery not found"
}
```

### Response 404 Error - Picking Order Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Picking order not found"
}
```

### Response 400 Error - Picking Order Not Completed



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Picking order not completed"
}
```

### Response 400 Error - Handoff Already Exists



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Handoff already exists for delivery"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 400 Error - Invalid Request Body



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request body"
}
```

## GET /warehousing/handoffs/:id — Get Package Handoff

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/package-handoffs-management#get-get-package-handoff)

Retrieves detailed information about a specific package handoff including related entities such as warehouse, picking order, warehouse staff, driver, and delivery information.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns detailed information about the specified handoff
- Includes related entities information (warehouse, picking order, staff, driver)
- Includes delivery details such as delivery code, pickup/drop-off locations, and customer information

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/handoffs/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the package handoff (required) | handoff-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) UUID of the tenant (required if not in auth context) | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Handoff retrieved successfully",
  "handoff": {
    "id": "12345678-1234-1234-1234-123456789012",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "delivery_id": "delivery-uuid",
    "picking_order_id": "picking-order-uuid",
    "warehouse_staff_id": "staff-uuid",
    "driver_id": "driver-uuid",
    "status": "completed",
    "signature_url": "https://example.com/signatures/sig123.png",
    "notes": "Handoff completed without issues",
    "initiated_at": "2023-05-15T14:30:00Z",
    "completed_at": "2023-05-15T14:45:00Z",
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    },
    "picking_order": {
      "id": "picking-order-uuid",
      "reference_number": "PO-12345"
    },
    "warehouse_staff": {
      "id": "staff-uuid",
      "user_id": {
        "id": "user-uuid",
        "full_name": "John Smith"
      }
    },
    "driver": {
      "id": "driver-uuid",
      "user_id": {
        "id": "user-uuid",
        "full_name": "Jane Doe"
      }
    },
    "delivery": {
      "id": "delivery-uuid",
      "delivery_code": "DLV-000123",
      "status": "assigned",
      "pickup": {
        "id": "pickup-location-uuid",
        "full_address": "123 Warehouse Street, City, State 12345"
      },
      "drop_off": {
        "id": "dropoff-location-uuid",
        "full_address": "456 Customer Avenue, City, State 67890"
      },
      "customer": {
        "id": "customer-uuid",
        "full_name": "John Customer"
      }
    }
  }
}
```

### Response 404 Error - Handoff Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Handoff not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## PUT /warehousing/handoffs/:id — Update Package Handoff

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/package-handoffs-management#put-update-package-handoff)

Updates an existing package handoff with new information such as status, driver assignment, signature, or notes. Special validation rules apply when marking handoffs as completed.

- User must have 'manage:operations:tenant' permission

### Business Rules
- At least one field must be provided for update
- Valid status values are: pending, in_progress, completed
- If driver_id is provided, the driver must exist and belong to the tenant
- A lifecycle-managed P&D/operational-return handoff has `assignment_id`. Do not send `status=completed` for it; the assigned driver's Pickup action completes the handoff atomically with stock and custody. Manual completion returns HTTP 409 `pnd_handoff_completed_by_driver_pickup`.
- **Special rules for legacy 'completed' status:**
  - delivery_id must be present in the handoff record
  - The delivery must have a driver assignment in a pre-pickup state: `assigned`, `accepted`, or `en_route_to_pickup`
  - If the delivery is already `picked_up` or beyond, the handoff can no longer be completed through this warehouse step
  - signature_url is mandatory for completion
  - driver_id will be automatically set from the delivery assignment
- If status is changed to 'completed', the completed_at timestamp is set automatically

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/handoffs/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the package handoff to update (required) | handoff-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) UUID of the tenant (required if not in auth context) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| driver_id | string |  |  |  |
| signature_url | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "status": "completed",
  "driver_id": "driver-uuid",
  "signature_url": "https://example.com/signatures/sig123.png",
  "notes": "Updated notes for the handoff"
}
```

### Request example 1

```json
{
  "status": "string", // optional (pending, in_progress, completed)
  "driver_id": "uuid", // optional - auto-set when status becomes completed
  "signature_url": "string", // optional - required for completed status
  "notes": "string" // optional
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Handoff updated successfully",
  "handoff": {
    "id": "12345678-1234-1234-1234-123456789012",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "delivery_id": "delivery-uuid",
    "picking_order_id": "picking-order-uuid",
    "warehouse_staff_id": "staff-uuid",
    "driver_id": "driver-uuid",
    "status": "completed",
    "signature_url": "https://example.com/signatures/sig123.png",
    "notes": "Updated notes for the handoff",
    "initiated_at": "2023-05-15T14:30:00Z",
    "completed_at": "2023-05-15T15:30:00Z"
  }
}
```

### Response 404 Error - Handoff Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Handoff not found"
}
```

### Response 404 Error - Driver Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Driver not found"
}
```

### Response 400 Error - Invalid Status



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid status"
}
```

### Response 400 Error - No Fields to Update



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "No fields to update"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 400 Error - Delivery ID Required for Completion



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery ID required for completion"
}
```

### Response 400 Error - Delivery Not Assigned to Driver



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery not assigned to driver"
}
```

### Response 400 Error - Signature URL Required for Completion



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Signature URL required for completion"
}
```

### Response 409 Error - P&D Handoff Completes During Driver Pickup



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "pnd_handoff_completed_by_driver_pickup",
  "message": "This handoff is completed automatically when the assigned driver picks up the parcel"
}
```

### Response 400 Error - Invalid Request Body



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request body"
}
```

## GET /warehousing/staff — List Warehouse Staff

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-staff#get-list-warehouse-staff)

Retrieves staff assigned to warehouses with optional filtering. Standalone params (warehouse_id, role, status, user_id, search) are kept for backward compatibility with form-modal callers. New list-page callers use column_filters + sort_by + sort_order. Response includes a stats block for KPI cards.

### column_filters example

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/staff?tenant_id={{tenant_id}}&warehouse_id=&role=&status=&user_id=&search=&sort_by=created_at&sort_order=desc&column_filters=&page=1&limit=20&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth context) | {{tenant_id}} |
| warehouse_id | query string | No | Legacy: filter by warehouse UUID (optional) |  |
| role | query string | No | Legacy: filter by role — manager \| supervisor \| picker \| receiver (optional) |  |
| status | query string | No | Legacy: filter by status — active \| inactive \| on_leave (optional) |  |
| user_id | query string | No | Legacy: filter by specific user UUID (optional) |  |
| search | query string | No | Full-text search across full_name, email, phone (optional) |  |
| sort_by | query string |  | Sort column — created_at (only supported value, default: created_at) | created_at |
| sort_order | query string | No | Sort direction — asc \| desc (optional, default: desc) | desc |
| column_filters | query string |  | JSON object of column filters, AND-combined. Supported keys: warehouse_id (exact), role (exact), status (exact), created_at ({from, to} date range). AND-combined with standalone params if same key is present. |  |
| page | query string | No | Page number for pagination (optional, default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 20) | 20 |
| language | query string | No | Response language (optional, default: en) | en |



### column_filters example

```json
{
  "warehouse_id": "uuid",
  "role": "manager",
  "status": "active",
  "created_at": {
    "from": "2025-01-01",
    "to": "2025-12-31"
  }
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse staff retrieved successfully",
  "staff": [
    {
      "id": "staff-uuid-1",
      "tenant_id": "tenant-uuid",
      "warehouse_id": "warehouse-uuid-1",
      "user_id": "user-uuid-1",
      "role": "manager",
      "status": "active",
      "created_at": "2024-06-01T10:00:00Z",
      "updated_at": "2024-06-01T10:00:00Z",
      "warehouse": {
        "id": "warehouse-uuid-1",
        "name": "Main Distribution Center",
        "code": "WH001",
        "address_id": "address-uuid"
      },
      "user": {
        "id": "user-uuid-1",
        "email": "john.doe@example.com",
        "phone": "+15551234567",
        "full_name": "John Doe",
        "first_name": "John",
        "last_name": "Doe",
        "avatar_url": null
      }
    },
    {
      "id": "staff-uuid-2",
      "tenant_id": "tenant-uuid",
      "warehouse_id": "warehouse-uuid-1",
      "user_id": "user-uuid-2",
      "role": "picker",
      "status": "on_leave",
      "created_at": "2024-05-15T08:30:00Z",
      "updated_at": "2024-05-15T08:30:00Z",
      "warehouse": {
        "id": "warehouse-uuid-1",
        "name": "Main Distribution Center",
        "code": "WH001",
        "address_id": "address-uuid"
      },
      "user": {
        "id": "user-uuid-2",
        "email": "jane.smith@example.com",
        "phone": "+15559876543",
        "full_name": "Jane Smith",
        "first_name": "Jane",
        "last_name": "Smith",
        "avatar_url": null
      }
    }
  ],
  "stats": {
    "totalStaff": 15,
    "activeCount": 13,
    "inactiveCount": 1,
    "onLeaveCount": 1
  },
  "page": 1,
  "limit": 20,
  "total": 15
}
```

### Response 200 Success Response — with column_filters + sort

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse staff retrieved successfully",
  "staff": [
    {
      "id": "staff-uuid-1",
      "tenant_id": "tenant-uuid",
      "warehouse_id": "warehouse-uuid-1",
      "user_id": "user-uuid-1",
      "role": "manager",
      "status": "active",
      "created_at": "2025-03-10T09:00:00Z",
      "updated_at": "2025-03-10T09:00:00Z",
      "warehouse": {
        "id": "warehouse-uuid-1",
        "name": "Main Distribution Center",
        "code": "WH001",
        "address_id": "address-uuid"
      },
      "user": {
        "id": "user-uuid-1",
        "email": "john.doe@example.com",
        "phone": "+15551234567",
        "full_name": "John Doe",
        "first_name": "John",
        "last_name": "Doe",
        "avatar_url": null
      }
    }
  ],
  "stats": {
    "totalStaff": 15,
    "activeCount": 13,
    "inactiveCount": 1,
    "onLeaveCount": 1
  },
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 400 Error - Invalid column_filters JSON

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "column_filters",
  "message": "invalid_json"
}
```

### Response 500 Error - Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /warehousing/staff — Assign User as Warehouse Staff

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-staff#post-assign-user-as-warehouse-staff)

Creates a new staff assignment for a user to a warehouse with a specific role. Verifies that the warehouse and user exist and belong to the specified tenant, and that the user is not already assigned to the warehouse.

- User must have 'manage:users:tenant' permission or be a system admin

### Business Rules
- User must be a member of the specified tenant
- Warehouse must exist and belong to the specified tenant
- Role must be one of: "manager", "supervisor", "picker", "receiver"
- Status must be one of: "active", "inactive", "on_leave"
- A user cannot be assigned to the same warehouse more than once

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/staff?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth context) (required if not in auth context): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| user_id | string |  |  |  |
| role | string |  |  |  |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "user_id": "{{user_id}}",
  "role": "manager",
  "status": "active"
}
```

### Request example 1

```json
{
  "warehouse_id": "uuid", // required - UUID of the warehouse
  "user_id": "uuid", // required - UUID of the user to assign
  "role": "string", // required - Role of the user (e.g., "manager", "supervisor", "picker", "receiver")
  "status": "string" // required - Status of the assignment (e.g., "active", "inactive", "on_leave")
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse staff assigned successfully",
  "Staff": {
    "id": "assignment-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid-1",
    "user_id": "user-uuid-1",
    "role": "manager",
    "status": "active",
    "created_at": "2023-04-15T10:30:00Z",
    "updated_at": "2023-04-15T10:30:00Z",
    "warehouse": {
      "id": "warehouse-uuid-1",
      "name": "Main Distribution Center",
      "code": "WH001"
    },
    "user": {
      "id": "user-uuid-1",
      "first_name": "John",
      "last_name": "Doe",
      "email": "john.doe@example.com"
    }
  }
}
```

### Response 400 Error - Missing Required Field

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required role"
}
```

### Response 400 Error - Invalid Role

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid role: invalid-role"
}
```

### Response 400 Error - Invalid Status

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid status: invalid-status"
}
```

### Response 400 Error - User Already Assigned

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "User already assigned to warehouse"
}
```

### Response 404 Error - Warehouse Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - User Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "User not found or not tenant member"
}
```

## PUT /warehousing/staff/:id — Update Warehouse Staff

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-staff#put-update-warehouse-staff)

Updates an existing warehouse staff assignment with new role or status information.

- User must have 'manage:users:tenant' permission or be a system admin

### Business Rules
- Staff assignment must exist and belong to the specified tenant
- At least one field (role or status) must be provided for update
- If role is provided, it must be one of: "manager", "supervisor", "picker", "receiver"
- If status is provided, it must be one of: "active", "inactive", "on_leave"
- Updated timestamp is automatically set to current time

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/staff/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the staff assignment to update (required) | {{staff_assignment_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth context) (required if not in auth context): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| role | string |  |  |  |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "role": "supervisor",
  "status": "on_leave"
}
```

### Request example 1

```json
{
  "role": "string", // optional - New role (e.g., "manager", "supervisor", "picker", "receiver")
  "status": "string" // optional - New status (e.g., "active", "inactive", "on_leave")
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse staff updated successfully",
  "staff": {
    "id": "staff-assignment-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid-1",
    "user_id": "user-uuid-1",
    "role": "supervisor",
    "status": "on_leave",
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-04-15T15:30:00Z",
    "warehouse": {
      "id": "warehouse-uuid-1",
      "name": "Main Distribution Center",
      "code": "WH001",
      "address_id": "address-uuid"
    },
    "user": {
      "id": "user-uuid-1",
      "first_name": "John",
      "last_name": "Doe",
      "email": "john.doe@example.com",
      "phone": "+15551234567",
      "avatar_url": null,
      "full_name": "John Doe"
    }
  }
}
```

### Response 404 Error - Staff Assignment Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Staff assignment not found"
}
```

## GET /warehousing/staff/:staff_id — Get Warehouse Staff by ID (or Own Profile)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-staff#get-get-warehouse-staff-by-id-or-own-profile)

Retrieves warehouse staff details by ID. If the ID belongs to the current user, it returns as 'profile', otherwise as 'staff'. System automatically detects ownership.

- For own profile: any warehouse staff member
- For others: requires 'view:tenant' or 'manage:users:tenant' permission

### Response
- Returns warehouse staff details with user and warehouse information
- Uses 'profile' key if viewing own data, 'staff' key if viewing others

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/staff/:staff_id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| staff_id | path string | Yes | UUID of the warehouse staff record (the API automatically detects if this is your own profile) | {{staff_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth context) (required if not in auth context): UUID of the tenant | {{tenant_id}} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Profile retrieved successfully",
  "profile": {
    "id": "staff-uuid-1",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid-1",
    "user_id": "user-uuid-1",
    "role": "picker",
    "status": "active",
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-01-01T00:00:00Z",
    "warehouse": {
      "id": "warehouse-uuid-1",
      "name": "Main Distribution Center",
      "code": "WH001",
      "address_id": "address-uuid"
    },
    "user": {
      "id": "user-uuid-1",
      "first_name": "John",
      "last_name": "Doe",
      "email": "john.doe@example.com",
      "phone": "+15559876543",
      "avatar_url": "https://example.com/avatars/john.jpg",
      "full_name": "John Doe"
    }
  }
}
```

### Response 404 Error - Profile Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse staff profile not found"
}
```

## PUT /warehousing/staff/:staff_id — Update Warehouse Staff by ID (or Own Profile)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-staff#put-update-warehouse-staff-by-id-or-own-profile)

Updates warehouse staff details by ID. If the ID belongs to the current user, they can update personal info + status. System automatically detects ownership and permissions.

- For own profile: any warehouse staff member (can update personal info + status)
- For others: requires 'manage:users:tenant' permission (can update role + status only)

### Business Rules
- System detects if user is updating own profile vs others
- Own profile: can update user fields + staff status
- Others: admin can update role + status only
- Updates both user and warehouse_staff tables as needed

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/staff/:staff_id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| staff_id | path string | Yes | UUID of the warehouse staff record (the API automatically detects if this is your own profile) | {{staff_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth context) (required if not in auth context): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| first_name | string |  |  |  |
| last_name | string |  |  |  |
| phone | string |  |  |  |
| avatar_url | string |  |  |  |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "first_name": "John",
  "last_name": "Smith",
  "phone": "+15559876543",
  "avatar_url": "https://example.com/new-avatar.jpg",
  "status": "active"
}
```

### Request example 1

```json
{
  "first_name": "string", // optional - user's first name
  "last_name": "string", // optional - user's last name
  "phone": "string", // optional - user's phone number
  "avatar_url": "string", // optional - user's avatar URL
  "status": "string" // optional - staff status ("active", "inactive", "on_leave")
}
```

### Request example 2

```json
{
  "role": "string", // optional - staff role
  "status": "string" // optional - staff status
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Profile updated successfully",
  "profile": {
    "id": "staff-uuid-1",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid-1",
    "user_id": "user-uuid-1",
    "role": "picker",
    "status": "active",
    "created_at": "2023-01-01T00:00:00Z",
    "updated_at": "2023-04-15T16:30:00Z",
    "warehouse": {
      "id": "warehouse-uuid-1",
      "name": "Main Distribution Center",
      "code": "WH001",
      "address_id": "address-uuid"
    },
    "user": {
      "id": "user-uuid-1",
      "first_name": "John",
      "last_name": "Smith",
      "email": "john.doe@example.com",
      "phone": "+15559876543",
      "avatar_url": "https://example.com/avatars/john.jpg",
      "full_name": "John Smith"
    }
  }
}
```

### Response 400 Error - Invalid Status

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid enum value: invalid-status"
}
```

### Response 404 Error - Profile Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse staff profile not found"
}
```

## GET /warehousing/picking-orders — List Picking Orders

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/picking-orders#get-list-picking-orders)

Retrieves a list of picking orders with filtering options. Supports pagination and provides metadata for available filter options.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns a paginated list of picking orders with detailed item information
- Includes information about the creating user and assigned user
- Includes `stats` and `status_matrix` status counts scoped to the tenant and to `warehouse_id` when a warehouse filter is provided; `status` itself is not applied to these counts so the UI can render status tabs for the selected warehouse
- Includes pagination information (total count, current page, limit)

### Frontend integration contract
- When calling this endpoint with `warehouse_id`, use `status_matrix`/`stats` as the counts for that selected warehouse only.
- Use `total` for the current result set after all filters, including `status`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/picking-orders?tenant_id={{tenant_id}}&warehouse_id=&status=&order_type=&priority=&completed=&search=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |
| warehouse_id | query string | No | Filter by specific warehouse UUID (optional) |  |
| status | query string | No | Filter by order status: pending, in_progress, completed, cancelled (optional) Filter by order status (e.g., pending, in_progress, completed, handed_off, cancelled) |  |
| order_type | query string | No | Filter by order type: individual, batch, waybill (optional) Filter by order type (e.g., individual, batch, waybill) |  |
| priority | query string | No | Sort by priority: asc or desc (optional) Filter by priority level (numeric value) |  |
| completed | query string | No | Filter by completion status: true (completed), false (not completed) (optional) |  |
| search | query string | No | Search by reference number (case insensitive) (optional) |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 20, max: 100) Number of items per page (default: 20) | 20 |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Picking orders retrieved successfully",
  "orders": [
    {
      "id": "01234567-89ab-cdef-0123-456789abcdef",
      "tenant_id": "f6a1b2c3-d4e5-4321-8901-f1234567890a",
      "warehouse_id": "w1e2f3a4-b5c6-7890-1234-56789abcdef0",
      "reference_number": "PKO-00001",
      "external_reference": "ORD-2024-001",
      "order_type": "individual",
      "priority": 1,
      "status": "pending",
      "due_date": "2024-01-15T10:00:00Z",
      "completed_date": null,
      "item_details": [
        {
          "item_id": "i1e2f3a4-b5c6-7890-1234-56789abcdef0",
          "bin_id": "l1e2f3a4-b5c6-7890-1234-56789abcdef0",
          "quantity": 10,
          "picked_quantity": 0,
          "status": "pending"
        }
      ],
      "picking_progress": {
        "total_items": 10,
        "picked_items": 0,
        "start_time": null,
        "end_time": null,
        "picker_notes": ""
      },
      "notes": "Express shipment for VIP customer",
      "assigned_to": "u1e2f3a4-b5c6-7890-1234-56789abcdef0",
      "created_by": "admin-uuid-1234",
      "created_at": "2024-01-10T10:00:00Z",
      "updated_at": "2024-01-10T10:00:00Z",
      "created_by_user": {
        "id": "admin-uuid-1234",
        "full_name": "Admin User"
      },
      "assigned_to_user": {
        "id": "u1e2f3a4-b5c6-7890-1234-56789abcdef0",
        "full_name": "John Picker"
      },
      "warehouse": {
        "id": "w1e2f3a4-b5c6-7890-1234-56789abcdef0",
        "name": "Main Warehouse",
        "code": "WH001"
      }
    }
  ],
  "stats": {
    "pending": 1,
    "in_progress": 0,
    "completed": 0,
    "handed_off": 0,
    "cancelled": 0,
    "total": 1
  },
  "status_matrix": {
    "pending": 1,
    "in_progress": 0,
    "completed": 0,
    "handed_off": 0,
    "cancelled": 0,
    "total": 1
  },
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /warehousing/picking-orders — Create Picking Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/picking-orders#post-create-picking-order)

Creates a new picking order with items to be retrieved from inventory. The endpoint performs extensive validation including warehouse existence, item availability, bin locations, and stock quantity.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Warehouse must exist and belong to the specified tenant
- Each item must exist in inventory and be stored in the specified bin
- Requested quantity is calculated only from unreserved stock rows with literal `status=available` and `quantity>0`
- A stock row depleted by a warehouse-exception write-off cannot be offered, picked, or newly reserved
- If assigned_to is provided, user must exist and be a member of the tenant
- Reference number is auto-generated if not provided (format: PKO-XXXXX)
- Order type must be one of: individual, batch, waybill
- Status must be one of: pending, in_progress, completed, cancelled

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/picking-orders?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| reference_number | string |  |  |  |
| external_reference | string |  |  |  |
| order_type | string |  |  |  |
| priority | number |  |  |  |
| status | string |  |  |  |
| item_details | array<object> |  |  |  |
| item_details.item_id | string |  |  |  |
| item_details.bin_id | string |  |  |  |
| item_details.quantity | number |  |  |  |
| item_details.picked_quantity | number |  |  |  |
| item_details.status | string |  |  |  |
| picking_progress | object |  |  |  |
| picking_progress.total_items | number |  |  |  |
| picking_progress.picked_items | number |  |  |  |
| picking_progress.start_time | null |  |  |  |
| picking_progress.end_time | null |  |  |  |
| picking_progress.picker_notes | string |  |  |  |
| notes | string |  |  |  |
| assigned_to | string |  |  |  |
| due_date | string |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "reference_number": "PKO-00012",
  "external_reference": "EXT-12345",
  "order_type": "individual",
  "priority": 1,
  "status": "pending",
  "item_details": [
    {
      "item_id": "{{item_id_1}}",
      "bin_id": "{{bin_id_1}}",
      "quantity": 10,
      "picked_quantity": 0,
      "status": "pending"
    },
    {
      "item_id": "{{item_id_2}}",
      "bin_id": "{{bin_id_2}}",
      "quantity": 5,
      "picked_quantity": 0,
      "status": "pending"
    }
  ],
  "picking_progress": {
    "total_items": 15,
    "picked_items": 0,
    "start_time": null,
    "end_time": null,
    "picker_notes": ""
  },
  "notes": "Express shipment for VIP customer",
  "assigned_to": "{{user_id}}",
  "due_date": "2023-07-01T10:00:00Z"
}
```

### Request example 1

```json
{
  "warehouse_id": "uuid", // required
  "order_type": "string", // required (individual, batch, waybill)
  "status": "string", // required (pending, in_progress, completed, cancelled)
  "item_details": [ // required array
    {
      "item_id": "uuid", // required
      "bin_id": "uuid", // required
      "quantity": number, // required, must be positive
      "picked_quantity": number, // optional, defaults to 0
      "status": "string" // optional, defaults to "pending"
    }
  ],
  "reference_number": "string", // optional, auto-generated if not provided
  "external_reference": "string", // optional (e.g., source system reference)
  "priority": number, // optional, defaults to 0
  "picking_progress": { // optional
    "total_items": number,
    "picked_items": number,
    "start_time": "timestamp",
    "end_time": "timestamp",
    "picker_notes": "string"
  },
  "notes": "string", // optional
  "assigned_to": "uuid", // optional, user ID to assign the order to
  "due_date": "timestamp" // optional
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Picking order created successfully",
  "order": {
    "id": "order-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "reference_number": "PKO-00012",
    "external_reference": "EXT-12345",
    "order_type": "individual",
    "priority": 1,
    "status": "pending",
    "item_details": [
      {
        "item_id": "item-uuid-1",
        "bin_id": "bin-uuid-1",
        "quantity": 10,
        "picked_quantity": 0,
        "status": "pending"
      },
      {
        "item_id": "item-uuid-2",
        "bin_id": "bin-uuid-2",
        "quantity": 5,
        "picked_quantity": 0,
        "status": "pending"
      }
    ],
    "picking_progress": {
      "total_items": 15,
      "picked_items": 0,
      "start_time": null,
      "end_time": null,
      "picker_notes": ""
    },
    "notes": "Express shipment for VIP customer",
    "assigned_to": "user-uuid",
    "due_date": "2023-07-01T10:00:00Z",
    "created_by": "admin-uuid",
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-01T10:00:00Z"
  }
}
```

### Response 400 Error - Missing Required Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: warehouse_id"
}
```

### Response 400 Error - Invalid Order Type



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Must be one of: individual, batch, waybill"
}
```

### Response 400 Error - Invalid Status



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid status: unknown. Must be one of: pending, in_progress, completed, cancelled"
}
```

### Response 400 Error - Invalid Item Details



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item details must be a non-empty array"
}
```

### Response 400 Error - Missing Item Fields



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Each item must have item_id, quantity, and bin_id"
}
```

### Response 400 Error - Invalid Quantity



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Quantity must be positive (item_id: item-uuid-1)"
}
```

### Response 400 Error - Insufficient Stock



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient stock (item_id: item-uuid-1, bin_id: bin-uuid-1, available: 5, requested: 10)"
}
```

### Response 404 Error - Item Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found (item_id: item-uuid-1)"
}
```

### Response 404 Error - Item Not In Bin



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not in specified bin (item_id: item-uuid-1, bin_id: bin-uuid-1)"
}
```

### Response 404 Error - Bin Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Bin not found (bin_id: bin-uuid-1)"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Assigned User Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Assigned user not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /warehousing/picking-orders/:id — Get Picking Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/picking-orders#get-get-picking-order)

Retrieves detailed information about a specific picking order by its ID, including warehouse, creator, and assignee details.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns comprehensive picking order details
- Includes associated warehouse information
- Includes information about the creator and assigned user
- Provides detailed item specifications with quantities

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/picking-orders/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the picking order to retrieve (required) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Picking order retrieved successfully",
  "order": {
    "id": "order-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "reference_number": "PKO-00012",
    "external_reference": "EXT-12345",
    "order_type": "individual",
    "priority": 1,
    "status": "pending",
    "item_details": [
      {
        "item_id": "item-uuid-1",
        "bin_id": "bin-uuid-1",
        "quantity": 10,
        "picked_quantity": 0,
        "status": "pending"
      },
      {
        "item_id": "item-uuid-2",
        "bin_id": "bin-uuid-2",
        "quantity": 5,
        "picked_quantity": 0,
        "status": "pending"
      }
    ],
    "picking_progress": {
      "total_items": 15,
      "picked_items": 0,
      "start_time": null,
      "end_time": null,
      "picker_notes": ""
    },
    "notes": "Express shipment for VIP customer",
    "assigned_to": "user-uuid",
    "due_date": "2023-07-01T10:00:00Z",
    "created_by": "admin-uuid",
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-01T10:00:00Z",
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    },
    "created_by_user": {
      "id": "admin-uuid",
      "first_name": "Admin",
      "last_name": "User",
      "email": "admin@example.com"
    },
    "assigned_to_user": {
      "id": "user-uuid",
      "first_name": "John",
      "last_name": "Picker",
      "email": "john.picker@example.com"
    }
  }
}
```

### Response 400 Error - Order ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Picking order ID required"
}
```

### Response 404 Error - Order Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Picking order not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## PUT /warehousing/picking-orders/:id — Update Picking Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/picking-orders#put-update-picking-order)

Updates the status and assignee of a specific picking order.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Warehouse stock validation and the existing status rules remain unchanged.
- `delivery_id` marks a lifecycle-managed P&D/operational-return picking cycle. Do not send `status=handed_off` for that record; the assigned driver's Pickup action performs the handoff atomically with stock exit and custody transfer. Manual `handed_off` returns HTTP 409 `pnd_picking_handed_off_by_driver_pickup`.

### Frontend integration contract

Keep lifecycle-managed picking orders at `completed` while they wait for the driver. Trigger the delivery Pickup action; do not call this endpoint with `handed_off`. Display the localized 409 message if a stale client attempts it.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/picking-orders/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the picking order to retrieve (required) UUID of the picking order to update |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| assigned_to | string |  |  |  |
| item_details | array<object> |  |  |  |
| item_details.item_id | string |  |  |  |
| item_details.bin_id | string |  |  |  |
| item_details.quantity | number |  |  |  |
| item_details.picked_quantity | number |  |  |  |
| item_details.status | string |  |  |  |
| picking_progress | object |  |  |  |
| picking_progress.total_items | number |  |  |  |
| picking_progress.picked_items | number |  |  |  |
| picking_progress.start_time | string |  |  |  |
| picking_progress.end_time | null |  |  |  |
| picking_progress.picker_notes | string |  |  |  |
| notes | string |  |  |  |
| due_date | string |  |  |  |



### Request body (raw)

```json
{
  "status": "in_progress",
  "assigned_to": "{{new_user_id}}",
  "item_details": [
    {
      "item_id": "{{item_id_1}}",
      "bin_id": "{{bin_id_1}}",
      "quantity": 10,
      "picked_quantity": 5,
      "status": "in_progress"
    },
    {
      "item_id": "{{item_id_2}}",
      "bin_id": "{{bin_id_2}}",
      "quantity": 5,
      "picked_quantity": 3,
      "status": "in_progress"
    }
  ],
  "picking_progress": {
    "total_items": 15,
    "picked_items": 8,
    "start_time": "2023-06-01T11:00:00Z",
    "end_time": null,
    "picker_notes": "In progress, some items located"
  },
  "notes": "Updated priority for this order",
  "due_date": "2023-06-30T10:00:00Z"
}
```

### Request example 1

```json
{
  "status": "string", // optional (pending, in_progress, completed, cancelled, handed_off)
  "assigned_to": "uuid", // optional, user ID to assign the order to
  "item_details": [
    {
      "item_id": "uuid",
      "bin_id": "uuid",
      "quantity": number,
      "picked_quantity": number
    }
  ],
  "picking_progress": {
    "total_items": number,
    "picked_items": number,
    "start_time": "timestamp",
    "end_time": "timestamp",
    "picker_notes": "string"
  },
  "notes": "string",
  "due_date": "timestamp"
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Picking order updated successfully",
  "order": {
    "id": "order-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "reference_number": "PKO-00012",
    "external_reference": "EXT-12345",
    "order_type": "individual",
    "priority": 1,
    "status": "in_progress",
    "item_details": [
      {
        "item_id": "item-uuid-1",
        "bin_id": "bin-uuid-1",
        "quantity": 10,
        "picked_quantity": 5,
        "status": "in_progress"
      },
      {
        "item_id": "item-uuid-2",
        "bin_id": "bin-uuid-2",
        "quantity": 5,
        "picked_quantity": 3,
        "status": "in_progress"
      }
    ],
    "picking_progress": {
      "total_items": 15,
      "picked_items": 8,
      "start_time": "2023-06-01T11:00:00Z",
      "end_time": null,
      "picker_notes": "In progress, some items located"
    },
    "notes": "Updated priority for this order",
    "due_date": "2023-06-30T10:00:00Z",
    "created_by": "admin-uuid",
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-01T10:00:00Z",
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    },
    "created_by_user": {
      "id": "admin-uuid",
      "first_name": "Admin",
      "last_name": "User",
      "email": "admin@example.com"
    },
    "assigned_to_user": {
      "id": "user-uuid",
      "first_name": "John",
      "last_name": "Picker",
      "email": "john.picker@example.com"
    }
  }
}
```

### Response 400 Error - Order ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Picking order ID required"
}
```

### Response 404 Error - Order Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Picking order not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 409 Error - P&D Picking Handoff Completes During Driver Pickup



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "pnd_picking_handed_off_by_driver_pickup",
  "message": "This picking order is handed off automatically when the assigned driver picks up the parcel"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /warehousing/sorting-sessions — List Sorting Sessions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#get-list-sorting-sessions)

Retrieve sorting sessions for a specific warehouse with optional filtering by status and date range.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions?tenant_id={{tenant_id}}&warehouse_id={{warehouse_id}}&status=active&page=1&limit=20&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| warehouse_id | query string | Yes | Filter by specific warehouse (required) | {{warehouse_id}} |
| status | query string |  | Filter by session status (active, completed) | active |
| date_from | query string | No | Filter sessions from date (YYYY-MM-DD) | 2024-01-01 |
| date_to | query string | No | Filter sessions to date (YYYY-MM-DD) | 2024-12-31 |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of items per page | 20 |
| language | query string | No | Response language (optional, default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Sorting sessions retrieved successfully",
  "sessions": [
    {
      "id": "session-uuid-1",
      "status": "active",
      "started_at": "2024-06-15T08:00:00Z",
      "ended_at": null,
      "warehouse": {
        "id": "warehouse-uuid-1",
        "name": "Main Distribution Center",
        "code": "WH001"
      },
      "assignments": [
        {
          "id": "assignment-uuid-1",
          "destination_type": "city",
          "destination_value": "Baghdad",
          "cage_location": {
            "id": "loc-uuid-1",
            "code": "CAGE-BAGHDAD-001",
            "name": "Baghdad Cage"
          },
          "staff": {
            "id": "user-uuid-1",
            "full_name": "John Doe",
            "employee_id": "EMP001"
          }
        },
        {
          "id": "assignment-uuid-2",
          "destination_type": "zone",
          "destination_value": "Anawa",
          "cage_location": {
            "id": "loc-uuid-2",
            "code": "TROLLEY-ANAWA-001",
            "name": "Anawa Trolley"
          },
          "staff": {
            "id": "user-uuid-2",
            "full_name": "Jane Smith",
            "employee_id": "EMP002"
          }
        }
      ]
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 404 Error - Warehouse Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

## POST /warehousing/sorting-sessions — Create Sorting Session with Assignments

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#post-create-sorting-session-with-assignments)

Create a new sorting session with assignments. API automatically creates:
- CAGE locations for cities and partner areas
- TROLLEY locations for zones

Capacity is optional (defaults to 10). Each assignment can have different capacity.
Temporary locations are created with {total_capacity: X, filled_capacity: 0}

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| assignments | array<object> |  |  |  |
| assignments.staff_id | string |  |  |  |
| assignments.destination_type | string |  |  |  |
| assignments.destination_value | string |  |  |  |
| assignments.capacity | number |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "assignments": [
    {
      "staff_id": "{{staff_id_1}}",
      "destination_type": "city",
      "destination_value": "Baghdad",
      "capacity": 25
    },
    {
      "staff_id": "{{staff_id_2}}",
      "destination_type": "zone",
      "destination_value": "Anawa"
    },
    {
      "staff_id": "{{staff_id_3}}",
      "destination_type": "partner_area",
      "destination_value": "Prime_Baghdad",
      "capacity": 15
    }
  ]
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Sorting session created successfully",
  "session": {
    "id": "session-uuid-1",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid-1",
    "status": "active",
    "started_at": "2024-06-15T08:00:00Z",
    "ended_at": null,
    "created_at": "2024-06-15T08:00:00Z",
    "warehouse": {
      "id": "warehouse-uuid-1",
      "name": "Main Distribution Center",
      "code": "WH001"
    },
    "assignments": [
      {
        "id": "assignment-uuid-1",
        "session_id": "session-uuid-1",
        "staff_id": "staff-uuid-1",
        "destination_type": "city",
        "destination_value": "Baghdad",
        "cage_location_id": "loc-uuid-1",
        "created_at": "2024-06-15T08:00:00Z"
      },
      {
        "id": "assignment-uuid-2",
        "session_id": "session-uuid-1",
        "staff_id": "staff-uuid-2",
        "destination_type": "zone",
        "destination_value": "Anawa",
        "cage_location_id": "loc-uuid-2",
        "created_at": "2024-06-15T08:00:00Z"
      }
    ],
    "temporary_locations": [
      {
        "id": "loc-uuid-1",
        "code": "CAGE-BAGHDAD-001",
        "name": "Baghdad Cage",
        "location_type": "CAGE",
        "total_capacity": 25,
        "filled_capacity": 0
      },
      {
        "id": "loc-uuid-2",
        "code": "TROLLEY-ANAWA-001",
        "name": "Anawa Trolley",
        "location_type": "TROLLEY",
        "total_capacity": 10,
        "filled_capacity": 0
      }
    ]
  }
}
```

### Response 400 Error - Missing Warehouse ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "warehouse_id is required"
}
```

## POST /warehousing/sorting-sessions — Create Sorting Session (Simple)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#post-create-sorting-session-simple)

Create a new sorting session without assignments. You can add assignments later.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}"
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Sorting session created successfully",
  "session": {
    "id": "session-uuid-1",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid-1",
    "status": "active",
    "started_at": "2024-06-15T08:00:00Z",
    "ended_at": null,
    "created_at": "2024-06-15T08:00:00Z",
    "warehouse": {
      "id": "warehouse-uuid-1",
      "name": "Main Distribution Center",
      "code": "WH001"
    },
    "assignments": [],
    "temporary_locations": []
  }
}
```

## GET /warehousing/sorting-sessions/:session_id — Get Sorting Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#get-get-sorting-session)

Get detailed information about a specific sorting session including assignments and temporary locations with their capacity information.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions/:session_id?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| session_id | path string | Yes | UUID of the sorting session | {{session_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Sorting session retrieved successfully",
  "session": {
    "id": "session-uuid-1",
    "status": "active",
    "started_at": "2024-06-15T08:00:00Z",
    "ended_at": null,
    "warehouse": {
      "id": "warehouse-uuid-1",
      "name": "Main Distribution Center",
      "code": "WH001"
    },
    "assignments": [
      {
        "id": "assignment-uuid-1",
        "destination_type": "city",
        "destination_value": "Baghdad",
        "created_at": "2024-06-15T08:00:00Z",
        "cage_location": {
          "id": "loc-uuid-1",
          "code": "CAGE-BAGHDAD-001",
          "name": "Baghdad Cage",
          "location_type": "CAGE"
        },
        "staff": {
          "id": "user-uuid-1",
          "full_name": "John Doe",
          "employee_id": "EMP001"
        }
      },
      {
        "id": "assignment-uuid-2",
        "destination_type": "zone",
        "destination_value": "Anawa",
        "created_at": "2024-06-15T08:00:00Z",
        "cage_location": {
          "id": "loc-uuid-2",
          "code": "TROLLEY-ANAWA-001",
          "name": "Anawa Trolley",
          "location_type": "TROLLEY"
        },
        "staff": {
          "id": "user-uuid-2",
          "full_name": "Jane Smith",
          "employee_id": "EMP002"
        }
      }
    ]
  }
}
```

### Response 404 Error - Session Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Sorting session not found"
}
```

## PUT /warehousing/sorting-sessions/:session_id — Complete Sorting Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#put-complete-sorting-session)

Mark a sorting session as completed (automatically sets ended_at timestamp). This would typically trigger cleanup of temporary locations.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions/:session_id?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| session_id | path string | Yes | UUID of the sorting session | {{session_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "status": "completed"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Sorting session updated successfully",
  "session": {
    "id": "session-uuid-1",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid-1",
    "status": "completed",
    "started_at": "2024-06-15T08:00:00Z",
    "ended_at": "2024-06-15T17:00:00Z",
    "created_at": "2024-06-15T08:00:00Z",
    "updated_at": "2024-06-15T17:00:00Z"
  }
}
```

### Response 404 Error - Session Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Sorting session not found"
}
```

## GET /warehousing/sorting-sessions/:session_id/assignments — List Session Assignments

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#get-list-session-assignments)

Get all assignments for a specific sorting session with their temporary cage/trolley locations and capacity information.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions/:session_id/assignments?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| session_id | path string | Yes | UUID of the sorting session | {{session_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignments retrieved successfully",
  "assignments": [
    {
      "id": "assignment-uuid-1",
      "session_id": "session-uuid-1",
      "staff_id": "staff-uuid-1",
      "destination_type": "city",
      "destination_value": "Baghdad",
      "cage_location_id": "loc-uuid-1",
      "created_at": "2024-06-15T08:00:00Z"
    },
    {
      "id": "assignment-uuid-2",
      "session_id": "session-uuid-1",
      "staff_id": "staff-uuid-2",
      "destination_type": "zone",
      "destination_value": "Anawa",
      "cage_location_id": "loc-uuid-2",
      "created_at": "2024-06-15T08:00:00Z"
    }
  ]
}
```

## POST /warehousing/sorting-sessions/:session_id/assignments — Add More Assignments to Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#post-add-more-assignments-to-session)

Add new assignments to an existing active sorting session. API automatically creates additional temporary locations for each new assignment with specified capacity.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions/:session_id/assignments?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| session_id | path string | Yes | UUID of the sorting session | {{session_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| assignments | array<object> |  |  |  |
| assignments.staff_id | string |  |  |  |
| assignments.destination_type | string |  |  |  |
| assignments.destination_value | string |  |  |  |
| assignments.capacity | number |  |  |  |



### Request body (raw)

```json
{
  "assignments": [
    {
      "staff_id": "{{staff_id_5}}",
      "destination_type": "partner_area",
      "destination_value": "Aramex_Sulaymaniyah",
      "capacity": 20
    },
    {
      "staff_id": "{{staff_id_6}}",
      "destination_type": "zone",
      "destination_value": "Local_Erbil_North",
      "capacity": 35
    }
  ]
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignments created successfully",
  "assignments": [
    {
      "id": "assignment-uuid-5",
      "session_id": "session-uuid-1",
      "staff_id": "staff-uuid-5",
      "destination_type": "partner_area",
      "destination_value": "Aramex_Sulaymaniyah",
      "cage_location_id": "loc-uuid-5",
      "created_at": "2024-06-15T10:00:00Z"
    }
  ],
  "temporary_locations": [
    {
      "id": "loc-uuid-5",
      "code": "CAGE-ARAMEX-SULAYMANIYAH-001",
      "name": "Aramex Sulaymaniyah Cage",
      "location_type": "CAGE",
      "total_capacity": 20,
      "filled_capacity": 0
    }
  ]
}
```

### Response 400 Error - Session Not Active

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Session is not active"
}
```

## POST /warehousing/sorting-sessions — Example: Multi-City Sorting Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#post-example-multi-city-sorting-session)

### Example

Create a sorting session for multiple cities with varying capacities. Will create separate CAGE locations for each city automatically. Note: Mosul assignment will use default capacity of 10.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| assignments | array<object> |  |  |  |
| assignments.staff_id | string |  |  |  |
| assignments.destination_type | string |  |  |  |
| assignments.destination_value | string |  |  |  |
| assignments.capacity | number |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "assignments": [
    {
      "staff_id": "{{staff_id_1}}",
      "destination_type": "city",
      "destination_value": "Baghdad",
      "capacity": 50
    },
    {
      "staff_id": "{{staff_id_2}}",
      "destination_type": "city",
      "destination_value": "Sulaymaniyah",
      "capacity": 40
    },
    {
      "staff_id": "{{staff_id_3}}",
      "destination_type": "city",
      "destination_value": "Basrah",
      "capacity": 30
    },
    {
      "staff_id": "{{staff_id_4}}",
      "destination_type": "city",
      "destination_value": "Mosul"
    }
  ]
}
```

## PATCH /warehousing/sorting-sessions/:session_id — Update Sorting Session

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#patch-update-sorting-session)

Updates a sorting session's status. When status is set to 'completed', the ended_at timestamp is automatically set.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions/:session_id?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| session_id | path string | Yes | UUID of the sorting session | session-uuid-1 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string | No | New status for the session (e.g. 'completed') |  |



### Request body (raw)

```json
{
  "status": "completed"
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Sorting session updated successfully",
  "session": {
    "id": "session-uuid-1",
    "status": "completed",
    "started_at": "2026-05-22T08:00:00Z",
    "ended_at": "2026-05-22T16:00:00Z",
    "warehouse": {
      "id": "wh-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    }
  }
}
```

### Response 404 Error - Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Sorting session not found"
}
```

## POST /warehousing/sorting-sessions/:session_id/assignments — Create Sorting Assignments

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#post-create-sorting-assignments)

Creates sorting assignments for an existing session. Automatically creates temporary CAGE locations for each assignment.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions/:session_id/assignments?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| session_id | path string | Yes | UUID of the sorting session | session-uuid-1 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| assignments | array<object> | Yes | Array of assignment objects |  |
| assignments.staff_id | string | Yes | UUID of the warehouse staff |  |
| assignments.destination_type | string | Yes | 'city', 'zone', or 'partner_area' |  |
| assignments.destination_value | string | Yes | The destination name |  |
| assignments.capacity | number | No | Capacity for the temporary bin (default: 10) |  |



### Request body (raw)

```json
{
  "assignments": [
    {
      "staff_id": "staff-uuid-1",
      "destination_type": "city",
      "destination_value": "Baghdad",
      "capacity": 20
    },
    {
      "staff_id": "staff-uuid-2",
      "destination_type": "zone",
      "destination_value": "Anawa",
      "capacity": 15
    }
  ]
}
```

### Response 201 Success

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignments created successfully",
  "assignments": [
    {
      "id": "assign-uuid-1",
      "destination_type": "city",
      "destination_value": "Baghdad",
      "created_at": "2026-05-22T08:30:00Z",
      "cage_location": {
        "id": "loc-uuid-1",
        "code": "CAGE-Baghdad-1",
        "name": "Sorting Cage - Baghdad",
        "location_type": "cage",
        "location_purpose": "sorting"
      },
      "staff": {
        "id": "staff-uuid-1",
        "full_name": "Ali Hassan",
        "employee_id": "EMP001"
      }
    },
    {
      "id": "assign-uuid-2",
      "destination_type": "zone",
      "destination_value": "Anawa",
      "created_at": "2026-05-22T08:30:00Z",
      "cage_location": {
        "id": "loc-uuid-2",
        "code": "CAGE-Anawa-1",
        "name": "Sorting Cage - Anawa",
        "location_type": "cage",
        "location_purpose": "sorting"
      },
      "staff": {
        "id": "staff-uuid-2",
        "full_name": "Sara Ahmed",
        "employee_id": "EMP002"
      }
    }
  ],
  "temporary_locations": [
    {
      "id": "loc-uuid-1",
      "code": "CAGE-Baghdad-1",
      "name": "Sorting Cage - Baghdad"
    },
    {
      "id": "loc-uuid-2",
      "code": "CAGE-Anawa-1",
      "name": "Sorting Cage - Anawa"
    }
  ]
}
```

### Response 404 Error - Session Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Sorting session not found"
}
```

## GET /warehousing/sorting-sessions/:session_id/assignments — List Sorting Assignments

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/sorting-sessions#get-list-sorting-assignments)

Lists all sorting assignments for a specific session.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/sorting-sessions/:session_id/assignments?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| session_id | path string | Yes | UUID of the sorting session | session-uuid-1 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| language | query string |  | Response language | en |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Assignments retrieved successfully",
  "assignments": [
    {
      "id": "assign-uuid-1",
      "destination_type": "city",
      "destination_value": "Baghdad",
      "created_at": "2026-05-22T08:30:00Z",
      "cage_location": {
        "id": "loc-uuid-1",
        "code": "CAGE-Baghdad-1",
        "name": "Sorting Cage - Baghdad",
        "location_type": "cage"
      },
      "staff": {
        "id": "staff-uuid-1",
        "full_name": "Ali Hassan",
        "employee_id": "EMP001"
      }
    }
  ]
}
```

### Response 404 Error - Session Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Sorting session not found"
}
```

## GET /warehousing/inventory-stock — List Inventory Stock

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-stock-management#get-list-inventory-stock)

Retrieves inventory stock levels with comprehensive filtering options for a **specific warehouse**. Provides detailed information about item quantities in specific bin locations, including status, lot numbers, and expiration dates.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Business Rules
- `statusCounts` and `stats` use the same tenant, warehouse, item, bin, search, lot, serial, quantity, reserved quantity, item name, and updated-date filters as the list.
- Only the selected `status` filter is excluded from counters, so all Stock Levels tab counts remain available.
- Counters are grouped in PostgreSQL; the Edge Function does not download all tenant stock rows to calculate them.

### Response
- Returns a paginated list of inventory stock records scoped to the specified warehouse
- Includes details of the items, bins, and warehouses
- Each stock row preserves `has_exception`, `has_open_exception`, and the latest `open_exception`, and also returns every `open_exceptions` entry plus `open_exception_count`
- The deprecated singular `exception` alias is not returned
- Includes pagination information (total count, current page, limit)

### Frontend integration contract

Continue sending the selected warehouse as `warehouse_id` and use `statusCounts.total`, `available`, `reserved`, `damaged`, and `expired` for the tabs. Render the stock exception indicator from `stock[].has_exception`; open `stock[].open_exceptions` to show every current flag. Use `stock[].open_exception` only when a singular newest open exception is needed; `stock[].exception` is no longer returned.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-stock?tenant_id={{tenant_id}}&warehouse_id={{warehouse_id}}&item_id=&bin_id=&status=&search=&lot_number=&min_quantity=&max_quantity=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| warehouse_id | query string | Yes | UUID of the warehouse (required) UUID of the warehouse whose stock should be listed | {{warehouse_id}} |
| item_id | query string | No | Filter by inventory item (optional) |  |
| bin_id | query string | No | Filter by bin location (optional) |  |
| status | query string | No | Filter by stock status (optional) Filter by stock status (available, reserved, damaged, expired) |  |
| search | query string | No | Search in lot number and serial number (optional) |  |
| lot_number | query string | No | Filter by lot number (optional) |  |
| min_quantity | query string | No | Filter by minimum quantity (optional) |  |
| max_quantity | query string | No | Filter by maximum quantity (optional) |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 20) Number of items per page (default: 20) | 20 |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory stock retrieved successfully",
  "stock": [
    {
      "id": "stock-uuid-1",
      "tenant_id": "tenant-uuid",
      "warehouse_id": "warehouse-uuid",
      "item_id": "item-uuid",
      "bin_id": "bin-uuid",
      "quantity": 100,
      "unit_of_measure": "ea",
      "status": "available",
      "has_exception": true,
      "has_open_exception": true,
      "open_exception": {
        "id": "exception-uuid",
        "exception_type": "damaged",
        "exception_label": "Damaged",
        "status": "open",
        "affected_quantity": 1,
        "exception_details": {
          "damage_area": "corner"
        },
        "notes": "Box crushed on arrival",
        "reported_at": "2026-06-30T10:00:00Z",
        "updated_at": "2026-06-30T10:00:00Z"
      },
      "lot_number": "LOT123",
      "serial_number": null,
      "expiration_date": "2024-12-31T00:00:00Z",
      "received_date": "2023-06-15T00:00:00Z",
      "last_counted_at": "2023-06-15T10:30:00Z",
      "created_at": "2023-06-15T09:45:00Z",
      "updated_at": "2023-06-15T09:45:00Z",
      "item": {
        "id": "item-uuid",
        "name": "Test Product",
        "sku": "ITEM-001",
        "barcode": "123456789",
        "category": "Electronics",
        "item_type": "finished_good"
      },
      "bin": {
        "id": "bin-uuid",
        "name": "Bin A1",
        "code": "B-001",
        "location_type": "bin"
      },
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Main Warehouse",
        "code": "WH001"
      },
      "open_exceptions": [
        {
          "id": "exception-uuid",
          "exception_type": "damaged",
          "exception_label": "Damaged",
          "status": "open",
          "affected_quantity": 1,
          "exception_details": {
            "damage_area": "corner"
          },
          "notes": "Box crushed on arrival",
          "reported_at": "2026-06-30T10:00:00Z",
          "updated_at": "2026-06-30T10:00:00Z"
        }
      ],
      "open_exception_count": 1
    }
  ],
  "statusCounts": {
    "available": 1,
    "reserved": 0,
    "damaged": 0,
    "expired": 0
  },
  "stats": {
    "available": 1,
    "reserved": 0,
    "damaged": 0,
    "expired": 0
  },
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /warehousing/inventory-stock — Create/Update Inventory Stock

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-stock-management#post-create-update-inventory-stock)

Creates or updates inventory stock for an item in a specific bin location. If a stock record already exists for the specified warehouse-item-bin combination, it updates the quantity; otherwise, it creates a new stock record. Also creates a transaction record to track the change.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Warehouse must exist and belong to the specified tenant
- Item must exist and belong to the tenant
- Bin must exist, belong to the specified warehouse, and be of location_type "bin"
- Quantity must be a non-negative number
- Status must be one of: available, reserved, damaged, expired
- If stock record already exists, it will be updated; otherwise, a new record is created
- An inventory transaction record is automatically created to track the stock change

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/inventory-stock?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| item_id | string |  |  |  |
| bin_id | string |  |  |  |
| quantity | number |  |  |  |
| unit_of_measure | string |  |  |  |
| status | string |  |  |  |
| lot_number | string |  |  |  |
| serial_number | null |  |  |  |
| expiration_date | string |  |  |  |
| received_date | string |  |  |  |
| last_counted_at | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "item_id": "{{item_id}}",
  "bin_id": "{{bin_id}}",
  "quantity": 100,
  "unit_of_measure": "ea",
  "status": "available",
  "lot_number": "LOT123",
  "serial_number": null,
  "expiration_date": "2024-12-31T00:00:00Z",
  "received_date": "2023-06-15T00:00:00Z",
  "last_counted_at": "2023-06-15T10:30:00Z",
  "notes": "Initial stock for product launch"
}
```

### Request example 1

```json
{
  "warehouse_id": "uuid", // required
  "item_id": "uuid", // required
  "bin_id": "uuid", // required
  "quantity": number, // required, non-negative
  "unit_of_measure": "string", // required (e.g., "each", "box", "kg")
  "status": "string", // required (available, reserved, damaged, expired)
  "lot_number": "string", // optional
  "serial_number": "string", // optional
  "expiration_date": "date", // optional
  "received_date": "date", // required
  "last_counted_at": "date", // optional
  "notes": "string" // optional
}
```

### Response 201 Success Response - Created



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory stock created successfully",
  "stock": {
    "id": "stock-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "item_id": "item-uuid",
    "bin_id": "bin-uuid",
    "quantity": 100,
    "unit_of_measure": "ea",
    "status": "available",
    "lot_number": "LOT123",
    "serial_number": null,
    "expiration_date": "2024-12-31T00:00:00Z",
    "received_date": "2023-06-15T00:00:00Z",
    "last_counted_at": "2023-06-15T10:30:00Z",
    "created_at": "2023-06-15T09:45:00Z",
    "updated_at": "2023-06-15T09:45:00Z",
    "item": {
      "id": "item-uuid",
      "name": "Test Product",
      "sku": "ITEM-001"
    },
    "bin": {
      "id": "bin-uuid",
      "name": "Bin A1",
      "code": "B-001"
    },
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    }
  },
  "operation": "created"
}
```

### Response 200 Success Response - Updated



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory stock updated successfully",
  "stock": {
    "id": "stock-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "item_id": "item-uuid",
    "bin_id": "bin-uuid",
    "quantity": 100,
    "unit_of_measure": "ea",
    "status": "available",
    "lot_number": "LOT123",
    "serial_number": null,
    "expiration_date": "2024-12-31T00:00:00Z",
    "received_date": "2023-06-15T00:00:00Z",
    "last_counted_at": "2023-06-15T10:30:00Z",
    "created_at": "2023-06-10T14:30:00Z",
    "updated_at": "2023-06-15T09:45:00Z",
    "item": {
      "id": "item-uuid",
      "name": "Test Product",
      "sku": "ITEM-001"
    },
    "bin": {
      "id": "bin-uuid",
      "name": "Bin A1",
      "code": "B-001"
    },
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    }
  },
  "operation": "updated"
}
```

### Response 400 Error - Missing Required Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: warehouse_id"
}
```

### Response 400 Error - Quantity Must Be Non-Negative



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Quantity must be non-negative"
}
```

### Response 400 Error - Invalid Status



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid status. Must be one of: available, reserved, damaged, expired"
}
```

### Response 400 Error - Location Not A Bin



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Location not a bin"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Item Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found"
}
```

### Response 404 Error - Bin Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Bin not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/inventory-stock/:id — Get Inventory Stock

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-stock-management#get-get-inventory-stock)

Retrieves details of a specific inventory stock record, including its transaction history and current warehouse exceptions. Provides comprehensive information about the item, bin location, and warehouse.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns detailed information about the specified inventory stock record
- Includes related item, bin, and warehouse details
- Includes transaction history for the stock (limited to most recent 5 transactions)
- `stock.open_exceptions` contains every currently open warehouse exception for the related inventory item; resolved exceptions are excluded
- `stock.open_exception` is the newest open exception and `stock.open_exception_count` is the number of open exceptions
- `stock.has_open_exception` and `stock.has_exception` remain boolean indicators
- The deprecated `stock.exception` alias is not returned

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-stock/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the inventory stock record (required) | stock-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory stock retrieved successfully",
  "stock": {
    "id": "stock-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "item_id": "item-uuid",
    "bin_id": "bin-uuid",
    "quantity": 100,
    "unit_of_measure": "ea",
    "status": "available",
    "has_exception": true,
    "has_open_exception": true,
    "open_exception": {
      "id": "exception-uuid",
      "exception_type": "wrong_item",
      "exception_label": "Wrong Item",
      "status": "open"
    },
    "open_exceptions": [
      {
        "id": "exception-uuid",
        "exception_type": "wrong_item",
        "exception_label": "Wrong Item",
        "status": "open"
      }
    ],
    "open_exception_count": 1,
    "lot_number": "LOT123",
    "serial_number": null,
    "expiration_date": "2024-12-31T00:00:00Z",
    "received_date": "2023-06-15T00:00:00Z",
    "last_counted_at": "2023-06-15T10:30:00Z",
    "created_at": "2023-06-15T09:45:00Z",
    "updated_at": "2023-06-15T09:45:00Z",
    "item": {
      "id": "item-uuid",
      "name": "Test Product",
      "sku": "ITEM-001",
      "barcode": "123456789",
      "category": "Electronics",
      "item_type": "finished_good"
    },
    "bin": {
      "id": "bin-uuid",
      "name": "Bin A1",
      "code": "B-001",
      "location_type": "bin"
    },
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    }
  },
  "transactions": [
    {
      "id": "transaction-uuid-1",
      "tenant_id": "tenant-uuid",
      "warehouse_id": "warehouse-uuid",
      "item_id": "item-uuid",
      "stock_id": "stock-uuid",
      "transaction_type": "initial_stock",
      "quantity": 100,
      "from_bin_id": null,
      "to_bin_id": "bin-uuid",
      "reference_type": "stock_action",
      "reference_id": null,
      "notes": "Initial stock via API",
      "performed_by": "user-uuid",
      "timestamp": "2023-06-15T09:45:00Z"
    }
  ]
}
```

### Response 404 Error - Stock Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Stock not found"
}
```

### Response 400 Error - Stock ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Stock ID required"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## PUT /warehousing/inventory-stock/:id — Update Inventory Stock

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-stock-management#put-update-inventory-stock)

Updates an existing inventory stock record. Only provided fields will be updated (partial update). Validates that the stock record exists and belongs to the tenant before performing the update. Creates an audit transaction record for tracking changes.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Stock record must exist and belong to the specified tenant
- Only provided fields are updated (partial update supported)
- Quantity must be a non-negative number if provided
- Status must be one of: available, reserved, damaged, expired if provided
- An inventory transaction record is automatically created for audit trail
- Response includes list of fields that were actually changed

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/inventory-stock/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the inventory stock record to update (required) | stock-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| quantity | number |  |  |  |
| unit_of_measure | string |  |  |  |
| status | string |  |  |  |
| lot_number | string |  |  |  |
| serial_number | string |  |  |  |
| expiration_date | string |  |  |  |
| received_date | string |  |  |  |
| last_counted_at | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "quantity": 150,
  "unit_of_measure": "ea",
  "status": "available",
  "lot_number": "LOT456",
  "serial_number": "SN789",
  "expiration_date": "2025-01-31T00:00:00Z",
  "received_date": "2023-07-01T00:00:00Z",
  "last_counted_at": "2023-07-15T14:30:00Z",
  "notes": "Stock updated after cycle count"
}
```

### Request example 1

```json
{
  "quantity": number, // optional, non-negative
  "unit_of_measure": "string", // optional (e.g., "each", "box", "kg")
  "status": "string", // optional (available, reserved, damaged, expired)
  "lot_number": "string", // optional
  "serial_number": "string", // optional
  "expiration_date": "date", // optional
  "received_date": "date", // optional
  "last_counted_at": "date", // optional
  "notes": "string" // optional, used for transaction audit trail
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory stock updated successfully",
  "stock": {
    "id": "stock-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "item_id": "item-uuid",
    "bin_id": "bin-uuid",
    "quantity": 150,
    "unit_of_measure": "ea",
    "status": "available",
    "lot_number": "LOT456",
    "serial_number": "SN789",
    "expiration_date": "2025-01-31T00:00:00Z",
    "received_date": "2023-07-01T00:00:00Z",
    "last_counted_at": "2023-07-15T14:30:00Z",
    "created_at": "2023-06-15T09:45:00Z",
    "updated_at": "2023-07-15T14:30:00Z",
    "item": {
      "id": "item-uuid",
      "name": "Test Product",
      "sku": "ITEM-001",
      "barcode": "123456789",
      "category": "Electronics",
      "subcategory": "Phones"
    },
    "bin": {
      "id": "bin-uuid",
      "name": "Bin A1",
      "code": "B-001",
      "location_type": "bin"
    },
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    }
  },
  "changes": [
    "quantity",
    "lot_number",
    "serial_number",
    "expiration_date",
    "received_date",
    "last_counted_at"
  ]
}
```

### Response 404 Error - Stock Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Stock not found"
}
```

### Response 400 Error - Stock ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Stock ID required"
}
```

### Response 400 Error - Invalid Quantity



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Quantity must be a non-negative number"
}
```

### Response 400 Error - Invalid Status



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid status. Must be one of: available, reserved, damaged, expired"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 400 Error - Invalid Request Body



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request body"
}
```

## GET /warehousing/receiving-orders — List Receiving Orders

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#get-list-receiving-orders)

Retrieves receiving orders with pagination and filters.

### Status counts
- `stats` and `status_matrix` are identical.
- They are scoped by `tenant_id` and by `warehouse_id` when `warehouse_id` is provided either as a legacy query parameter or in `column_filters`.
- They are not narrowed by the selected `status`, so the frontend can render status tabs for the selected tenant or warehouse while `total` remains the current filtered result count.

### Frontend integration contract
- Use `total` for the current list result count after all filters, including `status`.
- Use `status_matrix` or `stats` for status-tab counts. When calling with `warehouse_id`, those counts are for that warehouse only.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders?tenant_id={{tenant_id}}&language=en&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |
| language | query string |  |  | en |
| sort_by | query string | No | Sort field: reference_number \| expected_arrival_date \| actual_arrival_date \| created_at (default: created_at) |  |
| sort_order | query string | No | asc \| desc (default: desc) |  |
| column_filters | query string | No | JSON-stringified filter object. Keys: warehouse_id, supplier_id, status, reference_number, expected_arrival_date {from,to}, actual_arrival_date {from,to}, total_items {min,max}, total_quantity {min,max}. When warehouse_id is present, stats/status_matrix are scoped to that warehouse. |  |
| warehouse_id | query string | No | Legacy: filter by warehouse (optional). When present, stats/status_matrix are scoped to this warehouse. |  |
| supplier_id | query string | No | Legacy: filter by supplier (optional) |  |
| status | query string | No | Legacy: filter by status (optional). stats/status_matrix remain across all statuses for the selected tenant/warehouse. |  |
| expected_arrival_date | query string | No | Legacy: filter by exact date YYYY-MM-DD (optional) |  |
| search | query string | No | Search in reference_number |  |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |



### Response 200 Success — plain list

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving orders retrieved successfully",
  "receiving_orders": [
    {
      "id": "ro-uuid-1",
      "tenant_id": "tenant-uuid",
      "warehouse_id": "warehouse-uuid",
      "reference_number": "RO-1Z2X3C4V",
      "external_reference": "PO-9876",
      "supplier_id": "supplier-uuid",
      "supplier_name": "Acme Suppliers Inc.",
      "expected_arrival_date": "2026-06-15T00:00:00Z",
      "actual_arrival_date": null,
      "status": "pending",
      "total_items": 2,
      "total_quantity": 75,
      "items": [
        {
          "item_id": "item-uuid-1",
          "item_name": "Premium T-Shirt",
          "quantity": 50,
          "received_quantity": 0,
          "lot_number": "LOT123",
          "serial_number": "SN456",
          "bin_id": null,
          "attributes": {
            "color": "black",
            "size": "medium"
          }
        },
        {
          "item_id": "item-uuid-2",
          "item_name": "Cotton Polo Shirt",
          "quantity": 25,
          "received_quantity": 0,
          "lot_number": "LOT456",
          "serial_number": "SN789",
          "bin_id": null,
          "attributes": {
            "color": "white",
            "size": "large"
          }
        }
      ],
      "notes": "Monthly restock order",
      "created_at": "2026-05-01T10:00:00Z",
      "updated_at": "2026-05-01T10:00:00Z",
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Main Warehouse",
        "code": "WH001"
      },
      "received_by_user": null,
      "supplier": {
        "id": "supplier-uuid",
        "name": "Acme Suppliers Inc."
      }
    }
  ],
  "stats": {
    "total": 21,
    "pending": 5,
    "in_progress": 1,
    "received": 11,
    "partially_received": 3,
    "received_with_exceptions": 0,
    "cancelled": 1
  },
  "status_matrix": {
    "total": 21,
    "pending": 5,
    "in_progress": 1,
    "received": 11,
    "partially_received": 3,
    "received_with_exceptions": 0,
    "cancelled": 1
  },
  "page": 1,
  "limit": 20,
  "total": 21
}
```

### Response 200 Success — with column_filters (date range + numeric range)

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving orders retrieved successfully",
  "receiving_orders": [
    {
      "id": "ro-uuid-2",
      "reference_number": "RO-ABCD1234",
      "status": "pending",
      "expected_arrival_date": "2026-06-10T00:00:00Z",
      "total_items": 3,
      "total_quantity": 60,
      "items": [],
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Main Warehouse",
        "code": "WH001"
      },
      "received_by_user": null,
      "supplier": {
        "id": "supplier-uuid",
        "name": "Acme Suppliers Inc."
      }
    }
  ],
  "stats": {
    "total": 21,
    "pending": 5,
    "in_progress": 1,
    "received": 11,
    "partially_received": 3,
    "received_with_exceptions": 0,
    "cancelled": 1
  },
  "status_matrix": {
    "total": 21,
    "pending": 5,
    "in_progress": 1,
    "received": 11,
    "partially_received": 3,
    "received_with_exceptions": 0,
    "cancelled": 1
  },
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 404 Error — Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 400 Error — Invalid column_filters JSON

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "field": "column_filters",
  "message": "invalid_json"
}
```

### Response 500 Error — Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /warehousing/receiving-orders — Create Receiving Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#post-create-receiving-order)

Creates a new receiving order (incoming shipment) for a warehouse. Validates warehouse existence, item details, and optional supplier references.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Warehouse must exist and belong to the specified tenant
- All items must exist in the inventory system
- Each item must have a valid item_id and positive quantity
- If supplier_id is provided, it must exist in the system
- Status must be one of: pending, received, partially_received, received_with_exceptions, cancelled
- Item attributes must be a valid object if provided

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| expected_arrival_date | string |  |  |  |
| item_details | array<object> |  |  |  |
| item_details.item_id | string |  |  |  |
| item_details.quantity | number |  |  |  |
| item_details.lot_number | string |  |  |  |
| item_details.serial_number | string |  |  |  |
| item_details.attributes | object |  |  |  |
| item_details.attributes.color | string |  |  |  |
| item_details.attributes.size | string |  |  |  |
| reference_number | string |  |  |  |
| external_reference | string |  |  |  |
| supplier_id | string |  |  |  |
| status | string |  |  |  |
| receiving_details | object |  |  |  |
| receiving_details.tracking_number | string |  |  |  |
| receiving_details.carrier | string |  |  |  |
| receiving_details.shipping_method | string |  |  |  |
| receiving_details.notes | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "expected_arrival_date": "2023-06-15",
  "item_details": [
    {
      "item_id": "{{item_id_1}}",
      "quantity": 50,
      "lot_number": "LOT123",
      "serial_number": "SN456",
      "attributes": {
        "color": "black",
        "size": "medium"
      }
    },
    {
      "item_id": "{{item_id_2}}",
      "quantity": 25,
      "lot_number": "LOT456",
      "serial_number": "SN789",
      "attributes": {
        "color": "white",
        "size": "large"
      }
    }
  ],
  "reference_number": "RO-1Z2X3C4V",
  "external_reference": "PO-9876",
  "supplier_id": "{{supplier_id}}",
  "status": "pending",
  "receiving_details": {
    "tracking_number": "TRK12345",
    "carrier": "Express Shipping Co.",
    "shipping_method": "2-day",
    "notes": "Deliver at loading dock B"
  },
  "notes": "Monthly restock order"
}
```

### Request example 1

```json
{
  "warehouse_id": "uuid", // required
  "expected_arrival_date": "date", // required
  "item_details": [ // required array of items
    {
      "item_id": "uuid", // required
      "quantity": number, // required, must be positive
      "lot_number": "string", // optional
      "serial_number": "string", // optional
      "attributes": {} // optional custom attributes
    }
  ],
  "reference_number": "string", // optional
  "external_reference": "string", // optional (e.g., purchase order number)
  "supplier_id": "uuid", // optional
  "status": "string", // required (pending, received, partially_received, received_with_exceptions, cancelled)
  "receiving_details": { // optional
    "tracking_number": "string",
    "carrier": "string",
    "shipping_method": "string",
    "notes": "string"
  },
  "notes": "string" // optional
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving order created successfully",
  "receiving_order": {
    "id": "order-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "reference_number": "RO-1Z2X3C4V",
    "external_reference": "PO-9876",
    "supplier_id": "supplier-uuid",
    "expected_arrival_date": "2023-06-15T00:00:00Z",
    "status": "pending",
    "item_details": [
      {
        "item_id": "item-uuid-1",
        "quantity": 50,
        "lot_number": "LOT123",
        "serial_number": "SN456",
        "attributes": {
          "color": "black",
          "size": "medium"
        }
      },
      {
        "item_id": "item-uuid-2",
        "quantity": 25,
        "lot_number": "LOT456",
        "serial_number": "SN789",
        "attributes": {
          "color": "white",
          "size": "large"
        }
      }
    ],
    "receiving_details": {
      "tracking_number": "TRK12345",
      "carrier": "Express Shipping Co.",
      "shipping_method": "2-day",
      "notes": "Deliver at loading dock B"
    },
    "actual_arrival_date": null,
    "received_by": null,
    "notes": "Monthly restock order",
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-01T10:00:00Z"
  }
}
```

### Response 400 Error - Missing Required Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: warehouse_id"
}
```

### Response 400 Error - Invalid Item Details Format



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item details must be array"
}
```

### Response 400 Error - Empty Item Details



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item details cannot be empty"
}
```

### Response 400 Error - Invalid Item



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Each item must have valid item_id and quantity (item 1)"
}
```

### Response 400 Error - Invalid Status



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid status value: unknown. Must be one of: pending, received, partially_received, cancelled"
}
```

### Response 400 Error - Invalid Receiving Details



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Receiving details must be object"
}
```

### Response 400 Error - Invalid Receiving Details Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid receiving details field: invalid_field"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Item Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found (item 1, id: item-uuid-1)"
}
```

### Response 404 Error - Supplier Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Supplier not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /warehousing/receiving-orders/:id — Get Receiving Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#get-get-receiving-order)

Retrieves detailed information about a specific receiving order, including warehouse, supplier, receiver, and enriched receipt-item details.

- User must have `view:tenant` or `manage:operations:tenant` permission.

### Response contract
- Normal inventory receiving orders expose `items[]` with `item_id`, item metadata, quantity, received quantity, `bin_id`, `bin` as `{ id, name, code, location_type }` (or `null` when no valid bin is assigned), and exception context.
- Bin enrichment is tenant- and receiving-warehouse-scoped.
- When `receiving_details.exception_id` references an original delivery exception, `receiving_details` also includes `exception_type`, `exception_label`, `exception_reason`, and `exception_notes`. Missing optional reason or notes are returned as `null`.
- `return_reason` remains the physical return-handoff reason and is not used as the original delivery exception reason.
- Waybill-generated receiving orders expose one delivery manifest entry per `items[]` row with `delivery_id`, `delivery_code`, `barcode`, `delivery_status`, receipt `outcome`, `notes`, `quantity: 1`, and `received_quantity` (`0` for missing, otherwise `1`).
- `total_items` counts the returned receipt rows. For Waybill-generated orders, `total_quantity` is the manifest delivery count.
- Includes associated warehouse, supplier, and receiver information when available.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the receiving order to retrieve (required) UUID of the receiving order to retrieve. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant. | {{tenant_id}} |



### Normal item bin example

```json
{
  "item_id": "item-uuid",
  "bin_id": "bin-uuid",
  "bin": {
    "id": "bin-uuid",
    "name": "Receiving Bin",
    "code": "B001",
    "location_type": "bin"
  }
}
```

### Waybill example

```json
{
  "success": true,
  "receiving_order": {
    "waybill_id": "waybill-uuid",
    "status": "partially_received",
    "total_items": 2,
    "total_quantity": 2,
    "items": [
      {
        "delivery_id": "delivery-uuid",
        "delivery_code": "DEL1001",
        "barcode": "1001",
        "delivery_status": "delivered",
        "outcome": "present",
        "notes": null,
        "quantity": 1,
        "received_quantity": 1
      }
    ]
  }
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving order retrieved successfully",
  "receiving_order": {
    "id": "order-uuid",
    "waybill_id": "waybill-uuid",
    "warehouse_id": "warehouse-uuid",
    "reference_number": "RO-WB-WB00039",
    "status": "partially_received",
    "receiving_details": {
      "source": "failed_delivery_return",
      "exception_id": "exception-uuid",
      "return_reason": "Returned as instructed",
      "exception_type": "damaged",
      "exception_label": "Package Damage",
      "exception_reason": "Outer box was crushed",
      "exception_notes": "Driver uploaded a photo"
    },
    "total_items": 2,
    "total_quantity": 2,
    "items": [
      {
        "delivery_id": "delivery-uuid-1",
        "delivery_code": "DEL1001",
        "barcode": "1001",
        "delivery_status": "delivered",
        "outcome": "present",
        "notes": null,
        "quantity": 1,
        "received_quantity": 1
      },
      {
        "delivery_id": "delivery-uuid-2",
        "delivery_code": "DEL1002",
        "barcode": "1002",
        "delivery_status": "in_transit",
        "outcome": "missing",
        "notes": null,
        "quantity": 1,
        "received_quantity": 0
      }
    ]
  }
}
```

### Response 400 Error - Order ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Receiving order ID required"
}
```

### Response 404 Error - Order Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Receiving order not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/receiving-orders/scan/{{delivery_barcode}} — Scan Receiving Order Barcode

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#get-scan-receiving-order-barcode)

Scans a staged receiving barcode and resolves its receiving warehouse from the matched receiving order.

- The actor must have an active `warehouse_staff` assignment for the matched receiving order's warehouse.

- `warehouse_id` is not required or read; the backend derives it from the receiving order.

### Response contract
- Exact delivery-item codes are verified against the tenant package-item record and mapped to an inventory line in the same receiving order before their parent code is used. Arbitrary suffixes such as `DEL3501-99` return HTTP 404.
- `scan.type` is `delivery_package_item`, `scan.package_item_id` identifies the printed box, and `scan.inventory_item_id` identifies its receiving-order inventory line; parent scans retain `delivery_package` and return null item IDs.
- When the barcode exists in both an origin and a Waybill destination receiving order, the destination receipt order is selected.
- `receiving_order.default_receiving_location` is the active default receiving location, or null.
- HTTP 403 `warehouse_access_denied` means the actor is not active staff of the matched receiving order's warehouse.

### Frontend integration contract
- Send the complete scanned barcode without removing the `-N` suffix.
- Do not send `warehouse_id` to this scan endpoint.
- Preselect `receiving_order.default_receiving_location.id` when non-null.
- Keep the location scanner visible. For location scans, use `receiving_order.warehouse.id` from this response as the `warehouse_id` in `GET /warehousing/locations/:barcode?search_type=barcode&warehouse_id=:warehouse_id`.

Resolve a receiving barcode to receiving-order details. Accepts a parent delivery barcode such as `DEL3501` and an exact printed delivery-item barcode such as `DEL3501-1`. For an item barcode, the backend verifies the complete code, uses `DEL3501` to locate the parent receiving order, and keeps `DEL3501-1` as the exact box identity. Failed staged P&D child legs use the physical parent parcel barcode; operational returns use the separate return delivery's own barcode. Eligible unassigned G2G receiving work remains supported through the shared atomic warehouse-staging owner. The backend derives the warehouse from the matched receiving order and verifies that the actor is active staff of that warehouse.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders/scan/{{delivery_barcode}}?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_barcode | path string | Yes | Variable used inside the request path. |  |
| barcode | path string | Yes | Parent delivery barcode such as `DEL3501`, or exact printed delivery-item barcode such as `DEL3501-1`. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) is required only when it is unavailable from the authenticated request context. | {{tenant_id}} |
| language | query string |  |  | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving order retrieved successfully",
  "scan": {
    "barcode": "DEL1001",
    "type": "delivery_package"
  },
  "receiving_order": {
    "id": "receiving-order-uuid",
    "reference_number": "RO-1001",
    "external_reference": "DEL1001",
    "status": "pending",
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    },
    "default_receiving_location": {
      "id": "location-uuid",
      "code": "B001",
      "name": "Receiving Bin",
      "barcode": "WLCWH001B001",
      "location_type": "bin"
    },
    "item_details": [
      {
        "item_id": "item-uuid-1",
        "quantity": 1,
        "received_quantity": 0,
        "bin_id": null
      }
    ]
  },
  "items": []
}
```

### Response 403 Error - Warehouse Access Denied

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "warehouse_access_denied",
  "message": "You are not assigned to this warehouse and cannot execute this delivery",
  "details": {
    "warehouse_id": "matched-receiving-order-warehouse-uuid"
  }
}
```

### Response 404 Error - Receiving Order Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Receiving order not found"
}
```

## POST /warehousing/receiving-orders/bulk-receive/validate — Validate Bulk Package Receiving Barcodes

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#post-validate-bulk-package-receiving-barcodes)

Validates parent delivery and exact delivery-item barcodes for receiving. This endpoint does not mutate receiving orders or inventory stock.

- User must have warehouse/tenant access through the normal warehousing middleware.

### Business Rules
- A parent barcode such as `DEL3501` selects every remaining box in its receiving order, preserving the existing receive-all behavior.
- An exact item barcode such as `DEL3501-1` is verified against `delivery_package_items`, resolves parent `DEL3501`, and selects only its matching receiving-order inventory line.
- Multiple item barcodes for one order are combined without selecting unscanned boxes.
- Duplicate scans in the same request return `duplicate_scan`; unknown or invented item suffixes return `unknown_barcode`.
- Cancelled receiving orders return `receiving_order_not_receivable`; fully received lines return `already_received`.
- Staged inbound delivery readiness is enforced before package scans are marked valid.
- Validation uses set-based indexed barcode lookups and no per-barcode database queries.

### Frontend integration contract
- Send every complete scanned code, including its `-N` suffix.
- Treat both `delivery_package` and `delivery_package_item` result types as valid when `results[].valid === true`.
- Show `invalid_scans[].message` or `invalid_scans[].reason` inline.
- Use `counts.scanned_count`, `counts.valid_count`, and `counts.invalid_count` for the live scan summary.

If a scanned staged P&D receiving order has an unrelated open parent exception, that scan is invalid with `reason=parent_exception_open`. The exact order linked to an Arrived-at-Warehouse exception remains receivable so warehouse custody can be confirmed.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders/bulk-receive/validate?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant. | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcodes | array<string> |  |  |  |



### Request body (raw)

```json
{
  "barcodes": [
    "DEL3501-1",
    "DEL3501-2"
  ]
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk package receiving barcodes validated successfully",
  "results": [
    {
      "barcode": "DEL1001",
      "valid": true,
      "type": "delivery_package",
      "reason": "valid_scan",
      "message": "Valid scan",
      "receiving_order_id": "receiving-order-uuid-1",
      "reference_number": "RO-STAGE-1001",
      "warehouse_id": "warehouse-uuid",
      "remaining_items": 1
    },
    {
      "barcode": "DEL9999",
      "valid": false,
      "type": "unknown",
      "reason": "unknown_barcode",
      "message": "Unknown barcode"
    }
  ],
  "invalid_scans": [
    {
      "barcode": "DEL9999",
      "valid": false,
      "type": "unknown",
      "reason": "unknown_barcode",
      "message": "Unknown barcode"
    }
  ],
  "counts": {
    "scanned_count": 2,
    "valid_count": 1,
    "invalid_count": 1
  }
}
```

## POST /warehousing/receiving-orders/bulk-receive — Bulk Receive Package Receiving Orders

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#post-bulk-receive-package-receiving-orders)

Completes receiving from parent delivery or exact delivery-item barcodes across staged receiving orders.

- User must have warehouse/tenant access through the normal warehousing middleware.

- `bin_id` is optional. If omitted, backend uses the active default receiving location.

### Business Rules
- Backend re-runs the same exact barcode validation before mutating data.
- A parent code receives all remaining lines. Exact item codes receive only their matching lines; unscanned lines remain outstanding and the order becomes `partially_received`.
- Receiving the remaining exact item codes later changes the order to `received`.
- All valid scans in one completion request must resolve to one warehouse.
- Stock and receive movements are projected only for newly selected quantities into the selected/default location.
- Invalid scans are returned in `invalid_scans`; valid scans can still complete.

### Frontend integration contract
- Send complete physical item barcodes without stripping the suffix.
- Keep the scanned barcode list and selected `bin_id`; do not construct `item_details`.
- Use each `receive_result.status` to display `partially_received` or `received`.
- Handle `mixed_warehouse_codes`, `bin_id_required_for_received_items`, `inbound_delivery_not_ready`, `already_received`, `not_part_of_this_receiving_order`, and `unknown_barcode` from `results[]` / `invalid_scans[]`.

Normal staged P&D receiving remains blocked by an unrelated open parent exception. The exact staged order linked to an Arrived-at-Warehouse parent exception and a failed outbound re-receiving order are explicitly receivable while their exceptions remain open. The receiving-order update atomically adds only each line's remaining received delta to stock and one receive movement; a replay adds zero.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders/bulk-receive?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant. | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcodes | array<string> |  |  |  |
| bin_id | string |  |  |  |



### Request body (raw)

```json
{
  "barcodes": [
    "DEL3501-1",
    "DEL3501-2"
  ],
  "bin_id": "{{location_id}}"
}
```

### Request example 1

```json
{
  "barcodes": [
    "DEL3501-1",
    "DEL3501-2"
  ],
  "bin_id": "warehouse-location-uuid"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk package receiving completed",
  "received_count": 2,
  "failed_count": 0,
  "bin_id": "location-uuid",
  "results": [
    {
      "barcode": "DEL1001",
      "valid": true,
      "type": "delivery_package",
      "reason": "valid_scan",
      "message": "Valid scan",
      "receiving_order_id": "receiving-order-uuid-1",
      "reference_number": "RO-STAGE-1001",
      "warehouse_id": "warehouse-uuid",
      "remaining_items": 1,
      "success": true,
      "receive_result": {
        "success": true,
        "receiving_order_id": "receiving-order-uuid-1",
        "reference_number": "RO-STAGE-1001",
        "external_reference": "DEL1001",
        "warehouse_id": "warehouse-uuid",
        "status": "received",
        "received_items": 1,
        "auto_execute_delivery": {
          "enabled": false,
          "attempted": 0,
          "executed": 0,
          "skipped": 0,
          "failed": 0,
          "details": []
        }
      }
    }
  ],
  "invalid_scans": [],
  "counts": {
    "scanned_count": 2,
    "valid_count": 2,
    "invalid_count": 0,
    "received_count": 2,
    "failed_count": 0
  }
}
```

### Response 409 Error - Mixed Warehouse Codes



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "reason": "mixed_warehouse_codes",
  "message": "Scanned packages belong to multiple warehouses",
  "warehouse_ids": [
    "warehouse-uuid-1",
    "warehouse-uuid-2"
  ]
}
```

## POST /warehousing/receiving-orders/:id/bulk-receive/validate — Validate Bulk Receiving Barcodes

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#post-validate-bulk-receiving-barcodes)

Validates scans for one receiving order without mutating receiving status or stock.

- User must have warehouse/tenant access through the normal warehousing middleware.

### Business Rules
- Exact delivery-item codes are verified against `delivery_package_items`, resolved through their parent delivery code, and mapped to the matching `STAGE-<delivery>-<ordinal>` inventory line in this order.
- Existing `inventory_items.barcode` scans remain supported.
- A parent delivery barcode confirms context but does not select an item in this order-specific flow.
- Item codes for another order return `wrong_receiving_order_context`; invented codes return `unknown_barcode`.
- Duplicate and already-received scans are rejected.
- Lookups are batched and indexed.

### Frontend integration contract
- Send the complete scanned code, including the `-N` suffix.
- Treat valid `warehouse_item` and `delivery_package_item` results as receivable items.
- Show `invalid_scans[].message` inline.
- Use `counts.expected_count`, `counts.scanned_valid_count`, and `counts.remaining_count` for the live display.

Returns HTTP 409 with `code=parent_exception_open` when the staged P&D parent has an open exception that is not awaiting this exact receiving order. A linked Arrived-at-Warehouse exception must be received before dispatcher resolution.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders/:id/bulk-receive/validate?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the receiving order being scanned. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcodes | array<string> |  |  |  |
| warehouse_id | string |  |  |  |



### Request body (raw)

```json
{
  "barcodes": [
    "DEL3501-1",
    "DEL3501-2"
  ],
  "warehouse_id": "{{warehouse_id}}"
}
```

### Request example 1

```json
{
  "barcodes": [
    "DEL3501-1",
    "DEL3501-2"
  ],
  "warehouse_id": "warehouse-uuid"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk receiving barcodes validated successfully",
  "receiving_order": {
    "id": "receiving-order-uuid",
    "reference_number": "RO-1001",
    "external_reference": "DEL1001",
    "status": "pending",
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    }
  },
  "results": [
    {
      "barcode": "DEL1001",
      "valid": true,
      "type": "receiving_order",
      "reason": "valid_scan",
      "message": "Valid scan",
      "receiving_order_id": "receiving-order-uuid",
      "reference_number": "RO-1001"
    },
    {
      "barcode": "INV1001",
      "valid": true,
      "type": "warehouse_item",
      "reason": "valid_scan",
      "message": "Valid scan",
      "item_id": "item-uuid-1",
      "sku": "SKU-001",
      "item_name": "Package Item",
      "quantity": 1,
      "received_quantity": 0
    },
    {
      "barcode": "INV9999",
      "valid": false,
      "type": "unknown",
      "reason": "unknown_barcode",
      "message": "Unknown barcode"
    }
  ],
  "valid_items": [
    {
      "item_id": "item-uuid-1",
      "quantity": 1,
      "received_quantity": 0,
      "bin_id": null
    }
  ],
  "invalid_scans": [
    {
      "barcode": "INV9999",
      "valid": false,
      "type": "unknown",
      "reason": "unknown_barcode",
      "message": "Unknown barcode"
    }
  ],
  "counts": {
    "expected_count": 2,
    "already_received_count": 0,
    "scanned_valid_count": 1,
    "remaining_count": 1
  }
}
```

## POST /warehousing/receiving-orders/:id/bulk-receive — Bulk Receive Receiving Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#post-bulk-receive-receiving-order)

Finalizes receiving for valid inventory or exact delivery-item barcodes. The backend revalidates the complete codes and constructs the received item payload.

- User must have warehouse/tenant access through the normal warehousing middleware.

### Business Rules
- `bin_id` is required and must belong to the receiving order warehouse.
- Only the valid scanned item lines are received and stocked in that bin.
- Unscanned lines remain outstanding; status is `partially_received` until every line is received.
- Existing inventory barcodes remain supported.
- Invalid scans are returned in `invalid_scans`; if no valid item scans remain, the endpoint returns HTTP 400.
- Stock increases only by the newly received quantity.

### Frontend integration contract
- Submit the complete scanned barcode list and selected `bin_id`; do not strip delivery-item suffixes.
- Do not construct or submit `item_details`.
- Use `receiving_order.status`, `counts.received_count`, and `counts.remaining_count` after success.

Returns HTTP 409 with `code=parent_exception_open` when the staged P&D parent has an open exception that is not awaiting this exact receiving order. Receiving status and inventory stock remain unchanged for blocked orders. For accepted scans, the receiving-order update atomically projects only the newly received cumulative delta into stock and one receive movement; a replay adds zero.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders/:id/bulk-receive?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the receiving order to bulk receive UUID of the receiving order to receive. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |
| language | query string |  |  | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| barcodes | array<string> |  |  |  |
| bin_id | string |  |  |  |
| warehouse_id | string |  |  |  |



### Request body (raw)

```json
{
  "barcodes": [
    "DEL3501-1",
    "DEL3501-2"
  ],
  "bin_id": "{{location_id}}",
  "warehouse_id": "{{warehouse_id}}"
}
```

### Request example 1

```json
{
  "barcodes": [
    "DEL3501-1",
    "DEL3501-2"
  ],
  "bin_id": "warehouse-location-uuid",
  "warehouse_id": "warehouse-uuid"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk receiving completed successfully",
  "receiving_order": {
    "id": "receiving-order-uuid",
    "warehouse_id": "warehouse-uuid",
    "reference_number": "RO-1001",
    "external_reference": "DEL1001",
    "status": "partially_received",
    "supplier_name": null,
    "total_items": 2,
    "total_quantity": 2,
    "items": [
      {
        "item_id": "item-uuid-1",
        "item_name": "Package Item",
        "quantity": 1,
        "received_quantity": 1,
        "bin_id": "location-uuid"
      }
    ]
  },
  "received_items": [
    {
      "item_id": "item-uuid-1",
      "bin_id": "location-uuid",
      "received_quantity": 1,
      "lot_number": null,
      "serial_number": null
    }
  ],
  "invalid_scans": [],
  "counts": {
    "expected_count": 2,
    "received_count": 1,
    "newly_received_count": 1,
    "remaining_count": 1
  },
  "auto_execute_delivery": {
    "enabled": false,
    "attempted": 0,
    "executed": 0,
    "skipped": 0,
    "failed": 0,
    "details": []
  }
}
```

### Response 200 Success - Receive Remaining Boxes

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk receiving completed successfully",
  "receiving_order": {
    "id": "receiving-order-uuid",
    "status": "received"
  },
  "received_items": [
    {
      "item_id": "item-uuid-2",
      "bin_id": "location-uuid",
      "received_quantity": 1
    }
  ],
  "counts": {
    "expected_count": 2,
    "received_count": 2,
    "newly_received_count": 1,
    "remaining_count": 0
  }
}
```

### Response 400 Error - No Valid Item Barcodes



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "No valid item barcodes to receive",
  "invalid_scans": [
    {
      "barcode": "INV9999",
      "valid": false,
      "type": "unknown",
      "reason": "unknown_barcode",
      "message": "Unknown barcode"
    }
  ],
  "counts": {
    "expected_count": 2,
    "already_received_count": 0,
    "scanned_valid_count": 0,
    "remaining_count": 2
  }
}
```

### Response 200 Wallet Error Contract - Receiving Auto Execution



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Bulk receiving completed successfully",
  "auto_execute_delivery": {
    "enabled": true,
    "attempted": 1,
    "executed": 0,
    "skipped": 0,
    "failed": 1,
    "details": [
      {
        "item_id": "item-uuid",
        "delivery_id": "delivery-uuid",
        "status": "failed",
        "reason": "insufficient_available_balance",
        "code": "insufficient_available_balance",
        "message": "Customer has reached the Minimum Wallet Balance limit."
      }
    ]
  }
}
```

## POST /warehousing/receiving-orders/:id/execute — Execute Receiving Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#post-execute-receiving-order)

Atomic outbound execution for a received order. The action is allowed only when the receiving order still owns current custody and the actor is active staff of that order's warehouse. A warehouse-staged P&D Execute requires exact equality between received, stocked, picked, and reserved quantities, then returns a completed pick, reserved stock, and `handoff: null`; the later assignment owns handoff creation. A confirmed Waybill transfer removes execution from the origin and creates the actionable destination order without changing the existing Waybill execution contract. Frontend must branch on HTTP 403 `warehouse_access_denied`, HTTP 409 `receiving_order_not_current_warehouse`, and HTTP 409 `warehouse_preparation_inconsistent`.

Executes the exact outbound `delivery_id` owned by a received order. The receiving order determines the warehouse; no request body is required. The actor must have an active `warehouse_staff` assignment for that warehouse or the API returns HTTP 403 `warehouse_access_denied`. A Waybill receipt creates one destination receiving order per logical delivery. Only a `present` order returns `can_execute: true`; missing, damaged, no-location, and origin-history rows return a stable `execution_block_reason`. For a staged-delivery receiving order, `can_execute` is true only when the linked parent is `staged_at_warehouse`; otherwise it is false with `execution_block_reason=parent_not_staged_for_outbound_execution`, matching the execution guard. For a warehouse-staged P&D or operational-return order, execution atomically moves the outbound delivery to `pending`, creates one completed picking order, reserves destination stock without reducing its quantity, and returns `handoff: null`; received, stocked, picked, and reserved quantities must match exactly or the request returns HTTP 409 `warehouse_preparation_inconsistent`. Assignment creates the handoff later with driver context. Repeating P&D execution returns the existing preparation with `idempotent_replay: true`. Waybill and other non-staged receiving-order execution keep their existing behavior.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders/:id/execute?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | Received order UUID | {{receiving_order_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant when it is not supplied by authentication context | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving order executed successfully",
  "idempotent_replay": false,
  "receiving_order": {
    "id": "{{receiving_order_id}}",
    "warehouse_id": "{{destination_warehouse_id}}",
    "status": "received"
  },
  "delivery": {
    "id": "{{delivery_id}}",
    "status": "pending"
  },
  "picking_order": {
    "id": "{{picking_order_id}}",
    "status": "completed"
  },
  "handoff": null
}
```

### Response 403 Error - Warehouse Access Denied



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "warehouse_access_denied",
  "message": "You are not assigned to this warehouse and cannot execute this delivery",
  "details": {
    "warehouse_id": "{{destination_warehouse_id}}"
  }
}
```

### Response 409 Error - Parcel Is No Longer in This Warehouse



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "receiving_order_not_current_warehouse",
  "message": "This parcel is no longer in this warehouse",
  "details": {
    "receiving_order_warehouse_id": "{{origin_warehouse_id}}",
    "current_warehouse_id": "{{destination_warehouse_id}}"
  }
}
```

### Response 404 Error - Receiving Order Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "receiving_order_not_found",
  "message": "Receiving order not found"
}
```

## PUT /warehousing/receiving-orders/:id — Update Receiving Order

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/receiving-orders#put-update-receiving-order)

Updates a receiving order through the normal warehouse receiving flow; no separate failed-delivery receiving endpoint exists. Send `{ "status": "received" }`. A failed-delivery receipt may send one selected location through `item_details[0].bin_id`; if omitted, backend uses the warehouse's active default receiving location. If neither exists, the API returns HTTP 400 `bin_id_required_for_received_items`. Do not send a return-specific location field.

Inventory receiving may also send `exceptions`, where each entry contains `inventory_item_id` (or `item_id`), `exception_type`, optional `affected_quantity`, `inventory_stock_id`, `location_id`, `exception_details`, and `notes`. For an item with positive `received_quantity`, exceptions are recorded after receipt stock exists. If the tenant-defined exception type has `write_off_stock=true`, the response is reloaded after the atomic write-off and returns that item with reduced `received_quantity`. A fully received order changes to `received_with_exceptions`; the order remains `partially_received` while other lines are outstanding. Both `received` and `received_with_exceptions` are completed receipt states: for a staged P&D order either state advances its parent from `arrived_at_warehouse` to `staged_at_warehouse`, so unaffected packages can continue to outbound preparation. During dispatcher warehouse staging, a package already recorded as `returned` on the inbound child remains in `item_details` for audit with `received_quantity=0` and `receipt_outcome=returned_before_warehouse_receipt`; it creates no warehouse stock, while forward packages are fully received and the order completes as `received_with_exceptions`. Auto-execution uses the same corrected quantities. A linked item with `received_quantity=0`, or no `received_quantity` before receiving starts, is a valid no-stock exception: the exception is saved with `stock_written_off=false`, null write-off audit fields, and no unrelated stock is consumed. HTTP 409 code `warehouse_write_off_stock_not_available` remains reserved for positive received stock that cannot be written off.

For any Picked up/In transit delivery whose physical origin is a warehouse, exception creation has already created one exception-specific pending receiving order. While its custody is `awaiting_warehouse_receipt`, Retry, Return, and Cancel are unavailable. Receipt atomically restores item-level stock at the resolved receiving location, records one `receive` inventory movement per parcel item against this receiving order, confirms warehouse custody, removes the old route/trip, marks the old assignment Unassigned, and releases driver capacity exactly once. The delivery remains Exception and the exception remains Open until the dispatcher makes a separate decision after receipt.

For P&D outbound, the parent becomes Staged at warehouse; a later Retry makes Leg B Pending and a later Return stops the failed outbound before creating the existing operational return. For an operational return, there is no parent update and a later Retry makes the same operational return Pending. No nested return order or extra driver action is created.

### Frontend

package scan continues opening this normal receiving order. Submit literal status `received`, optionally one existing location ID. A successful re-stage returns `failed_delivery_return.delivery_status=exception`, `custody_state=staged_at_warehouse`, `exception_status=open`, `stock_restored=true`, `receiving_location_id`, and `return_action=null`; `parent_status` is Staged for P&D and null for operational returns. Refetch staged P&D receiving details and do not present lines with `receipt_outcome=returned_before_warehouse_receipt` as warehouse stock or customer-dispatch candidates. Handle `failed_delivery_return_items_not_found`, `failed_delivery_return_pick_quantity_not_found`, and `failed_delivery_return_stock_projection_failed` as conflicts without advancing the screen. Refetch the exception after receipt and render only the actions returned by the backend. Do not show Assign until the dispatcher resolves Retry.

The receiving-order update is the atomic stock boundary. For received, partially received, and received-with-exceptions item lines, the backend projects only the positive cumulative delta into stock and one receiving-order movement; retrying the same cumulative quantities adds zero. Failed-delivery receipt also appends the operational child's warehouse-restage event while keeping the exception open.

When `status` is `received`, `partially_received`, or `received_with_exceptions` and inventory `item_details` are supplied, ordinary receipt returns `auto_execute_delivery` with `enabled`, `attempted`, `executed`, `skipped`, `failed`, and `details[]`. A disabled package setting returns `enabled: false` and zero counters. Paths that do not check auto-execution, including Waybill and failed-delivery receipt, return null. Each detail identifies `item_id`, optional `delivery_id`, and `status` (`executed`, `skipped`, or `failed`); skipped/failed entries may include `reason`, `code`, and `message`. Executed entries may include `picking_order_id`; `handoff_id` exists only when the existing non-P&D execution creates a handoff. P&D and operational-return preparation waits for later assignment to create the handoff.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/receiving-orders/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the receiving order to update (required) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| actual_arrival_date | string |  |  |  |
| notes | string |  |  |  |
| receiving_details | object |  |  |  |
| receiving_details.tracking_number | string |  |  |  |
| receiving_details.carrier | string |  |  |  |
| receiving_details.shipping_method | string |  |  |  |
| receiving_details.notes | string |  |  |  |
| item_details | array<object> |  |  |  |
| item_details.item_id | string |  |  |  |
| item_details.quantity | number |  |  |  |
| item_details.received_quantity | number |  |  |  |
| item_details.bin_id | null |  |  |  |
| item_details.lot_number | string |  |  |  |
| item_details.serial_number | string |  |  |  |
| item_details.attributes | object |  |  |  |
| item_details.attributes.color | string |  |  |  |
| item_details.attributes.size | string |  |  |  |
| item_details.attributes.condition | string |  |  |  |



### Request body (raw)

```json
{
  "status": "received",
  "actual_arrival_date": "2023-06-15",
  "notes": "Received complete shipment on time",
  "receiving_details": {
    "tracking_number": "TRK12345-UPDATED",
    "carrier": "Express Shipping Co.",
    "shipping_method": "2-day",
    "notes": "Delivered at loading dock B"
  },
  "item_details": [
    {
      "item_id": "item-uuid-1",
      "quantity": 50,
      "received_quantity": 50,
      "bin_id": null,
      "lot_number": "LOT123",
      "serial_number": "SN456",
      "attributes": {
        "color": "black",
        "size": "medium",
        "condition": "perfect"
      }
    },
    {
      "item_id": "item-uuid-2",
      "quantity": 25,
      "received_quantity": 25,
      "bin_id": "{{location_id}}",
      "lot_number": "LOT456",
      "serial_number": "SN789",
      "attributes": {
        "color": "white",
        "size": "large",
        "condition": "good"
      }
    }
  ]
}
```

### Response 200 Success - Received With P&D Auto-execution



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving order updated successfully",
  "receiving_order": {
    "id": "order-uuid",
    "warehouse_id": "warehouse-uuid",
    "status": "received",
    "supplier_name": null,
    "total_items": 1,
    "total_quantity": 1,
    "items": [
      {
        "item_id": "item-uuid-1",
        "delivery_id": null,
        "waybill_delivery_id": null,
        "outcome": null,
        "item_name": "Product Name",
        "quantity": 1,
        "received_quantity": 1,
        "bin_id": "location-uuid",
        "has_open_exception": false,
        "open_exception": null,
        "open_exceptions": [],
        "open_exception_count": 0
      }
    ]
  },
  "auto_execute_delivery": {
    "enabled": true,
    "attempted": 1,
    "executed": 1,
    "skipped": 0,
    "failed": 0,
    "details": [
      {
        "item_id": "item-uuid-1",
        "delivery_id": "delivery-uuid-1",
        "status": "executed",
        "picking_order_id": "picking-order-uuid"
      }
    ]
  }
}
```

### Response 200 Success - Notes Update Without Auto-execution



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving order updated successfully",
  "receiving_order": {
    "id": "order-uuid",
    "waybill_id": "waybill-uuid",
    "warehouse_id": "warehouse-uuid",
    "status": "received",
    "total_items": 1,
    "total_quantity": 1,
    "items": [
      {
        "delivery_id": "delivery-uuid",
        "delivery_code": "DEL1001",
        "barcode": "1001",
        "delivery_status": "delivered",
        "outcome": "present",
        "notes": null,
        "quantity": 1,
        "received_quantity": 1
      }
    ]
  },
  "auto_execute_delivery": null
}
```

### Response 200 Compatibility - Legacy Pre-resolved Return Received



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving order updated successfully",
  "receiving_order": {
    "id": "receiving-order-uuid",
    "warehouse_id": "warehouse-uuid",
    "status": "received"
  },
  "auto_execute_delivery": null,
  "failed_delivery_return": {
    "success": true,
    "delivery_id": "failed-outbound-uuid",
    "delivery_status": "cancelled",
    "parent_delivery_id": "pnd-parent-uuid",
    "parent_status": "staged_at_warehouse",
    "custody_state": "staged_at_warehouse",
    "return_action": {
      "success": true,
      "action_key": "return",
      "return_request_id": "return-request-uuid",
      "return_delivery_id": "return-delivery-uuid",
      "return_delivery_status": "pending"
    }
  }
}
```

### Response 200 Success - P&D Outbound Re-staged With Open Exception



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving order updated successfully",
  "receiving_order": {
    "id": "receiving-order-uuid",
    "warehouse_id": "warehouse-uuid",
    "status": "received"
  },
  "auto_execute_delivery": null,
  "failed_delivery_return": {
    "success": true,
    "idempotent_replay": false,
    "delivery_id": "outbound-delivery-uuid",
    "delivery_status": "exception",
    "parent_delivery_id": "pnd-parent-uuid",
    "parent_status": "staged_at_warehouse",
    "custody_state": "staged_at_warehouse",
    "exception_status": "open",
    "attempt": {
      "number": 2,
      "allowed": 2,
      "limit_reached": true,
      "driver_id": "driver-uuid",
      "failed_at": "2026-08-19T10:30:00Z",
      "reason": "not_home"
    },
    "return_action": null
  }
}
```

### Response 200 Success - Failed Operational Return Received



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Receiving order updated successfully",
  "receiving_order": {
    "id": "receiving-order-uuid",
    "warehouse_id": "warehouse-uuid",
    "status": "received"
  },
  "auto_execute_delivery": null,
  "failed_delivery_return": {
    "success": true,
    "delivery_id": "operational-return-uuid",
    "delivery_status": "exception",
    "parent_delivery_id": null,
    "parent_status": null,
    "custody_state": "staged_at_warehouse",
    "exception_status": "open",
    "return_action": null
  }
}
```

### Response 409 Error - Parent Exception Open



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "parent_exception_open",
  "message": "Resolve the parent order exception before starting this delivery"
}
```

### Response 400 Error - Order ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Receiving order ID required"
}
```

### Response 404 Error - Order Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Receiving order not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/suppliers — List Suppliers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/suppliers-management#get-list-suppliers)

Retrieves a paginated list of suppliers with optional filtering by name and supplier type.

- User must have 'view:tenant', 'manage:operations:tenant' permission, or be a system admin

### Response
- Returns a paginated list of suppliers with their details
- Includes related tenant user information
- Provides filter options for supplier types
- Includes pagination metadata (total count, current page, limit)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/suppliers?tenant_id={{tenant_id}}&name=&supplier_type=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth context) (required if not in auth context): UUID of the tenant | {{tenant_id}} |
| name | query string | No | Filter suppliers by name (optional) Filter suppliers by name (partial match) |  |
| supplier_type | query string | No | Filter by supplier type (e.g., 'manufacturer', 'distributor') (optional) Filter by supplier type (exact match) |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of records per page (optional, default: 20) Number of records per page (default: 20) | 20 |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Suppliers retrieved successfully",
  "suppliers": [
    {
      "id": "supplier-uuid-1",
      "tenant_id": "tenant-uuid",
      "name": "ABC Manufacturing",
      "tenant_user_id": "tenant-user-uuid-1",
      "supplier_type": "manufacturer",
      "tenant_user": {
        "id": "tenant-user-uuid-1",
        "user_id": {
          "first_name": "John",
          "last_name": "Doe",
          "email": "john.doe@example.com"
        }
      }
    },
    {
      "id": "supplier-uuid-2",
      "tenant_id": "tenant-uuid",
      "name": "XYZ Distributors",
      "tenant_user_id": "tenant-user-uuid-2",
      "supplier_type": "distributor",
      "tenant_user": {
        "id": "tenant-user-uuid-2",
        "user_id": {
          "first_name": "Jane",
          "last_name": "Smith",
          "email": "jane.smith@example.com"
        }
      }
    }
  ],
  "stats": {
    "total": 2,
    "distributor": 1,
    "manufacturer": 1,
    "wholesaler": 0
  },
  "filter_options": {
    "supplier_types": [
      "manufacturer",
      "distributor",
      "wholesaler"
    ]
  },
  "page": 1,
  "limit": 20,
  "total": 2
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## POST /warehousing/suppliers — Create Supplier

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/suppliers-management#post-create-supplier)

Creates a new supplier with the specified name, tenant user, and supplier type.

- User must have 'manage:operations:tenant' permission or be a system admin

### Business Rules
- All fields are required
- Tenant user must exist and belong to the specified tenant
- The operation creates a new supplier record in the system

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/suppliers?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth context) (required if not in auth context): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| tenant_user_id | string |  |  |  |
| supplier_type | string |  |  |  |



### Request body (raw)

```json
{
  "name": "New Supplier Inc.",
  "tenant_user_id": "{{tenant_user_id}}",
  "supplier_type": "manufacturer"
}
```

### Request example 1

```json
{
  "name": "string", // required - Name of the supplier
  "tenant_user_id": "uuid", // required - UUID of the tenant user associated with this supplier
  "supplier_type": "string" // required - Type of supplier (e.g., "manufacturer", "distributor", "wholesaler")
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Supplier created successfully",
  "supplier": {
    "id": "supplier-uuid-new",
    "tenant_id": "tenant-uuid",
    "name": "New Supplier Inc.",
    "tenant_user_id": "tenant-user-uuid",
    "supplier_type": "manufacturer",
    "tenant_user": {
      "id": "tenant-user-uuid",
      "user_id": {
        "first_name": "John",
        "last_name": "Doe",
        "email": "john.doe@example.com"
      }
    }
  }
}
```

### Response 400 Error - Missing Required Field

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required supplier_type"
}
```

### Response 404 Error - Tenant User Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant user not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/suppliers/:id — Get Supplier

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/suppliers-management#get-get-supplier)

Retrieves detailed information about a specific supplier, including associated tenant user details.

- User must have 'view:tenant', 'manage:operations:tenant' permission, or be a system admin

### Response
- Returns complete supplier information
- Includes related tenant user details

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/suppliers/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the supplier to retrieve (required) | {{supplier_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth context) (required if not in auth context): UUID of the tenant | {{tenant_id}} |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Supplier retrieved successfully",
  "supplier": {
    "id": "supplier-uuid-1",
    "tenant_id": "tenant-uuid",
    "name": "ABC Manufacturing",
    "tenant_user_id": "tenant-user-uuid-1",
    "supplier_type": "manufacturer",
    "tenant_user": {
      "id": "tenant-user-uuid-1",
      "user_id": {
        "first_name": "John",
        "last_name": "Doe",
        "email": "john.doe@example.com"
      }
    }
  }
}
```

### Response 404 Error - Supplier Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Supplier not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## PUT /warehousing/suppliers/:id — Update Supplier

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/suppliers-management#put-update-supplier)

Updates an existing supplier with new information provided in the request body.

- User must have 'manage:operations:tenant' permission or be a system admin

### Business Rules
- At least one field must be provided for update
- If tenant_user_id is provided, it must be a valid tenant user belonging to the tenant
- Only provided fields will be updated

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/suppliers/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the supplier to update (required) | {{supplier_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth context) (required if not in auth context): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| tenant_user_id | string |  |  |  |
| supplier_type | string |  |  |  |



### Request body (raw)

```json
{
  "name": "Updated Supplier Name",
  "tenant_user_id": "{{tenant_user_id}}",
  "supplier_type": "distributor"
}
```

### Request example 1

```json
{
  "name": "string", // optional - New name for the supplier
  "tenant_user_id": "uuid", // optional - New tenant user UUID
  "supplier_type": "string" // optional - New supplier type
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Supplier updated successfully",
  "supplier": {
    "id": "supplier-uuid-1",
    "tenant_id": "tenant-uuid",
    "name": "Updated Supplier Name",
    "tenant_user_id": "tenant-user-uuid-2",
    "supplier_type": "distributor",
    "tenant_user": {
      "id": "tenant-user-uuid-2",
      "user_id": {
        "first_name": "Jane",
        "last_name": "Smith",
        "email": "jane.smith@example.com"
      }
    }
  }
}
```

### Response 404 Error - Supplier Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Supplier not found"
}
```

### Response 404 Error - Tenant User Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant user not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/inventory-items — List Inventory Items

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-items-management#get-list-inventory-items)

Retrieves inventory items with comprehensive filtering options.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Business Rules
- With `warehouse_id`, every item returns `stock_quantity` from the transactionally maintained warehouse-item total.
- `stock_status` is `in_stock` only when `stock_quantity > 0`; zero returns `depleted`.
- Without `warehouse_id`, both stock fields are `null` because stock state is warehouse-specific.
- `is_active` is item activation and does not determine stock status.

### Frontend integration contract
- Send the currently selected warehouse ID as `warehouse_id`.
- Render the stock badge from `items[].stock_status` and quantity from `items[].stock_quantity`; do not derive stock from `is_active`.

### Response
- Returns a paginated list of inventory items
- Each item includes `categories` as an array of `{ id, name }`
- Provides pagination information (total count, current page, limit)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-items?tenant_id={{tenant_id}}&warehouse_id=&brand=&search=&is_active=&category_ids=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| warehouse_id | query string | No | Filter by warehouse (optional) Filter by warehouse and populate warehouse-specific stock fields |  |
| brand | query string | No | Filter by brand (optional) |  |
| search | query string | No | Text search on name, SKU, or barcode (optional) |  |
| is_active | query string | No | Filter by active status: true or false (optional) Filter by active status (true or false) |  |
| category_ids | query string | No | Comma-separated list of warehouse_item_categories IDs to filter by (optional) Comma-separated list of category IDs (from `warehouse_item_categories`) to filter items by |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 20) Number of items per page (default: 20) | 20 |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory items retrieved successfully",
  "items": [
    {
      "id": "12345678-1234-1234-1234-123456789012",
      "name": "Wireless Headphones",
      "sku": "WH-200-BLK",
      "barcode": "INV1048",
      "description": "Premium noise-canceling wireless headphones",
      "brand": "SoundCore",
      "model": "Pro X",
      "unit_size": 1,
      "stock_quantity": 0,
      "stock_status": "depleted",
      "categories": [
        {
          "id": "category-uuid",
          "name": {
            "en": "Electronics"
          }
        },
        {
          "id": "nested-category-uuid",
          "name": {
            "en": "Headphones"
          }
        }
      ],
      "is_active": true,
      "updated_at": "2023-04-15T08:30:00Z"
    }
  ],
  "stats": {
    "total": 1,
    "active": 1,
    "inactive": 0,
    "labelPending": 0,
    "labelSuccess": 0
  },
  "kpis": {
    "total": 1,
    "active": 1,
    "inactive": 0,
    "labelPending": 0,
    "labelSuccess": 0
  },
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /warehousing/inventory-items — Create Inventory Item

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-items-management#post-create-inventory-item)

Creates a new inventory item with detailed specifications and attributes. Supports adding comprehensive product information including dimensions, categories (via IDs), and custom attributes.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Required fields: `name`, `sku`, and **either** `unit_size` **or** (`weight` **and** `dimensions.length/width/height`)
- If `unit_size` is omitted, the system will compute it using tenant unit-size settings (volumetric/weight rules) based on the provided `weight` and `dimensions`
- If `item_code` is provided, it must be a unique positive integer; otherwise the system allocates the next sequence code not already in use. Manual item codes remain supported, and concurrent manual/automatic claims for the same code are serialized
- When `is_single_item` is true, `location_id` is required and an `inventory_stock` row is created/updated at that bin with `quantity = 1`
- SKU must be unique within a tenant
- Barcode is generated by the system as `INV` + `item_code`
- Numeric values (`unit_size`, `weight`, `dimensions`) must be positive
- `is_active` defaults to true if not specified

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/inventory-items?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| sku | string |  |  |  |
| item_code | number |  |  |  |
| description | string |  |  |  |
| brand | string |  |  |  |
| model | string |  |  |  |
| unit_size | number |  |  |  |
| weight | number |  |  |  |
| dimensions | object |  |  |  |
| dimensions.length | number |  |  |  |
| dimensions.width | number |  |  |  |
| dimensions.height | number |  |  |  |
| dimensions.unit | string |  |  |  |
| attributes | object |  |  |  |
| attributes.color | string |  |  |  |
| attributes.connectivity | string |  |  |  |
| attributes.battery_life | string |  |  |  |
| category_ids | array<string> |  |  |  |
| is_single_item | boolean |  |  |  |
| location_id | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": "Wireless Headphones",
  "sku": "WH-200-BLK",
  "item_code": 1048,
  "description": "Premium noise-canceling wireless headphones",
  "brand": "SoundCore",
  "model": "Pro X",
  "unit_size": 1,
  "weight": 0.35,
  "dimensions": {
    "length": 7.5,
    "width": 6.8,
    "height": 3.2,
    "unit": "inches"
  },
  "attributes": {
    "color": "black",
    "connectivity": "bluetooth",
    "battery_life": "24h"
  },
  "category_ids": [
    "{{category_id_1}}",
    "{{category_id_2}}"
  ],
  "is_single_item": true,
  "location_id": "{{bin_location_id}}",
  "is_active": true
}
```

### Request example 1

```json
{
  "name": "string", // required
  "sku": "string", // required, must be unique within tenant
  "item_code": number, // optional, positive integer; if omitted, allocated from the sequence while skipping codes already claimed manually
  "description": "string", // optional
  "brand": "string", // optional
  "model": "string", // optional
  "unit_size": number, // optional, must be positive; if omitted, weight + dimensions are required
  "weight": number, // optional, must be positive
  "dimensions": { // optional
    "length": number, // must be positive
    "width": number, // must be positive
    "height": number, // must be positive
    "unit": "string" // unit of measurement (must match tenant dimension unit settings)
  },
  "attributes": { // optional
    "color": "string",
    "size": "string",
    "material": "string",
    "custom_attributes": {
      "key1": "value1",
      "key2": "value2"
    }
  },
  "category_ids": ["uuid", "uuid"], // optional, link to warehouse_item_categories
  "is_single_item": boolean, // optional; when true, a single stock record is created
  "location_id": "uuid", // required if is_single_item is true; warehouse location/bin id
  "is_active": boolean // optional, defaults to true
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory item created successfully",
  "item": {
    "id": "12345678-1234-1234-1234-123456789012",
    "tenant_id": "tenant-uuid",
    "name": "Wireless Headphones",
    "sku": "WH-200-BLK",
    "item_code": 1048,
    "barcode": "INV1048",
    "description": "Premium noise-canceling wireless headphones",
    "brand": "SoundCore",
    "model": "Pro X",
    "unit_size": 1,
    "weight": 0.35,
    "dimensions": {
      "length": 7.5,
      "width": 6.8,
      "height": 3.2,
      "unit": "inches"
    },
    "attributes": {
      "color": "black",
      "connectivity": "bluetooth",
      "battery_life": "24h"
    },
    "category_ids": [
      "category-uuid",
      "nested-category-uuid"
    ],
    "category": {
      "id": "category-uuid",
      "name": {
        "en": "Electronics"
      },
      "parent_id": null
    },
    "sub_category": {
      "id": "nested-category-uuid",
      "name": {
        "en": "Headphones"
      },
      "parent_id": "category-uuid"
    },
    "categories": [
      {
        "id": "category-uuid",
        "name": {
          "en": "Electronics"
        }
      },
      {
        "id": "nested-category-uuid",
        "name": {
          "en": "Headphones"
        }
      }
    ],
    "is_active": true,
    "created_at": "2023-04-15T08:30:00Z",
    "updated_at": "2023-04-15T08:30:00Z"
  }
}
```

### Response 400 Error - Missing Required Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: name"
}
```

### Response 400 Error - Duplicate SKU



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Duplicate SKU"
}
```

### Response 400 Error - Invalid Unit Size



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unit size must be a positive number"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/inventory-items/:id — Get Inventory Item

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-items-management#get-get-inventory-item)

Retrieves a specific inventory item by its unique identifier.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Business Rules
- A warehouse-scoped request never returns stock from another warehouse.
- If the item has no stock record in the selected warehouse, `stock_levels` is an empty array.
- A zero-quantity stock record remains in `stock_levels` with its actual quantity and status.
- Omitting `warehouse_id` preserves the existing cross-warehouse detail response for non-warehouse clients.

### Frontend integration contract
- The warehouse app must send its currently selected warehouse as literal `warehouse_id`.
- Calculate the top-card quantity only from the returned `stock_levels`; do not reuse stock from another request or warehouse.

### Response
- Returns detailed information about the specified inventory item
- Includes linked category metadata in `category_ids`, `category`, `sub_category`, and `categories`
- `categories` is returned as an array of `{ id, name }`
- Includes only the requested warehouse's stock levels when `warehouse_id` is supplied

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-items/:id?tenant_id={{tenant_id}}&warehouse_id={{warehouse_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the inventory item (required) | item-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |
| warehouse_id | query string | No | Selected warehouse UUID; limits stock_levels to this warehouse Selected warehouse UUID (optional). When supplied, `stock_levels` contains records only from that warehouse. | {{warehouse_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory item retrieved successfully",
  "item": {
    "id": "12345678-1234-1234-1234-123456789012",
    "tenant_id": "tenant-uuid",
    "name": "Wireless Headphones",
    "sku": "WH-200-BLK",
    "barcode": "INV1048",
    "description": "Premium noise-canceling wireless headphones",
    "brand": "SoundCore",
    "model": "Pro X",
    "unit_size": 1,
    "weight": 0.35,
    "dimensions": {
      "length": 7.5,
      "width": 6.8,
      "height": 3.2,
      "unit": "inches"
    },
    "attributes": {
      "color": "black",
      "connectivity": "bluetooth",
      "battery_life": "24h"
    },
    "category_ids": [
      "category-uuid",
      "nested-category-uuid"
    ],
    "category": {
      "id": "category-uuid",
      "name": {
        "en": "Electronics"
      },
      "parent_id": null
    },
    "sub_category": {
      "id": "nested-category-uuid",
      "name": {
        "en": "Headphones"
      },
      "parent_id": "category-uuid"
    },
    "categories": [
      {
        "id": "category-uuid",
        "name": {
          "en": "Electronics"
        }
      },
      {
        "id": "nested-category-uuid",
        "name": {
          "en": "Headphones"
        }
      }
    ],
    "is_active": true,
    "created_at": "2023-04-15T08:30:00Z",
    "updated_at": "2023-04-15T08:30:00Z"
  },
  "stock_levels": [
    {
      "id": "stock-uuid-1",
      "warehouse_id": "warehouse-uuid-1",
      "warehouse": {
        "id": "warehouse-uuid-1",
        "name": "Main Warehouse",
        "code": "WH001"
      },
      "bin_id": "bin-uuid-1",
      "bin": {
        "id": "bin-uuid-1",
        "code": "A-01-01",
        "name": "Section A, Shelf 1, Bin 1"
      },
      "quantity": 15,
      "status": "available",
      "lot_number": "LOT20230401",
      "expiration_date": "2025-04-01T00:00:00Z"
    }
  ],
  "outbound_delivery": {
    "id": "delivery-uuid",
    "delivery_code": "DEL1001",
    "customer_name": "John Doe",
    "status": "draft"
  },
  "inbound_delivery": null
}
```

### Response 404 Error - Item Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/inventory-items/code/:item_code — Get Inventory Item By Code

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-items-management#get-get-inventory-item-by-code)

Retrieves an inventory item by numeric item_code. Excludes label fields and enriches with delivery summary, receiving order, stock details, category, sub category, and `categories` when applicable. `categories` is returned as an array of `{ id, name }`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-items/code/:item_code?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| item_code | path string | Yes | Numeric item code | 1048 |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory item retrieved successfully",
  "item": {
    "id": "item-uuid",
    "tenant_id": "tenant-uuid",
    "sku": "PKG-1756815438880",
    "name": "Garbalia Black Unisex Wallet",
    "unit_size": "1.00",
    "category_ids": [
      "category-uuid",
      "nested-category-uuid"
    ],
    "category": {
      "id": "category-uuid",
      "name": {
        "en": "Accessories"
      },
      "parent_id": null
    },
    "sub_category": {
      "id": "nested-category-uuid",
      "name": {
        "en": "Wallets"
      },
      "parent_id": "category-uuid"
    },
    "categories": [
      {
        "id": "category-uuid",
        "name": {
          "en": "Accessories"
        }
      },
      {
        "id": "nested-category-uuid",
        "name": {
          "en": "Wallets"
        }
      }
    ],
    "is_active": true,
    "item_code": 1048,
    "barcode": "INV1048"
  },
  "outbound_delivery": {
    "id": "delivery-uuid",
    "delivery_code": "DEL1001",
    "customer_name": "John Doe",
    "pickup_full_address": "Pickup address...",
    "dropoff_full_address": "Dropoff address...",
    "status": "draft"
  },
  "inbound_delivery": null,
  "receiving_order": {
    "id": "ro-uuid",
    "reference_number": "RO-0001",
    "status": "pending",
    "warehouse": {
      "name": "Main Warehouse",
      "code": "WH001"
    },
    "item_details": [
      {
        "item_id": "item-uuid",
        "quantity": 1
      }
    ]
  },
  "stock": [
    {
      "id": "stock-uuid",
      "quantity": 5,
      "reserved_quantity": 0,
      "status": "available",
      "warehouse": {
        "id": "wh-uuid",
        "name": "Main Warehouse",
        "code": "WH001"
      },
      "bin": {
        "id": "bin-uuid",
        "code": "A-01-01",
        "name": "Section A, Shelf 1, Bin 1"
      }
    }
  ]
}
```

### Response 404 Error - Item Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found"
}
```

## PATCH /warehousing/inventory-items/:id — Update Inventory Item

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-items-management#patch-update-inventory-item)

Updates an existing inventory item with new details. Only the provided fields will be updated, while others remain unchanged.

- User must have 'manage:operations:tenant' permission

### Business Rules
- At least one field must be provided for update
- If providing SKU, it must be unique within the tenant
- Unknown/forbidden fields are rejected with 400 (error: forbidden_fields_present)
- If no valid fields provided, 400 (error: no_valid_fields_to_update)
- Numeric values (unit_size, weight, dimensions) must be positive if provided

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PATCH https://staging-api.ondi.io/functions/v1/warehousing/inventory-items/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the inventory item to update (required) | item-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| description | string |  |  |  |
| brand | string |  |  |  |
| dimensions | object |  |  |  |
| dimensions.length | number |  |  |  |
| dimensions.width | number |  |  |  |
| dimensions.height | number |  |  |  |
| dimensions.unit | string |  |  |  |
| attributes | object |  |  |  |
| attributes.color | string |  |  |  |
| attributes.connectivity | string |  |  |  |
| attributes.battery_life | string |  |  |  |
| category_ids | array<string> |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "name": "Wireless Headphones Pro",
  "description": "Updated description for premium noise-canceling wireless headphones",
  "brand": "SoundCore Pro",
  "dimensions": {
    "length": 7.5,
    "width": 6.8,
    "height": 3.2,
    "unit": "inches"
  },
  "attributes": {
    "color": "black",
    "connectivity": "bluetooth 5.0",
    "battery_life": "30h"
  },
  "category_ids": [
    "{{category_id_1}}",
    "{{category_id_2}}"
  ],
  "is_active": true
}
```

### Request example 1

```json
{
  "name": "string", // optional
  "sku": "string", // optional, must be unique within tenant
  "description": "string", // optional
  "brand": "string", // optional
  "model": "string", // optional
  "unit_size": number, // optional, must be positive
  "weight": number, // optional, must be positive
  "dimensions": { // optional
    "length": number, // must be positive
    "width": number, // must be positive
    "height": number, // must be positive
    "unit": "string" // unit of measurement
  },
  "attributes": { // optional
    "color": "string",
    "size": "string",
    "material": "string",
    "custom_attributes": {
      "key1": "value1",
      "key2": "value2"
    }
  },
  "category_ids": ["uuid", "uuid"], // optional, replaces existing category links
  "is_active": boolean // optional
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory item updated successfully",
  "item": {
    "id": "12345678-1234-1234-1234-123456789012",
    "tenant_id": "tenant-uuid",
    "name": "Wireless Headphones Pro",
    "sku": "WH-200-BLK",
    "barcode": "INV1048",
    "description": "Updated description for premium noise-canceling wireless headphones",
    "brand": "SoundCore Pro",
    "model": "Pro X",
    "unit_size": 1,
    "weight": 0.35,
    "dimensions": {
      "length": 7.5,
      "width": 6.8,
      "height": 3.2,
      "unit": "inches"
    },
    "attributes": {
      "color": "black",
      "connectivity": "bluetooth 5.0",
      "battery_life": "30h"
    },
    "category_ids": [
      "category-uuid",
      "nested-category-uuid"
    ],
    "category": {
      "id": "category-uuid",
      "name": {
        "en": "Electronics"
      },
      "parent_id": null
    },
    "sub_category": {
      "id": "nested-category-uuid",
      "name": {
        "en": "Headphones"
      },
      "parent_id": "category-uuid"
    },
    "categories": [
      {
        "id": "category-uuid",
        "name": {
          "en": "Electronics"
        }
      },
      {
        "id": "nested-category-uuid",
        "name": {
          "en": "Headphones"
        }
      }
    ],
    "is_active": true,
    "created_at": "2023-04-15T08:30:00Z",
    "updated_at": "2023-04-20T10:15:00Z"
  }
}
```

### Response 404 Error - Item Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found"
}
```

### Response 400 Error - Duplicate SKU



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Duplicate SKU"
}
```

### Response 400 Error - Forbidden Fields Present



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request body",
  "error": "forbidden_fields_present",
  "fields": [
    "barcode"
  ]
}
```

### Response 400 Error - No Valid Fields To Update



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request body",
  "error": "no_valid_fields_to_update"
}
```

### Response 400 Error - Invalid Unit Size



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Unit size must be a positive number"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## POST /warehousing/inventory-items/:id/execute-delivery — Execute Delivery from Inventory Item

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-items-management#post-execute-delivery-from-inventory-item)

Executes a delivery for an inventory item in one explicit warehouse. Optionally receives the item first if `bin_id` is provided.

- The actor must have an active `warehouse_staff` assignment for `warehouse_id`.

### Business Rules
- The requested warehouse must be the delivery's current warehouse.
- Only positive `available` stock in that exact warehouse can be prepared.
- The delivery must be `draft`.
- For a warehouse-staged P&D or operational-return order, success keeps stock quantity in the warehouse, reserves it, creates one completed picking order, and returns `handoff: null`. Assignment creates the handoff later with driver context.
- Existing non-P&D execution creates a pending handoff and returns its object, including `handoff.id` and `handoff.status`.
- HTTP 403 `warehouse_access_denied` means the actor is not active staff of the requested warehouse.
- HTTP 409 `receiving_order_not_current_warehouse` means custody belongs to another warehouse.

### Frontend integration contract
- Send the warehouse currently open in the warehouse app as literal `warehouse_id`.
- For a P&D success, treat `handoff: null` as prepared and waiting for assignment, not as an error.
- Do not retry with a different warehouse inferred from the response.
- Hide/disable Execute after either custody conflict, and show the localized backend message.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/inventory-items/:id/execute-delivery?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the inventory item (required). | item-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string | Yes | is required. |  |
| bin_id | string | No | is optional. |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "bin_id": "{{bin_id}}"
}
```

### Request example 1

```json
{
  "warehouse_id": "warehouse-uuid",
  "bin_id": "bin-uuid"
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery executed successfully from item",
  "inventory_item": {
    "id": "item-uuid",
    "name": "Product Name",
    "sku": "SKU-123",
    "delivery_id": "delivery-uuid"
  },
  "delivery": {
    "id": "delivery-uuid",
    "status": "pending"
  },
  "stock_update": {
    "previous_quantity": 5,
    "new_quantity": 5,
    "reserved_quantity": 5
  },
  "picking_order": {
    "id": "picking-order-uuid",
    "reference_number": "PKO-1234567890",
    "status": "completed"
  },
  "handoff": null
}
```

### Response 404 Error - Item Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found"
}
```

### Response 400 Error - No Delivery Assigned



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item has no delivery assigned"
}
```

### Response 400 Error - Delivery Status Must Be Draft



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Delivery status must be draft"
}
```

### Response 404 Error - Item Not Found in Stock



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item not found in inventory stock"
}
```

### Response 409 Error - Parent Exception Open



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "parent_exception_open",
  "message": "Resolve the parent order exception before starting this delivery"
}
```

### Response 403 Error - Warehouse Access Denied



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "warehouse_access_denied",
  "message": "You are not assigned to this warehouse and cannot execute this delivery",
  "details": {
    "warehouse_id": "warehouse-uuid"
  }
}
```

### Response 500 Wallet Error Contract - Warehouse Execution



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "insufficient_available_balance",
  "message": "Customer has reached the Minimum Wallet Balance limit."
}
```

## POST /warehousing/inventory-items/execute-deliveries — Execute Deliveries from Inventory Items

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-items-management#post-execute-deliveries-from-inventory-items)

Executes outbound deliveries for multiple inventory items in one explicit warehouse.

- The actor must have an active `warehouse_staff` assignment for `warehouse_id`. Access is checked once for the batch.

### Business Rules
- Every item is scoped to positive `available` stock in the requested warehouse.
- Every delivery must currently belong to the requested warehouse and be `draft`.
- Warehouse-staged P&D and operational-return rows are prepared in one set-based call: stock quantity stays in the warehouse and becomes reserved, one completed pick is created per delivery, and each result returns `handoff: null`.
- Existing non-P&D execution creates a pending handoff and returns its object, including `handoff.id` and `handoff.status`, in that item result.
- Duplicate item IDs are skipped with `duplicate_item_id`.

### Frontend integration contract
- Send the warehouse currently open in the warehouse app as literal `warehouse_id`.
- Use `executed_count`, `failed_count`, and `results[]` for item results.
- For a P&D success, treat `handoff: null` as prepared and waiting for assignment.
- Treat top-level HTTP 403 `warehouse_access_denied` as a warehouse authorization failure, not an item failure.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/inventory-items/execute-deliveries?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| item_ids | array<string> | Yes | are required. |  |
| bin_id | string | No | is optional. |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "item_ids": [
    "item-uuid-1",
    "item-uuid-2"
  ],
  "bin_id": "{{bin_id}}"
}
```

### Request example 1

```json
{
  "warehouse_id": "warehouse-uuid",
  "item_ids": [
    "item-uuid-1",
    "item-uuid-2"
  ],
  "bin_id": "bin-uuid"
}
```

### Response 200 Success - Non-P&D Execution With Handoff



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Some item deliveries executed successfully",
  "executed_count": 1,
  "failed_count": 1,
  "results": [
    {
      "item_id": "item-uuid-1",
      "success": true,
      "status": 200,
      "message": "Delivery executed successfully from item",
      "inventory_item": {
        "id": "item-uuid-1",
        "name": "Product Name",
        "sku": "SKU-123",
        "outbound_delivery_id": "delivery-uuid-1"
      },
      "delivery": {
        "id": "delivery-uuid-1",
        "status": "pending"
      },
      "stock_update": {
        "previous_quantity": 1,
        "new_quantity": 0,
        "reserved_quantity": 1
      },
      "picking_order": {
        "id": "picking-order-uuid",
        "status": "completed"
      },
      "handoff": {
        "id": "handoff-uuid",
        "status": "pending"
      }
    },
    {
      "item_id": "item-uuid-2",
      "success": false,
      "status": 400,
      "message": "Delivery status must be draft"
    }
  ]
}
```

### Response 200 Success - Partial or Full Batch



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Some item deliveries executed successfully",
  "executed_count": 1,
  "failed_count": 1,
  "results": [
    {
      "item_id": "item-uuid-1",
      "success": true,
      "status": 200,
      "message": "Delivery executed successfully from item",
      "inventory_item": {
        "id": "item-uuid-1",
        "name": "Product Name",
        "sku": "SKU-123",
        "outbound_delivery_id": "delivery-uuid-1"
      },
      "delivery": {
        "id": "delivery-uuid-1",
        "status": "pending"
      },
      "stock_update": {
        "previous_quantity": 1,
        "new_quantity": 1,
        "reserved_quantity": 1
      },
      "picking_order": {
        "id": "picking-order-uuid",
        "status": "completed"
      },
      "handoff": null
    },
    {
      "item_id": "item-uuid-2",
      "success": false,
      "status": 400,
      "message": "Delivery status must be draft"
    }
  ]
}
```

### Response 400 Error - No Items Executed



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "No item deliveries were executed",
  "executed_count": 0,
  "failed_count": 1,
  "results": [
    {
      "item_id": "item-uuid-1",
      "success": false,
      "status": 400,
      "message": "Delivery status must be draft"
    }
  ]
}
```

### Response 400 Wallet Error Contract - Warehouse Bulk Execution



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "No item deliveries were executed",
  "executed_count": 0,
  "failed_count": 1,
  "results": [
    {
      "item_id": "item-uuid-1",
      "success": false,
      "status": 500,
      "code": "insufficient_available_balance",
      "message": "Customer has reached the Minimum Wallet Balance limit."
    }
  ]
}
```

## GET /warehousing/locations — List Warehouse Locations

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-locations#get-list-warehouse-locations)

Retrieves warehouse locations with filtering options by warehouse, location type, status, and parent location. Supports pagination and hierarchical data exploration.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns a paginated list of locations with their hierarchical relationships
- Includes parent location details where applicable
- Provides available location types and statuses for filtering
- Includes pagination information (total count, current page, limit)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/locations?tenant_id={{tenant_id}}&warehouse_id=&location_type=&status=&parent_id=&search=&sort_by=updated_at&sort_order=desc&column_filters=&page=1&limit=20&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |
| warehouse_id | query string | Yes | Legacy: filter by warehouse UUID (optional). Overridden by column_filters.warehouse_id if both present. UUID of the warehouse to filter locations by |  |
| location_type | query string | No | Legacy: filter by location type — zone \| aisle \| shelf \| bin (optional). Overridden by column_filters.location_type if both present. Filter by location type (zone, aisle, shelf, bin) |  |
| status | query string | No | Legacy: filter by status — active \| inactive \| maintenance (optional). Overridden by column_filters.status if both present. Filter by location status |  |
| parent_id | query string | No | Legacy: filter by parent location UUID. Use 'null' for top-level locations (optional). Overridden by column_filters.parent_id if both present. Filter by parent location ID (use 'null' for top-level locations) |  |
| search | query string | No | Full-text search across code, name, barcode (optional) |  |
| sort_by | query string | No | Sort column — code \| name \| created_at \| updated_at (optional, default: updated_at) | updated_at |
| sort_order | query string | No | Sort direction — asc \| desc (optional, default: desc) | desc |
| column_filters | query string |  | JSON object of column filters. Supported keys: warehouse_id (exact), parent_id (exact), location_type (exact), status (exact), code (contains), name (contains), barcode (contains). Takes precedence over legacy standalone params. |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 20) Number of items per page (default: 20) | 20 |
| language | query string | No | Response language (optional, default: en) | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse locations retrieved successfully",
  "locations": [
    {
      "id": "location-uuid-1",
      "location_type": "zone",
      "code": "Z-001",
      "barcode": null,
      "label_url": null,
      "name": "North Zone",
      "status": "active",
      "default_receiving_location": false,
      "position": {
        "area": "North",
        "floor": "1"
      },
      "warehouse": {
        "id": "warehouse-uuid",
        "code": "WH-01",
        "name": "Main Warehouse"
      },
      "parent": null
    },
    {
      "id": "location-uuid-2",
      "location_type": "aisle",
      "code": "A-101",
      "barcode": "BC-A101",
      "label_url": null,
      "name": "Aisle 101",
      "status": "active",
      "default_receiving_location": true,
      "position": {
        "number": "101",
        "section": "A"
      },
      "warehouse": {
        "id": "warehouse-uuid",
        "code": "WH-01",
        "name": "Main Warehouse"
      },
      "parent": {
        "id": "location-uuid-1",
        "code": "Z-001",
        "name": "North Zone",
        "location_type": "zone"
      }
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 60,
  "stats": {
    "total": 60,
    "active": 50,
    "inactive": 5,
    "maintenance": 5
  }
}
```

### Response 200 Success Response — with column_filters + sort

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse locations retrieved successfully",
  "locations": [
    {
      "id": "location-uuid-2",
      "location_type": "aisle",
      "code": "A-101",
      "barcode": "BC-A101",
      "label_url": null,
      "name": "Aisle 101",
      "status": "active",
      "default_receiving_location": true,
      "position": {
        "number": "101",
        "section": "A"
      },
      "warehouse": {
        "id": "warehouse-uuid",
        "code": "WH-01",
        "name": "Main Warehouse"
      },
      "parent": {
        "id": "location-uuid-1",
        "code": "Z-001",
        "name": "North Zone",
        "location_type": "zone"
      }
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1,
  "stats": {
    "total": 60,
    "active": 50,
    "inactive": 5,
    "maintenance": 5
  }
}
```

### Response 400 Error - Warehouse ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse ID required"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /warehousing/locations — Create Warehouse Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-locations#post-create-warehouse-location)

Creates a new warehouse location with hierarchical organization. Supports different location types (zone, aisle, shelf, bin) with appropriate parent-child relationships.

- User must have 'manage:operations:tenant' permission

### Business rules
- Only one active, non-deleted location per warehouse can have `default_receiving_location=true`.
- Setting this flag to true clears the previous default receiving location in the same warehouse.

### System-generated fields
- `code` is auto-generated from `location_type` and warehouse sequence
- `barcode` is auto-generated from warehouse code + location code
- `label_url` is generated asynchronously after the label job completes

### Location Hierarchy Rules
- zone → aisle → shelf → bin (parent-child relationship)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/locations?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| location_type | string |  |  |  |
| name | string |  |  |  |
| parent_id | string |  |  |  |
| position | object |  |  |  |
| position.position | string |  |  |  |
| position.sequence | number |  |  |  |
| capacity | object |  |  |  |
| capacity.max_weight | number |  |  |  |
| capacity.max_volume | number |  |  |  |
| capacity.dimensions | object |  |  |  |
| capacity.dimensions.length | number |  |  |  |
| capacity.dimensions.width | number |  |  |  |
| capacity.dimensions.height | number |  |  |  |
| capacity.item_limit | number |  |  |  |
| status | string |  |  |  |
| default_receiving_location | boolean |  |  |  |
| metadata | object |  |  |  |
| metadata.temperature_range | object |  |  |  |
| metadata.temperature_range.min | number |  |  |  |
| metadata.temperature_range.max | number |  |  |  |
| metadata.special_handling | array<string> |  |  |  |
| metadata.security_level | string |  |  |  |
| metadata.access_restrictions | array<string> |  |  |  |
| metadata.notes | string |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "location_type": "bin",
  "name": "Storage Bin 1001",
  "parent_id": "{{parent_location_id}}",
  "position": {
    "position": "front",
    "sequence": 1
  },
  "capacity": {
    "max_weight": 100,
    "max_volume": 50,
    "dimensions": {
      "length": 30,
      "width": 40,
      "height": 20
    },
    "item_limit": 20
  },
  "status": "active",
  "default_receiving_location": true,
  "metadata": {
    "temperature_range": {
      "min": 15,
      "max": 25
    },
    "special_handling": [
      "fragile",
      "dust-free"
    ],
    "security_level": "high",
    "access_restrictions": [
      "key-card",
      "supervisor"
    ],
    "notes": "For high-value items only"
  }
}
```

### Request example 1

```json
{
  "warehouse_id": "uuid", // required
  "location_type": "string", // required (zone, aisle, shelf, bin)
  "name": "string", // optional, human-readable name
  "parent_id": "uuid", // optional, reference to parent location
  "position": {}, // required, position information based on location type
  "capacity": {}, // optional, storage capacity details
  "status": "string", // optional, defaults to "active"
  "default_receiving_location": true, // optional; warehouse fallback receiving location
  "metadata": {} // optional, additional location metadata
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse location created successfully",
  "location": {
    "id": "new-location-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "parent_id": "parent-location-uuid",
    "location_type": "bin",
    "code": "B-1001",
    "name": "Storage Bin 1001",
    "position": {
      "position": "front",
      "sequence": 1
    },
    "capacity": {
      "max_weight": 100,
      "max_volume": 50,
      "dimensions": {
        "length": 30,
        "width": 40,
        "height": 20
      },
      "item_limit": 20
    },
    "status": "active",
    "default_receiving_location": true,
    "metadata": {
      "temperature_range": {
        "min": 15,
        "max": 25
      },
      "special_handling": [
        "fragile",
        "dust-free"
      ],
      "security_level": "high",
      "access_restrictions": [
        "key-card",
        "supervisor"
      ],
      "notes": "For high-value items only"
    },
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-01T10:00:00Z",
    "deleted_at": null
  }
}
```

### Response 400 Error - Missing Required Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: position"
}
```

### Response 400 Error - Invalid Location Type



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid location type: unknown. Must be one of: zone, aisle, shelf, bin"
}
```

### Response 400 Error - Invalid Location Hierarchy



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid location hierarchy"
}
```

### Response 400 Error - Invalid Location Hierarchy



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid location hierarchy"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Parent Location Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Parent location not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/locations/:id — Get Warehouse Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-locations#get-get-warehouse-location)

Retrieves detailed information about a specific warehouse location by its ID or barcode, including parent location and warehouse details.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Frontend integration contract
- For location barcode scans in receiving, call `GET /warehousing/locations/:barcode?search_type=barcode&warehouse_id={{warehouse_id}}`.
- On success, replace the currently selected receiving location with `location.id`.
- Handle HTTP 404 as an invalid or wrong-warehouse location scan.

### Response
- Returns detailed location information
- Includes `default_receiving_location` to indicate whether this is the warehouse fallback receiving location
- Includes parent location data (if applicable)
- Includes warehouse information

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/locations/:id?tenant_id={{tenant_id}}&search_type=&warehouse_id=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the location to retrieve (required) UUID of the location to retrieve, or barcode when `search_type=barcode` |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |
| search_type | query string | No | Optional. Set to 'barcode' to treat :id as barcode Set to `barcode` to treat `:id` as a location barcode |  |
| warehouse_id | query string | No | Optional. Restricts the lookup to one warehouse. Use this when scanning a location barcode from a receiving flow. Restrict lookup to a warehouse. Frontend should pass this during receiving location scans so a scanned location from another warehouse returns 404. |  |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Location retrieved successfully",
  "location": {
    "id": "location-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "parent_id": "parent-location-uuid",
    "location_type": "bin",
    "code": "B-1001",
    "name": "Storage Bin 1001",
    "position": {
      "position": "front",
      "sequence": 1
    },
    "capacity": {
      "max_weight": 100,
      "max_volume": 50,
      "dimensions": {
        "length": 30,
        "width": 40,
        "height": 20
      },
      "item_limit": 20
    },
    "status": "active",
    "default_receiving_location": true,
    "metadata": {
      "temperature_range": {
        "min": 15,
        "max": 25
      },
      "special_handling": [
        "fragile",
        "dust-free"
      ],
      "security_level": "high",
      "access_restrictions": [
        "key-card",
        "supervisor"
      ],
      "notes": "For high-value items only"
    },
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-01T10:00:00Z",
    "deleted_at": null,
    "parent": {
      "id": "parent-location-uuid",
      "location_type": "shelf",
      "code": "S-101",
      "name": "Shelf 101"
    },
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH-001"
    }
  }
}
```

### Response 400 Error - Location ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Location ID required"
}
```

### Response 404 Error - Location Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Location not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## PUT /warehousing/locations/:id — Update Warehouse Location

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-locations#put-update-warehouse-location)

Updates an existing warehouse location with new information. Only specified fields will be updated.

- User must have 'manage:operations:tenant' permission

### Business rules
- Only one active, non-deleted location per warehouse can have `default_receiving_location=true`.

### Note
- Only specified fields are updated and `warehouse_id` cannot be changed.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/locations/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the location to update (required) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| location_type | string |  |  |  |
| parent_id | string |  |  |  |
| status | string |  |  |  |
| default_receiving_location | boolean |  |  |  |
| position | object |  |  |  |
| position.position | string |  |  |  |
| position.sequence | number |  |  |  |
| capacity | object |  |  |  |
| capacity.max_weight | number |  |  |  |
| capacity.item_limit | number |  |  |  |
| metadata | object |  |  |  |
| metadata.notes | string |  |  |  |
| metadata.security_level | string |  |  |  |



### Request body (raw)

```json
{
  "name": "Updated Location Name",
  "location_type": "bin",
  "parent_id": "{{parent_location_id}}",
  "status": "active",
  "default_receiving_location": true,
  "position": {
    "position": "back",
    "sequence": 2
  },
  "capacity": {
    "max_weight": 150,
    "item_limit": 25
  },
  "metadata": {
    "notes": "Updated location notes",
    "security_level": "medium"
  }
}
```

### Request example 1

```json
{
  "name": "string",
  "status": "string",
  "default_receiving_location": true,
  "position": {},
  "capacity": {},
  "metadata": {}
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Location updated successfully",
  "location": {
    "id": "location-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "parent_id": "parent-location-uuid",
    "location_type": "bin",
    "code": "B001",
    "barcode": "WLCWH001B001",
    "label_url": null,
    "name": "Updated Location Name",
    "position": {
      "position": "back",
      "sequence": 2
    },
    "capacity": {
      "max_weight": 150,
      "item_limit": 25
    },
    "status": "active",
    "default_receiving_location": true,
    "metadata": {
      "notes": "Updated location notes",
      "security_level": "medium"
    },
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-02T10:00:00Z",
    "deleted_at": null
  }
}
```

### Response 400 Error - Location ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Location ID required"
}
```

### Response 400 Error - Invalid Position Structure



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Bin position must include 'position' and 'sequence'"
}
```

### Response 404 Error - Location Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Location not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 400 Error - Invalid Request Body



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid request body"
}
```

## GET /warehousing/warehouses — List Warehouses

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouses#get-list-warehouses-2)

Retrieves a list of warehouses with optional filtering and pagination. Returns warehouses with their address information.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns a paginated list of warehouses
- Includes address information for each warehouse
- Provides pagination information (total count, current page, limit)

List warehouses with pagination, sorting, column filters and stats.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/warehouses?page=1&limit=10&status=&code=&search=&sort_by=&sort_order=desc&column_filters=&total_capacity_order=&available_capacity_order=&customer_id=&tenant_id=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string | No | Page number (default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Items per page (default: 10) Number of items per page (default: 10) | 10 |
| status | query string | No | Legacy: filter by status (active\|inactive\|maintenance\|full) Filter by warehouse status (e.g., active, inactive, maintenance) |  |
| code | query string | No | Legacy: exact code filter Filter by warehouse code |  |
| search | query string | No | Search by name or code (ILIKE) Search warehouses by name or code |  |
| sort_by | query string |  | Sort column: name \| code \| status \| total_capacity \| available_capacity \| created_at (default) |  |
| sort_order | query string |  | asc or desc (default: desc). Overridden by total_capacity_order/available_capacity_order if set. | desc |
| column_filters | query string |  | JSON object. Keys: name (ILIKE), code (ILIKE), status (exact), access_mode (exact: all_users\|exclude_users\|select_users), total_capacity (number or {min?,max?}), available_capacity (number or {min?,max?}), created_at ({from?,to?}). Example: {"status":"active","total_capacity":{"min":100}} |  |
| total_capacity_order | query string | No | Legacy sort: asc\|desc (takes precedence over sort_by) Sort by total_capacity. Values: asc \| desc |  |
| available_capacity_order | query string | No | Legacy sort: asc\|desc (takes precedence over sort_by) Sort by available_capacity. Values: asc \| desc |  |
| customer_id | query string | No | Filter by customer access mapping (business user_id). Applied implicitly for customer-role users. Apply customer-based access filter (user_id as customer_id). If the current user is a business customer or has no role, filtering is applied implicitly. |  |
| carrier_code | query string |  | When provided, returns carrier_config_present + external_integration per warehouse |  |
| tenant_id | query string |  | Tenant ID (optional if in auth token) (required if not in auth): UUID of the tenant |  |
| language | query string |  |  | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "warehouses_retrieved_successfully",
  "warehouses": [
    {
      "id": "uuid",
      "tenant_id": "uuid",
      "name": "Main Distribution Center",
      "code": "WH001",
      "status": "active",
      "access_mode": "all_users",
      "total_capacity": 10000,
      "available_capacity": 7500,
      "created_at": "2025-01-15T08:00:00Z",
      "address": {
        "id": "uuid",
        "full_address": "123 Warehouse Blvd",
        "city": "Metropolis",
        "country": "US",
        "latitude": 40.7128,
        "longitude": -74.006
      }
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 42,
  "stats": {
    "totalWarehouses": 50,
    "activeCount": 35,
    "maintenanceCount": 5,
    "inactiveCount": 10,
    "totalCapacity": 500000,
    "availableCapacity": 320000
  }
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /warehousing/warehouses — Create Warehouse

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouses#post-create-warehouse)

Creates a new warehouse with specified details including address, capacity, access control, and special features.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Warehouse code must be unique within a tenant
- Address must exist and belong to the specified tenant
- If total_capacity is provided but available_capacity is not, available_capacity will default to total_capacity
- If features are provided, they must only contain valid feature keys
- When `access_mode` is `select_users`, only mapped `user_ids` can access; when `exclude_users`, all except provided `user_ids` can access
- `user_ids` must belong to business users of the tenant; otherwise request fails with `invalid_customer_ids`

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/warehouses?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| code | string |  |  |  |
| address_id | string |  |  |  |
| status | string |  |  |  |
| total_capacity | number |  |  |  |
| available_capacity | number |  |  |  |
| features | object |  |  |  |
| features.climate_control | boolean |  |  |  |
| features.security_level | string |  |  |  |
| features.dock_doors | number |  |  |  |
| features.hazardous_materials | boolean |  |  |  |
| features.refrigeration | boolean |  |  |  |
| features.cross_docking | boolean |  |  |  |
| access_mode | string |  |  |  |
| user_ids | array |  |  |  |



### Request body (raw)

```json
{
  "name": "Main Distribution Center",
  "code": "WH001",
  "address_id": "{{address_id}}",
  "status": "active",
  "total_capacity": 10000,
  "available_capacity": 7500,
  "features": {
    "climate_control": true,
    "security_level": "high",
    "dock_doors": 12,
    "hazardous_materials": false,
    "refrigeration": true,
    "cross_docking": true
  },
  "access_mode": "all_users",
  "user_ids": []
}
```

### Request example 1

```json
{
  "name": "string", // required - name of the warehouse
  "code": "string", // required - unique identifier code
  "address_id": "uuid", // required - reference to an existing address
  "status": "string", // optional - defaults to "active"
  "total_capacity": number, // optional - total storage capacity
  "available_capacity": number, // optional - available storage capacity
  "features": { // optional - special warehouse features
    "climate_control": boolean,
    "security_level": string,
    "dock_doors": number,
    "hazardous_materials": boolean,
    "refrigeration": boolean,
    "cross_docking": boolean
  },
  "access_mode": "all_users|select_users|exclude_users", // optional - defaults to all_users
  "user_ids": ["uuid", ...] // required when access_mode is select_users or exclude_users (business users only)
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse created successfully",
  "warehouse": {
    "id": "warehouse-uuid",
    "tenant_id": "tenant-uuid",
    "name": "Main Distribution Center",
    "code": "WH001",
    "address_id": "address-uuid",
    "status": "active",
    "total_capacity": 10000,
    "available_capacity": 7500,
    "features": {
      "climate_control": true,
      "security_level": "high",
      "dock_doors": 12,
      "hazardous_materials": false,
      "refrigeration": true,
      "cross_docking": true
    },
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-01T10:00:00Z",
    "deleted_at": null
  }
}
```

### Response 400 Error - Invalid Access Mode



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid access mode"
}
```

### Response 400 Error - Customer IDs Required For Selected Mode



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Customer IDs required for selected access mode"
}
```

### Response 400 Error - Invalid Customer IDs



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid customer IDs",
  "invalid_customer_ids": [
    "user-uuid-not-in-tenant"
  ]
}
```

### Response 400 Error - Missing Required Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: name"
}
```

### Response 400 Error - Code Already Exists



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse code already exists"
}
```

### Response 400 Error - Invalid Feature Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid feature field: invalid_feature"
}
```

### Response 404 Error - Address Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Address not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /warehousing/warehouses/:id — Get Warehouse

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouses#get-get-warehouse)

Retrieves detailed information about a specific warehouse, including its address information.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns detailed warehouse information
- Includes `warehouse.address.title` as the stored localized title object, alongside the existing address fields
- Contains feature specifications and capacity information

### Frontend integration contract
- Read the address label from `warehouse.address.title` using the frontend's existing localized-object handling. No request fields, actions, IDs, navigation behavior, or error handling changed.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/warehouses/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the warehouse to retrieve (required) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse retrieved successfully",
  "warehouse": {
    "id": "warehouse-uuid",
    "tenant_id": "tenant-uuid",
    "name": "Main Distribution Center",
    "code": "WH001",
    "address_id": "address-uuid",
    "status": "active",
    "total_capacity": 10000,
    "available_capacity": 7500,
    "features": {
      "climate_control": true,
      "security_level": "high",
      "dock_doors": 12,
      "hazardous_materials": false,
      "refrigeration": true,
      "cross_docking": true
    },
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-01T10:00:00Z",
    "deleted_at": null,
    "address": {
      "id": "address-uuid",
      "title": {
        "en": "Main Distribution Center"
      },
      "full_address": "123 Warehouse Blvd, Industrial District",
      "city": "Metropolis",
      "country": "US",
      "latitude": 40.7128,
      "longitude": -74.006
    }
  }
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## PUT /warehousing/warehouses/:id — Update Warehouse

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouses#put-update-warehouse)

Updates an existing warehouse with new information. Only the fields included in the request will be updated.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Warehouse must exist and belong to the specified tenant
- Address ID cannot be null or empty
- If a new address ID is provided, it must exist and belong to the tenant
- If code is provided, it must be unique within the tenant (not used by another warehouse)
- Only valid feature fields are allowed in the features object
- If `access_mode` is select/exclude, `user_ids` are required and must refer to business users in the tenant
- Updated timestamp is automatically set to current time

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/warehouses/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the warehouse to update (required) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | string |  |  |  |
| code | string |  |  |  |
| address_id | string |  |  |  |
| status | string |  |  |  |
| total_capacity | number |  |  |  |
| available_capacity | number |  |  |  |
| features | object |  |  |  |
| features.climate_control | boolean |  |  |  |
| features.security_level | string |  |  |  |
| features.dock_doors | number |  |  |  |
| features.hazardous_materials | boolean |  |  |  |
| features.refrigeration | boolean |  |  |  |
| features.cross_docking | boolean |  |  |  |
| access_mode | string |  |  |  |
| user_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "name": "Updated Distribution Center",
  "code": "WH001-UPDATED",
  "address_id": "{{new_address_id}}",
  "status": "maintenance",
  "total_capacity": 12000,
  "available_capacity": 9000,
  "features": {
    "climate_control": true,
    "security_level": "maximum",
    "dock_doors": 15,
    "hazardous_materials": true,
    "refrigeration": true,
    "cross_docking": true
  },
  "access_mode": "select_users",
  "user_ids": [
    "{{customer_id_1}}",
    "{{customer_id_2}}"
  ]
}
```

### Request example 1

```json
{
  // All fields are optional for updates except address_id
  "name": "string",
  "code": "string",
  "address_id": "uuid", // Cannot be set to null
  "status": "string",
  "total_capacity": number,
  "available_capacity": number,
  "features": {
    "climate_control": boolean,
    "security_level": string,
    "dock_doors": number,
    "hazardous_materials": boolean,
    "refrigeration": boolean,
    "cross_docking": boolean
  },
  "access_mode": "all_users|select_users|exclude_users", // optional - if provided, validated against enum
  "user_ids": ["uuid", ...] // required when effective access_mode is select_users or exclude_users
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse updated successfully",
  "warehouse": {
    "id": "warehouse-uuid",
    "tenant_id": "tenant-uuid",
    "name": "Updated Distribution Center",
    "code": "WH001-UPDATED",
    "address_id": "new-address-uuid",
    "status": "maintenance",
    "total_capacity": 12000,
    "available_capacity": 9000,
    "features": {
      "climate_control": true,
      "security_level": "maximum",
      "dock_doors": 15,
      "hazardous_materials": true,
      "refrigeration": true,
      "cross_docking": true
    },
    "created_at": "2023-06-01T10:00:00Z",
    "updated_at": "2023-06-02T10:00:00Z",
    "deleted_at": null
  }
}
```

### Response 400 Error - Invalid Access Mode



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid access mode"
}
```

### Response 400 Error - Customer IDs Required For Selected Mode



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Customer IDs required for selected access mode"
}
```

### Response 400 Error - Invalid Customer IDs



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid customer IDs",
  "invalid_customer_ids": [
    "user-uuid-not-in-tenant"
  ]
}
```

### Response 400 Error - Address ID Cannot Be Null



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Address ID cannot be null"
}
```

### Response 400 Error - Code Already Exists



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse code already exists"
}
```

### Response 400 Error - Invalid Feature Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid feature field: invalid_feature"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Address Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Address not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## DELETE /warehousing/warehouses/:id — Delete Warehouse

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouses#delete-delete-warehouse)

Performs a soft delete of a warehouse by setting the deleted_at timestamp. This maintains referential integrity by not physically removing the record.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Warehouse must exist and belong to the specified tenant
- Warehouse cannot be deleted if it has active inventory
- Operation performs a soft delete by setting the deleted_at timestamp
- No physical deletion of records occurs

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/warehousing/warehouses/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the warehouse to delete (required) |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse deleted successfully"
}
```

### Response 400 Error - Warehouse Has Inventory



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse has inventory"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## GET /warehousing/dashboard — Get Warehouse Staff Dashboard

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-dashboard#get-get-warehouse-staff-dashboard)

## Summary
GET `/warehousing/dashboard?warehouse_id={{warehouse_id}}`

Live warehouse-staff dashboard. It does not use `warehouse_report` or a scheduled report.

## Auth and access
- The user must have a warehouse-related role.
- The user must have an active `warehouse_staff` assignment for `warehouse_id`.
- The warehouse must belong to the current tenant and be active.

## Business rules
- `status` totals current `inventory_stock.quantity` by stock status.
- `recent_activity` is the latest 10 committed inventory events for this tenant and warehouse, newest first.
- Each event has a stable dashboard `type` and display `label`; the source `transaction_type` is not exposed.
- Current types are `inventory_initial_stock`, `inventory_received`, `inventory_picked`, `inventory_moved`, `inventory_adjusted`, `inventory_returned`, `inventory_counted`, `inventory_transferred_in`, `inventory_transferred_out`, and `inventory_transaction` for an unknown source value.
- An empty activity array is a valid success response.

## Frontend contract
- Render Status from the `*_units` fields.
- Render the timeline from `type`, `label`, item, quantity, actor, locations, reference, and timestamp.
- Handle `403` as no active warehouse assignment and `409` as an inactive warehouse.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/dashboard?warehouse_id={{warehouse_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | Bearer {{access_token}} | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | query string | Yes | Required UUID of the active warehouse assigned to the authenticated staff member. | {{warehouse_id}} |



### Response 200 200 OK

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "",
  "warehouse": {
    "id": "{{warehouse_id}}",
    "name": "Main Warehouse",
    "code": "WH-MAIN"
  },
  "status": {
    "available_units": 120,
    "reserved_units": 15,
    "damaged_units": 2,
    "expired_units": 1,
    "other_units": 0,
    "total_units": 138
  },
  "recent_activity": [
    {
      "id": "00000000-0000-4000-8000-000000000001",
      "type": "inventory_received",
      "label": "Received",
      "quantity": 12,
      "timestamp": "2026-07-13T10:00:00.000Z",
      "item": {
        "id": "00000000-0000-4000-8000-000000000002",
        "name": "Packing Box",
        "sku": "BOX-001"
      },
      "performed_by": {
        "id": "00000000-0000-4000-8000-000000000003",
        "name": "Warehouse Staff"
      },
      "from_location": null,
      "to_location": {
        "id": "00000000-0000-4000-8000-000000000004",
        "code": "A-01"
      },
      "reference": {
        "id": "00000000-0000-4000-8000-000000000005",
        "type": "receiving_order"
      }
    }
  ]
}
```

### Response 400 400 Bad Request (missing warehouse ID)

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse Id Required"
}
```

### Response 400 400 Bad Request (invalid warehouse ID)

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "invalid_warehouse_id"
}
```

### Response 403 403 Forbidden (warehouse role required)

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "A warehouse role is required to access this dashboard"
}
```

### Response 403 403 Forbidden (not assigned)

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "You are not assigned to this warehouse"
}
```

### Response 404 404 Not Found (warehouse)

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse Not Found"
}
```

### Response 409 409 Conflict (inactive warehouse)

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse is inactive"
}
```

### Response 500 500 Internal Server Error

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal Server Error"
}
```

## GET /warehousing/inventory-categories — List Inventory Categories

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-categories-management#get-list-inventory-categories)

Retrieves warehouse inventory categories for a tenant with filtering and pagination.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns a paginated list of categories
- Each category includes:
  - `id`, `tenant_id`, `parent_id`, `name` (multi-language object), `description` (multi-language object), `is_active`, `sort_order`, `metadata`, `created_at`, `updated_at`
  - `parent`: if the parent category is present in the current list page, a nested object with the same shape as a category (otherwise `null`)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-categories?tenant_id={{tenant_id}}&parent_id=&root_category=&is_active=&search=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |
| parent_id | query string | No | Filter by parent category ID (use 'null' for root categories). Takes precedence over root_category. (optional) Filter by parent category (UUID). Use 'null' to return only root categories. If provided, it overrides `root_category`. |  |
| root_category | query string | No | When true, only root categories (parent_id IS NULL). When false, only non-root categories (parent_id IS NOT NULL). Ignored if parent_id is provided. (optional) When `true`, return only root categories (parent_id IS NULL); when `false`, only non-root categories (parent_id IS NOT NULL). |  |
| is_active | query string | No | Filter by active status: true or false (optional) Filter by active status (true or false) |  |
| search | query string | No | Search in name and description (optional, across all languages) Case-insensitive search on multilingual `name` and `description` |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 20) Number of items per page (default: 20) | 20 |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse item categories retrieved successfully",
  "categories": [
    {
      "id": "cat-root-uuid",
      "tenant_id": "tenant-uuid",
      "parent_id": null,
      "name": {
        "en": "Electronics",
        "ar": "الكترونيات",
        "ku": "Elektronîk"
      },
      "description": {
        "en": "All electronic items"
      },
      "is_active": true,
      "sort_order": 0,
      "metadata": {},
      "created_at": "2025-01-01T00:00:00Z",
      "updated_at": "2025-01-01T00:00:00Z",
      "parent": null
    },
    {
      "id": "cat-child-uuid",
      "tenant_id": "tenant-uuid",
      "parent_id": "cat-root-uuid",
      "name": {
        "en": "Audio"
      },
      "description": {
        "en": "Headphones, speakers, and audio accessories"
      },
      "is_active": true,
      "sort_order": 10,
      "metadata": {},
      "created_at": "2025-01-01T00:00:00Z",
      "updated_at": "2025-01-01T00:00:00Z",
      "parent": {
        "id": "cat-root-uuid",
        "tenant_id": "tenant-uuid",
        "parent_id": null,
        "name": {
          "en": "Electronics",
          "ar": "الكترونيات",
          "ku": "Elektronîk"
        },
        "description": {
          "en": "All electronic items"
        },
        "is_active": true,
        "sort_order": 0,
        "metadata": {},
        "created_at": "2025-01-01T00:00:00Z",
        "updated_at": "2025-01-01T00:00:00Z"
      }
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 2
}
```

## POST /warehousing/inventory-categories — Create Inventory Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-categories-management#post-create-inventory-category)

Creates a new warehouse inventory category.

- User must have 'manage:operations:tenant' permission

### Business Rules
- `name` is required, must be a multi-language object, and must be unique per tenant (on the stored JSON value)
- `parent_id`, if provided, must reference an existing category for the same tenant

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/inventory-categories?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| name.ar | string |  |  |  |
| name.ku | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| parent_id | null |  |  |  |
| is_active | boolean |  |  |  |
| sort_order | number |  |  |  |
| metadata | object |  |  |  |
| metadata.icon | string |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Electronics",
    "ar": "الكترونيات",
    "ku": "Elektronîk"
  },
  "description": {
    "en": "All electronic inventory items"
  },
  "parent_id": null,
  "is_active": true,
  "sort_order": 0,
  "metadata": {
    "icon": "bolt"
  }
}
```

### Request example 1

```json
{
  "name": { "en": "string", "ar": "string", "ku": "string" }, // required multi-language name
  "description": { "en": "string", "ar": "string", "ku": "string" }, // optional multi-language description
  "parent_id": "uuid", // optional, parent category id or null for root
  "is_active": boolean, // optional, defaults to true
  "sort_order": number, // optional, defaults to 0
  "metadata": {} // optional json object
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse item category created successfully",
  "category": {
    "id": "new-category-uuid",
    "tenant_id": "tenant-uuid",
    "parent_id": null,
    "name": {
      "en": "Electronics"
    },
    "description": {
      "en": "All electronic inventory items"
    },
    "is_active": true,
    "sort_order": 0,
    "metadata": {
      "icon": "bolt"
    },
    "created_at": "2025-01-01T10:00:00Z",
    "updated_at": "2025-01-01T10:00:00Z"
  }
}
```

## GET /warehousing/inventory-categories/:id — Get Inventory Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-categories-management#get-get-inventory-category)

Retrieves a single warehouse inventory category by ID.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-categories/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the inventory category (required) | category-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse item category retrieved successfully",
  "category": {
    "id": "category-uuid",
    "tenant_id": "tenant-uuid",
    "parent_id": "parent-category-uuid",
    "name": {
      "en": "Electronics"
    },
    "description": {
      "en": "All electronic inventory items"
    },
    "is_active": true,
    "sort_order": 0,
    "metadata": {},
    "created_at": "2025-01-01T00:00:00Z",
    "updated_at": "2025-01-01T00:00:00Z",
    "parent": {
      "id": "parent-category-uuid",
      "tenant_id": "tenant-uuid",
      "parent_id": null,
      "name": {
        "en": "Root Category"
      },
      "description": {
        "en": "Parent category description"
      },
      "is_active": true,
      "sort_order": 0,
      "metadata": {},
      "created_at": "2025-01-01T00:00:00Z",
      "updated_at": "2025-01-01T00:00:00Z"
    }
  }
}
```

## PUT /warehousing/inventory-categories/:id — Update Inventory Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-categories-management#put-update-inventory-category)

Updates an existing warehouse inventory category. Only provided fields will be updated.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/inventory-categories/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the inventory category to update (required) | category-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| name | object |  |  |  |
| name.en | string |  |  |  |
| description | object |  |  |  |
| description.en | string |  |  |  |
| is_active | boolean |  |  |  |
| sort_order | number |  |  |  |
| metadata | object |  |  |  |
| metadata.icon | string |  |  |  |



### Request body (raw)

```json
{
  "name": {
    "en": "Electronics & Gadgets"
  },
  "description": {
    "en": "Updated description for electronics inventory items"
  },
  "is_active": true,
  "sort_order": 5,
  "metadata": {
    "icon": "bolt"
  }
}
```

### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse item category updated successfully",
  "category": {
    "id": "category-uuid",
    "tenant_id": "tenant-uuid",
    "parent_id": null,
    "name": {
      "en": "Electronics & Gadgets"
    },
    "description": {
      "en": "Updated description for electronics inventory items"
    },
    "is_active": true,
    "sort_order": 5,
    "metadata": {
      "icon": "bolt"
    },
    "created_at": "2025-01-01T00:00:00Z",
    "updated_at": "2025-01-02T10:00:00Z"
  }
}
```

## DELETE /warehousing/inventory-categories/:id — Delete Inventory Category

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-categories-management#delete-delete-inventory-category)

Deletes a warehouse inventory category.

### Note

This performs a hard delete. Make sure the category is not in use before calling this endpoint.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/warehousing/inventory-categories/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the inventory category to delete (required) | category-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Warehouse item category deleted successfully",
  "id": "category-uuid"
}
```

## GET /warehousing/inventory-transactions — List Inventory Transactions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-transactions-management#get-list-inventory-transactions)

Retrieves inventory transactions with comprehensive filtering options. Each transaction represents a stock movement or adjustment, providing detailed information about what items were affected, where they moved from/to, who performed the action, and when it occurred.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns a paginated list of inventory transactions with details
- Includes related entities: warehouse, item, stock, bins, and user
- Provides available transaction types and reference types for filtering
- Includes pagination information (total count, current page, limit)

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-transactions?tenant_id={{tenant_id}}&warehouse_id=&item_id=&stock_id=&search=&transaction_type=&reference_type=&timestamp_range_start=&timestamp_range_end=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |
| warehouse_id | query string | No | UUID of the warehouse (optional) UUID of the warehouse to filter transactions |  |
| item_id | query string | No | Filter by inventory item (optional) |  |
| stock_id | query string | No | Filter by stock record (optional) |  |
| search | query string | No | Search in transaction type and reference type (optional) |  |
| transaction_type | query string | No | Filter by transaction type (optional) Filter by transaction type (`initial_stock`, `receive`, `pick`, `move`, `stock_adjustment`, `return`, `count`). Legacy `adjust` is also accepted and normalized to `stock_adjustment`. |  |
| reference_type | query string | No | Filter by reference type (optional) Filter by reference type (order, shipment, audit, return, transfer) |  |
| timestamp_range_start | query string | No | Filter by transactions after this date (ISO format) (optional) |  |
| timestamp_range_end | query string | No | Filter by transactions before this date (ISO format) (optional) |  |
| page | query string | No | Page number for pagination (optional, default: 1) Page number for pagination (default: 1) | 1 |
| limit | query string | No | Number of items per page (optional, default: 20) Number of items per page (default: 20) | 20 |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory transactions retrieved successfully",
  "inventory_transactions": [
    {
      "id": "transaction-uuid-1",
      "tenant_id": "tenant-uuid",
      "warehouse_id": "warehouse-uuid",
      "item_id": "item-uuid",
      "stock_id": "stock-uuid",
      "transaction_type": "move",
      "quantity": 10,
      "from_bin_id": "bin-uuid-1",
      "to_bin_id": "bin-uuid-2",
      "reference_id": "order-uuid",
      "reference_type": "order",
      "notes": "Stock moved to fulfill order",
      "performed_by": "user-uuid",
      "timestamp": "2023-06-15T10:30:00Z",
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Main Warehouse",
        "code": "WH001"
      },
      "item": {
        "id": "item-uuid",
        "sku": "ITEM-001",
        "name": "Test Product",
        "category": "Electronics"
      },
      "stock": {
        "id": "stock-uuid",
        "quantity": 40,
        "unit_of_measure": "ea",
        "status": "available"
      },
      "from_bin": {
        "id": "bin-uuid-1",
        "code": "A-01-01",
        "location_type": "bin"
      },
      "to_bin": {
        "id": "bin-uuid-2",
        "code": "B-01-01",
        "location_type": "bin"
      },
      "user": {
        "id": "user-uuid",
        "first_name": "John",
        "last_name": "Doe",
        "email": "john.doe@example.com"
      }
    }
  ],
  "stats": {
    "total": 1,
    "initial_stock": 0,
    "stock_adjustment": 0,
    "move": 1
  },
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 400 Error - Warehouse ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse ID required"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

### Response 500 Error - Server Error



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Internal server error"
}
```

## POST /warehousing/inventory-transactions — Create Inventory Transaction

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-transactions-management#post-create-inventory-transaction)

Records a new inventory transaction for tracking stock movements. The endpoint updates the related inventory stock records based on the transaction type. Automatically creates an audit trail of all inventory changes.

- User must have 'manage:operations:tenant' permission

### Business Rules
- Warehouse must exist and belong to the specified tenant
- Stock record must exist and belong to the specified warehouse
- Item ID must match the stock's item ID
- Transaction type must be one of the valid API types
- Legacy `adjust` is accepted and normalized to `stock_adjustment`
- Quantity must be positive
- For "move" transactions, both from_bin_id and to_bin_id are required
- For "pick" transactions, from_bin_id is required and quantity must not exceed available stock
- For "count" transactions, the stock quantity is set to the counted quantity and `last_counted_at` is updated
- From bin must match the stock's current bin location
- Reference type is required if reference_id is provided
- Stock record is automatically updated based on the transaction type

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/inventory-transactions?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_id | string |  |  |  |
| item_id | string |  |  |  |
| stock_id | string |  |  |  |
| transaction_type | string |  |  |  |
| quantity | number |  |  |  |
| from_bin_id | string |  |  |  |
| to_bin_id | string |  |  |  |
| reference_id | string |  |  |  |
| reference_type | string |  |  |  |
| notes | string |  |  |  |
| performed_by | string |  |  |  |
| timestamp | string |  |  |  |



### Request body (raw)

```json
{
  "warehouse_id": "{{warehouse_id}}",
  "item_id": "{{item_id}}",
  "stock_id": "{{stock_id}}",
  "transaction_type": "move",
  "quantity": 10,
  "from_bin_id": "{{from_bin_id}}",
  "to_bin_id": "{{to_bin_id}}",
  "reference_id": "{{reference_id}}",
  "reference_type": "order",
  "notes": "Stock moved to fulfill order",
  "performed_by": "{{user_id}}",
  "timestamp": "2023-06-15T10:30:00Z"
}
```

### Request example 1

```json
{
  "warehouse_id": "uuid", // required
  "item_id": "uuid", // required
  "stock_id": "uuid", // required
  "transaction_type": "string", // required (receive, pick, move, stock_adjustment, return, count)
  "quantity": number, // required, must be positive
  "from_bin_id": "uuid", // optional, required for move and pick
  "to_bin_id": "uuid", // optional, required for move
  "reference_id": "uuid", // optional, related entity
  "reference_type": "string", // optional, type of reference (order, shipment, audit, return, transfer)
  "notes": "string", // optional
  "performed_by": "uuid", // optional, defaults to authenticated user
  "timestamp": "string" // optional, defaults to current time
}
```

### Response 201 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory transaction created successfully",
  "inventory_transaction": {
    "id": "transaction-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "item_id": "item-uuid",
    "stock_id": "stock-uuid",
    "transaction_type": "move",
    "quantity": 10,
    "from_bin_id": "bin-uuid-1",
    "to_bin_id": "bin-uuid-2",
    "reference_id": "order-uuid",
    "reference_type": "order",
    "notes": "Stock moved to fulfill order",
    "performed_by": "user-uuid",
    "timestamp": "2023-06-15T10:30:00Z"
  },
  "updated_stock": {
    "id": "stock-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "item_id": "item-uuid",
    "bin_id": "bin-uuid-2",
    "quantity": 40,
    "status": "available",
    "updated_at": "2023-06-15T10:30:00Z"
  }
}
```

### Response 400 Error - Missing Required Field



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: transaction_type"
}
```

### Response 400 Error - Invalid Transaction Type



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid transaction type"
}
```

### Response 400 Error - Move Requires Bins



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Move requires from and to bins"
}
```

### Response 400 Error - From Bin Mismatch



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "From bin must match stock bin"
}
```

### Response 400 Error - Reference Type Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Reference type required when reference ID provided"
}
```

### Response 400 Error - Invalid Reference Type



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid reference type"
}
```

### Response 400 Error - Insufficient Stock



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient stock"
}
```

### Response 400 Error - Item ID Mismatch



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Item ID mismatch"
}
```

### Response 404 Error - Warehouse Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Warehouse not found"
}
```

### Response 404 Error - Stock Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Stock not found"
}
```

### Response 404 Error - To Bin Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "To bin not found"
}
```

### Response 404 Error - User Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Performed by user not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/inventory-transactions/:id — Get Inventory Transaction

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-transactions-management#get-get-inventory-transaction)

Retrieves details of a specific inventory transaction, including related entities such as the warehouse, item, stock, bins, and user who performed the action.

- User must have 'view:tenant' or 'manage:operations:tenant' permission

### Response
- Returns detailed information about the specified inventory transaction
- Includes related entities: warehouse, item, stock, bins, and user

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-transactions/:id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes | UUID of the inventory transaction (required) | transaction-uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required if not in auth) (required if not in auth): UUID of the tenant | {{tenant_id}} |



### Response 200 Success Response



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Transaction retrieved successfully",
  "inventory_transaction": {
    "id": "transaction-uuid",
    "tenant_id": "tenant-uuid",
    "warehouse_id": "warehouse-uuid",
    "item_id": "item-uuid",
    "stock_id": "stock-uuid",
    "transaction_type": "move",
    "quantity": 10,
    "from_bin_id": "bin-uuid-1",
    "to_bin_id": "bin-uuid-2",
    "reference_id": "order-uuid",
    "reference_type": "order",
    "notes": "Stock moved to fulfill order",
    "performed_by": "user-uuid",
    "timestamp": "2023-06-15T10:30:00Z",
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001"
    },
    "item": {
      "id": "item-uuid",
      "sku": "ITEM-001",
      "name": "Test Product",
      "category": "Electronics"
    },
    "stock": {
      "id": "stock-uuid",
      "quantity": 40,
      "unit_of_measure": "ea",
      "status": "available"
    },
    "from_bin": {
      "id": "bin-uuid-1",
      "code": "A-01-01",
      "location_type": "bin"
    },
    "to_bin": {
      "id": "bin-uuid-2",
      "code": "B-01-01",
      "location_type": "bin"
    },
    "user": {
      "id": "user-uuid",
      "first_name": "John",
      "last_name": "Doe",
      "email": "john.doe@example.com"
    }
  }
}
```

### Response 400 Error - Transaction ID Required



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Transaction ID required"
}
```

### Response 404 Error - Transaction Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Transaction not found"
}
```

### Response 404 Error - Tenant Not Found



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

### Response 403 Error - Insufficient Permissions



### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/inventory-transfers — List Inventory Transfers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-transfers#get-list-inventory-transfers)

Retrieves a paginated list of inventory transfers for the tenant.

Requires valid token with 'view:tenant' or 'manage:operations:tenant' or 'manage:module:warehouse' permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-transfers?tenant_id={{tenant_id}}&source_warehouse_id=&destination_warehouse_id=&search=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| source_warehouse_id | query string | No | Filter by source warehouse (optional) |  |
| destination_warehouse_id | query string | No | Filter by destination warehouse (optional) |  |
| search | query string | No | Search by reference number (optional) |  |
| page | query string | No | Page number (optional, default: 1) | 1 |
| limit | query string | No | Items per page (optional, default: 20) | 20 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory transfers retrieved successfully",
  "inventory_transfers": [
    {
      "id": "transfer-uuid-1",
      "tenant_id": "tenant-uuid",
      "reference_number": "TR-20260522-A1B2C3",
      "source_warehouse_id": "wh-source-uuid",
      "destination_warehouse_id": "wh-dest-uuid",
      "status": "completed",
      "items": [
        {
          "item_id": "item-uuid",
          "quantity": 10,
          "source_bin_id": "bin-source-uuid",
          "destination_bin_id": "bin-dest-uuid",
          "lot_number": null,
          "serial_number": null
        }
      ],
      "notes": "Internal stock rebalance",
      "performed_by": "user-uuid",
      "created_at": "2026-05-22T11:37:00Z",
      "source_warehouse": {
        "id": "wh-source-uuid",
        "name": "Warehouse A",
        "code": "WH-A"
      },
      "destination_warehouse": {
        "id": "wh-dest-uuid",
        "name": "Warehouse B",
        "code": "WH-B"
      },
      "performer": {
        "id": "user-uuid",
        "full_name": "John Doe"
      }
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1
}
```

### Response 404 Error - Tenant Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Tenant not found"
}
```

## GET /warehousing/inventory-transfers/:transfer_id — Get Inventory Transfer

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-transfers#get-get-inventory-transfer)

Retrieves details of a specific inventory transfer including all linked inventory transactions.

Requires valid token with view or manage permissions.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/inventory-transfers/:transfer_id?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| transfer_id | path string | Yes | UUID of the inventory transfer |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory transfer retrieved successfully",
  "inventory_transfer": {
    "id": "transfer-uuid-1",
    "tenant_id": "tenant-uuid",
    "reference_number": "TR-20260522-A1B2C3",
    "source_warehouse_id": "wh-source-uuid",
    "destination_warehouse_id": "wh-dest-uuid",
    "status": "completed",
    "items": [
      {
        "item_id": "item-uuid",
        "quantity": 10,
        "source_bin_id": "bin-source-uuid",
        "destination_bin_id": "bin-dest-uuid",
        "lot_number": null,
        "serial_number": null,
        "item": {
          "id": "item-uuid",
          "name": "Widget",
          "sku": "WDG-001",
          "barcode": "INV12345",
          "brand": "Acme",
          "model": "Pro",
          "unit_size": 1,
          "is_active": true
        },
        "source_bin": {
          "id": "bin-source-uuid",
          "code": "A-01-01",
          "name": "Aisle A - Shelf 1"
        },
        "destination_bin": {
          "id": "bin-dest-uuid",
          "code": "B-02-03",
          "name": "Aisle B - Shelf 2"
        }
      }
    ],
    "notes": "Internal stock rebalance",
    "performed_by": "user-uuid",
    "created_at": "2026-05-22T11:37:00Z",
    "source_warehouse": {
      "id": "wh-source-uuid",
      "name": "Warehouse A",
      "code": "WH-A"
    },
    "destination_warehouse": {
      "id": "wh-dest-uuid",
      "name": "Warehouse B",
      "code": "WH-B"
    },
    "performer": {
      "id": "user-uuid",
      "full_name": "John Doe"
    },
    "transactions": [
      {
        "id": "txn-uuid-1",
        "transaction_type": "transfer_out",
        "quantity": 10,
        "warehouse_id": "wh-source-uuid",
        "item": {
          "id": "item-uuid",
          "name": "Widget",
          "sku": "WDG-001"
        },
        "from_bin": {
          "id": "bin-source-uuid",
          "code": "A-01-01"
        },
        "to_bin": null
      },
      {
        "id": "txn-uuid-2",
        "transaction_type": "transfer_in",
        "quantity": 10,
        "warehouse_id": "wh-dest-uuid",
        "item": {
          "id": "item-uuid",
          "name": "Widget",
          "sku": "WDG-001"
        },
        "from_bin": null,
        "to_bin": {
          "id": "bin-dest-uuid",
          "code": "B-02-03"
        }
      }
    ]
  }
}
```

### Response 404 Error - Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Not found"
}
```

## POST /warehousing/inventory-transfers — Create Inventory Transfer

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/inventory-transfers#post-create-inventory-transfer)

Creates a cross-warehouse inventory stock transfer. Atomically decrements stock in the source warehouse and increments/creates stock in the destination warehouse.

Requires 'manage:operations:tenant' or 'manage:module:warehouse' permission.

### Behavior
- Validates both warehouses exist and belong to the tenant
- Validates source bins belong to source warehouse, destination bins to destination warehouse
- Checks available stock quantity at source
- Decrements source inventory_stock
- Increments or creates destination inventory_stock
- Creates transfer_out + transfer_in inventory_transactions linked via reference_type='transfer'

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/inventory-transfers?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| source_warehouse_id | string | Yes | UUID of the source warehouse |  |
| destination_warehouse_id | string | Yes | UUID of the destination warehouse (must differ from source) |  |
| reference_number | string | No | Custom reference number (auto-generated if omitted) |  |
| notes | string | No | Transfer notes |  |
| items | array<object> | Yes | (required, min 1): Array of transfer lines: |  |
| items.item_id | string | Yes | UUID of the inventory item |  |
| items.quantity | number | Yes | Positive integer quantity to transfer |  |
| items.source_bin_id | string | Yes | UUID of the bin in source warehouse |  |
| items.destination_bin_id | string | Yes | UUID of the bin in destination warehouse |  |
| items.lot_number |  | No | Filter source stock by lot number |  |
| items.serial_number |  | No | Filter source stock by serial number |  |



### Request body (raw)

```json
{
  "source_warehouse_id": "{{source_warehouse_id}}",
  "destination_warehouse_id": "{{destination_warehouse_id}}",
  "reference_number": "TR-000123",
  "notes": "Internal stock transfer",
  "items": [
    {
      "item_id": "item-uuid-1",
      "quantity": 10,
      "source_bin_id": "source-bin-uuid",
      "destination_bin_id": "dest-bin-uuid"
    },
    {
      "item_id": "item-uuid-2",
      "quantity": 5,
      "source_bin_id": "source-bin-uuid-2",
      "destination_bin_id": "dest-bin-uuid-2",
      "lot_number": "LOT-2026-A"
    }
  ]
}
```

### Response 201 Success - Transfer Completed

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Inventory transfer created successfully",
  "inventory_transfer": {
    "id": "transfer-uuid-1",
    "tenant_id": "tenant-uuid",
    "reference_number": "TR-20260522-A1B2C3",
    "source_warehouse_id": "wh-source-uuid",
    "destination_warehouse_id": "wh-dest-uuid",
    "status": "completed",
    "items": [
      {
        "item_id": "item-uuid-1",
        "quantity": 10,
        "source_bin_id": "source-bin-uuid",
        "destination_bin_id": "dest-bin-uuid",
        "lot_number": null,
        "serial_number": null
      }
    ],
    "notes": "Internal stock transfer",
    "performed_by": "user-uuid",
    "created_at": "2026-05-22T11:37:00Z"
  }
}
```

### Response 400 Error - Same Warehouse

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Source and destination must differ"
}
```

### Response 400 Error - Insufficient Stock

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "items[0]: insufficient stock (available: 5, requested: 10)"
}
```

### Response 404 Error - Source Warehouse Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Source warehouse not found"
}
```

### Response 404 Error - Source Bin Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "items[0]: source_bin not found in source warehouse"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /warehousing/package-transfers — List Package Transfers

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/package-transfers#get-list-package-transfers)

Retrieves a paginated list of warehouse-to-warehouse Package Transfer audit records.

### Waybill visibility

Every eligible warehouse-to-warehouse Waybill with at least one package has one `waybill` Package Transfer projection and is returned for its source or destination warehouse whatever its Waybill status, including `in_transit`, `received`, and `closed`. Empty Waybills, same-warehouse routes, and routes without a valid warehouse destination do not create a projection. The projection is created in the same transaction as the first delivery attachment; existing missing projections are backfilled idempotently.

### Search

`search` is case-insensitive, limited to 100 characters, and matches Waybill number plus source or destination warehouse name or code.

### Mode filtering

Package Transfers have two stored modes: `system_only` and `delivery_operation`. Delivery-operation transfers have an `operation_type` of `single_delivery` or `waybill`. Use `mode` and `operation_type` directly; neither value is a status.

### Exact pill requests
- All: omit both filters.
- System only: `mode=system_only`.
- Single delivery: `mode=delivery_operation&operation_type=single_delivery`.
- Waybill: `mode=delivery_operation&operation_type=waybill`.

`operation_type` implies `mode=delivery_operation` when mode is omitted. Sending `mode=system_only` together with an operation type returns HTTP 400. `column_filters` supports the same `mode` and `operation_type` fields and takes precedence over standalone parameters. `status` and `transfer_type` are not supported Package Transfer filters and return HTTP 400. Search, mode, and operation type are applied in the database before pagination, so response `total` is the filtered result count.

### Related lifecycle object

A `single_delivery` transfer is created only when selection resolves to exactly one delivery and no inventory items. Its list row includes that entity as the singular `delivery` object with `id`, `delivery_code`, `barcode`, and delivery `status`. The `delivery` property is not added to Waybill or system-only rows. A Waybill transfer instead exposes its linked `waybill` object and Waybill status. The existing `delivery_ids` audit field remains available for backward compatibility.

### Statistics

`stats.total`, `stats.system_only`, `stats.single_delivery`, and `stats.waybill` describe the same Package Transfer population after tenant, warehouse, and search filters. The selected mode/operation type does not narrow `stats`, allowing the frontend to render all mechanism pills. `stats.total` always equals the sum of the three counters. A linked Waybill's lifecycle status remains available only as `package_transfers[].waybill.status` for related-detail display.

Requires valid token with view or manage permissions.

### Frontend integration contract

Send the exact mode/operation_type combinations above. Use each row's existing `mode` and `operation_type` fields. For `single_delivery`, render the singular `delivery` object and use `delivery.status`; do not expect a `delivery` property on Waybill or system-only rows. For `waybill`, use the nested `waybill` object and `waybill.status`. Use the four `stats` fields for pills. Do not send `status` and do not use either related lifecycle status as the Package Transfer list status. Treat HTTP 400 as an invalid filter request and display the localized response message.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/package-transfers?tenant_id={{tenant_id}}&mode=&operation_type=&source_warehouse_id=&destination_warehouse_id=&search=&column_filters=&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |
| mode | query string | No | Optional Package Transfer mode filter: system_only or delivery_operation. |  |
| operation_type | query string | No | Optional delivery-operation subtype filter: single_delivery or waybill. It implies mode=delivery_operation when mode is omitted. |  |
| source_warehouse_id | query string | No | Filter by source warehouse (optional) |  |
| destination_warehouse_id | query string | No | Filter by destination warehouse (optional) |  |
| search | query string | No | Optional case-insensitive search, maximum 100 characters. Matches Waybill number and source or destination warehouse name/code. |  |
| column_filters | query string | No | Optional JSON object supporting mode and operation_type, for example {"mode":"delivery_operation","operation_type":"waybill"}. Values in column_filters take precedence over their standalone parameters. |  |
| page | query string | No | Page number (optional, default: 1) | 1 |
| limit | query string | No | Items per page (optional, backend default: 25) | 20 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Package transfers retrieved successfully",
  "package_transfers": [
    {
      "id": "transfer-uuid-1",
      "mode": "delivery_operation",
      "operation_type": "single_delivery",
      "notes": "Single package movement",
      "delivery_ids": [
        "delivery-uuid-1"
      ],
      "created_at": "2026-05-22T11:00:00Z",
      "source_warehouse": {
        "id": "wh-source-uuid",
        "name": "Main Warehouse",
        "code": "WH-A"
      },
      "destination_warehouse": {
        "id": "wh-dest-uuid",
        "name": "Branch Warehouse",
        "code": "WH-B"
      },
      "waybill": null,
      "performer": {
        "id": "user-uuid-1",
        "full_name": "Warehouse User"
      },
      "delivery": {
        "id": "delivery-uuid-1",
        "delivery_code": "DEL1001",
        "barcode": "PKG-1001",
        "status": "pending"
      }
    },
    {
      "id": "transfer-uuid-2",
      "mode": "delivery_operation",
      "operation_type": "waybill",
      "notes": "Batch transfer to branch warehouse",
      "delivery_ids": [
        "delivery-uuid-2",
        "delivery-uuid-3"
      ],
      "created_at": "2026-05-22T11:05:00Z",
      "source_warehouse": {
        "id": "wh-source-uuid",
        "name": "Main Warehouse",
        "code": "WH-A"
      },
      "destination_warehouse": {
        "id": "wh-dest-uuid",
        "name": "Branch Warehouse",
        "code": "WH-B"
      },
      "waybill": {
        "id": "waybill-uuid-1",
        "waybill_number": "WB00001",
        "status": "drafted"
      },
      "performer": {
        "id": "user-uuid-1",
        "full_name": "Warehouse User"
      }
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 7,
  "stats": {
    "total": 7,
    "system_only": 3,
    "single_delivery": 2,
    "waybill": 2
  }
}
```

### Response 400 Error - Invalid Mode Filter

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "mode: Must be one of: system_only, delivery_operation",
  "details": [
    {
      "field": "mode",
      "message": "Must be one of: system_only, delivery_operation"
    }
  ]
}
```

### Response 400 Error - Invalid Column Filters JSON

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "column_filters: Invalid JSON",
  "details": [
    {
      "field": "column_filters",
      "message": "Invalid JSON"
    }
  ]
}
```

### Response 400 Error - Search Too Long

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "search: Must be at most 100 characters long",
  "details": [
    {
      "field": "search",
      "message": "Must be at most 100 characters long"
    }
  ]
}
```

## POST /warehousing/package-transfers — Create Package Transfer - System Only

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/package-transfers#post-create-package-transfer-system-only)

Marks packages as transferred to another warehouse without creating delivery operations. Use when the movement already happened or doesn't need delivery tracking.

Requires 'manage:operations:tenant' or 'manage:module:warehouse' permission.

### Behavior
- Resolves barcodes to deliveries (tries barcode first, then delivery_code)
- Updates pickup_warehouse_id and dropoff_warehouse_id on each delivery
- Does NOT create a waybill, delivery order, or driver assignment

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/package-transfers?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| mode | string | Yes | Must be `"system_only"` |  |
| source_warehouse_id | string | Yes | UUID of the source warehouse |  |
| destination_warehouse_id | string | Yes | UUID of the destination warehouse |  |
| package_barcodes | array<string> | Yes | Array of package barcodes or delivery codes |  |
| notes | string | No | Transfer notes |  |



### Request body (raw)

```json
{
  "mode": "system_only",
  "source_warehouse_id": "{{source_warehouse_id}}",
  "destination_warehouse_id": "{{destination_warehouse_id}}",
  "package_barcodes": [
    "PKG-1001",
    "PKG-1002"
  ],
  "notes": "Moved manually by warehouse team"
}
```

### Response 201 Success - System Transfer

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Packages transferred successfully",
  "transfer_id": "transfer-uuid-1",
  "transferred_count": 2,
  "delivery_ids": [
    "del-uuid-1",
    "del-uuid-2"
  ],
  "mode": "system_only",
  "rejected": []
}
```

### Response 404 Error - No Deliveries Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "No deliveries found for barcodes"
}
```

### Response 400 Error - Same Warehouse

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Source and destination must differ"
}
```

## POST /warehousing/package-transfers — Create Package Transfer - Single Delivery Operation

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/package-transfers#post-create-package-transfer-single-delivery-operation)

Creates a delivery operation for a single package transfer between warehouses. Sets the delivery's pickup/dropoff warehouses so it can be assigned to a driver via the delivery module.

### Behavior
- Sets pickup_warehouse_id and dropoff_warehouse_id on the delivery
- Delivery is now ready for driver assignment in the delivery module
- No waybill is created for single packages

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/package-transfers?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| mode | string | Yes | Must be `"delivery_operation"` |  |
| operation_type | string | No | `"single_delivery"` (auto-detected if 1 barcode) |  |
| source_warehouse_id | string | Yes | Source warehouse UUID |  |
| destination_warehouse_id | string | Yes | Destination warehouse UUID |  |
| package_barcodes | array<string> | Yes | Array with 1 barcode |  |



### Request body (raw)

```json
{
  "mode": "delivery_operation",
  "operation_type": "single_delivery",
  "source_warehouse_id": "{{source_warehouse_id}}",
  "destination_warehouse_id": "{{destination_warehouse_id}}",
  "package_barcodes": [
    "PKG-1001"
  ]
}
```

### Response 201 Success - Single Delivery

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Delivery transfer created successfully",
  "transfer_id": "transfer-uuid-2",
  "mode": "delivery_operation",
  "operation_type": "single_delivery",
  "delivery_ids": [
    "del-uuid-1"
  ],
  "rejected": []
}
```

## POST /warehousing/package-transfers — Create Package Transfer - Waybill Operation

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/package-transfers#post-create-package-transfer-waybill-operation)

Thin alias over canonical warehouse Waybill validation/create. `Idempotency-Key` is required. Selection may combine package barcodes, delivery IDs, and individually tracked inventory IDs. The service applies one destination lock, reuses compatible Draft/Pending or staged-parent deliveries, creates Draft deliveries only for raw inventory, and returns enriched Waybill detail with `operation_result`. Creation stops at `drafted`; seal, driver warehouse_handover, staff receipt, and explicit close remain separate.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/package-transfers?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| Idempotency-Key | header string |  |  | package-transfer-waybill-{{$guid}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| mode | string |  |  |  |
| operation_type | string |  |  |  |
| source_warehouse_id | string |  |  |  |
| destination_warehouse_id | string |  |  |  |
| package_barcodes | array<string> |  |  |  |
| delivery_ids | array<string> |  |  |  |
| inventory_item_ids | array<string> |  |  |  |
| new_delivery_defaults | object |  |  |  |
| new_delivery_defaults.customer_id | string |  |  |  |
| new_delivery_defaults.delivery_service_id | string |  |  |  |
| new_delivery_defaults.payer_type | string |  |  |  |
| new_delivery_defaults.payment_method | string |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "mode": "delivery_operation",
  "operation_type": "waybill",
  "source_warehouse_id": "{{source_warehouse_id}}",
  "destination_warehouse_id": "{{destination_warehouse_id}}",
  "package_barcodes": [
    "PKG-1001"
  ],
  "delivery_ids": [
    "{{delivery_id}}"
  ],
  "inventory_item_ids": [
    "{{inventory_item_id}}"
  ],
  "new_delivery_defaults": {
    "customer_id": "{{customer_id}}",
    "delivery_service_id": "{{delivery_service_id}}",
    "payer_type": "sender",
    "payment_method": "cash"
  },
  "notes": "Batch transfer to branch"
}
```

### Response 201 Success - Waybill Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Waybill transfer created successfully",
  "transfer_id": "transfer-uuid-3",
  "mode": "delivery_operation",
  "operation_type": "waybill",
  "delivery_ids": [
    "del-uuid-1",
    "del-uuid-2",
    "del-uuid-3"
  ],
  "waybill_id": "waybill-uuid-1",
  "waybill_number": "WB00001",
  "status": "drafted",
  "rejected": []
}
```

### Response 500 Error - Waybill Creation Failed

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "waybill_creation_failed",
  "message": "Unable to create the Waybill. Please try again.",
  "data": null
}
```

### Response 404 Error - Source Warehouse Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Source warehouse not found"
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## POST /warehousing/package-transfers/validate — Validate Package Barcodes

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/package-transfers#post-validate-package-barcodes)

Read-only alias over canonical mixed Waybill preflight. Source warehouse is required. Destination may be inferred from a compatible delivery; raw-only inventory requires it explicitly. Response contains accepted deliveries/items, localized rejected rows, destination lock, defaults requirement, and `can_create`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/package-transfers/validate?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | Yes | UUID of the tenant (required) | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| mode | string |  |  |  |
| operation_type | string |  |  |  |
| source_warehouse_id | string |  |  |  |
| destination_warehouse_id | string |  |  |  |
| package_barcodes | array<string> |  |  |  |
| delivery_ids | array<string> |  |  |  |
| inventory_item_ids | array<string> |  |  |  |



### Request body (raw)

```json
{
  "mode": "delivery_operation",
  "operation_type": "waybill",
  "source_warehouse_id": "{{source_warehouse_id}}",
  "destination_warehouse_id": "{{destination_warehouse_id}}",
  "package_barcodes": [
    "PKG-1001",
    "PKG-9999"
  ],
  "delivery_ids": [
    "{{delivery_id}}"
  ],
  "inventory_item_ids": [
    "{{inventory_item_id}}"
  ]
}
```

### Response 200 Success - Validation Result

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "valid": [
    {
      "barcode": "PKG-1001",
      "delivery_id": "del-uuid-1",
      "delivery_code": "DEL-001",
      "status": "staged_at_warehouse",
      "current_warehouse_id": "wh-uuid-1"
    },
    {
      "barcode": "PKG-1002",
      "delivery_id": "del-uuid-2",
      "delivery_code": "DEL-002",
      "status": "staged_at_warehouse",
      "current_warehouse_id": "wh-uuid-1"
    }
  ],
  "invalid": [
    {
      "barcode": "PKG-9999",
      "reason": "not_found"
    }
  ],
  "total_scanned": 3,
  "valid_count": 2,
  "invalid_count": 1
}
```

### Response 400 Error - Missing Barcodes

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "package_barcodes_required"
}
```

## GET /warehousing/package-scan/{{delivery_barcode}} — Scan Delivery Package

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-package-scan#get-scan-delivery-package)

Scans a P&D package barcode and returns a navigation instruction for the existing warehouse frontend.

When a moving staged P&D outbound or operational-return exception is created, backend also creates one exception-specific pending receiving order with custody `awaiting_warehouse_receipt`. The driver needs no new return action; warehouse staff scan the same parcel barcode after it is physically brought back. The existing sources `failed_delivery_return` and `delivery_return_handoff` both return `action=open_receiving_order` and `reason=failed_delivery_return_ready_to_receive`.

Frontend opens `entity_id` as the normal receiving order and calls `PUT /warehousing/receiving-orders/:id` with `{ "status": "received" }`. It may send one selected location as `item_details[0].bin_id`; if omitted, backend uses the active default receiving location and returns `bin_id_required_for_received_items` when none exists. Successful receipt restores item-level warehouse stock, records one receive movement per parcel item, releases the old driver assignment/capacity, and keeps the delivery exception open. The scan itself is read-only.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/package-scan/{{delivery_barcode}}?tenant_id={{tenant_id}}&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| delivery_barcode | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | UUID of the tenant. Optional when available from the authenticated context. | {{tenant_id}} |
| language | query string |  | Response language. | en |



### Response 200 Original Barcode - Active Return Target

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "action": "open_receiving_order",
  "entity_id": "original-receiving-order-uuid",
  "reason": "package_ready_to_receive",
  "scanned_barcode": "DEL1001",
  "delivery": {
    "id": "original-delivery-uuid",
    "delivery_code": "1001",
    "barcode": "DEL1001",
    "status": "cancelled",
    "delivery_flow": "standard",
    "is_active": false,
    "next_action": "open_receiving_order"
  },
  "delivery_target": {
    "action": "open_receiving_order",
    "entity_id": "original-receiving-order-uuid",
    "reason": "package_ready_to_receive"
  },
  "return_delivery": {
    "id": "return-delivery-uuid",
    "delivery_code": "1050",
    "barcode": "DEL1050",
    "status": "pending",
    "delivery_flow": "operational_return",
    "is_active": true,
    "next_action": "open_receiving_order"
  },
  "return_target": {
    "action": "open_receiving_order",
    "entity_id": "return-receiving-order-uuid",
    "reason": "failed_delivery_return_ready_to_receive"
  },
  "warehouse_context": {},
  "receiving_order": {},
  "items": [],
  "inbound_deliveries": [],
  "outbound_deliveries": [],
  "handoff": null
}
```

### Response 200 Open Receiving Order - Ready To Receive

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "action": "open_receiving_order",
  "entity_id": "receiving-order-uuid",
  "reason": "package_ready_to_receive",
  "message": "The package is ready to be received",
  "barcode": "DEL1001",
  "warehouse_context": {
    "staff_id": "warehouse-staff-uuid",
    "staff_role": "receiver",
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001",
      "status": "active"
    }
  },
  "receiving_order": {
    "id": "receiving-order-uuid",
    "reference_number": "RO-1001",
    "status": "pending",
    "default_receiving_location": {
      "id": "location-uuid",
      "code": "B001",
      "name": "Receiving Bin",
      "barcode": "WLCWH001B001",
      "location_type": "bin"
    }
  },
  "items": [
    {
      "id": "inventory-item-uuid",
      "sku": "PKG-1001",
      "name": "Delivery Package DEL1001",
      "barcode": "DEL1001",
      "item_code": "PKG-1001",
      "inbound_delivery_id": "inbound-delivery-uuid",
      "outbound_delivery_id": "outbound-delivery-uuid",
      "has_exception": true,
      "has_open_exception": true,
      "open_exception": {
        "id": "exception-uuid",
        "exception_type": "damaged",
        "exception_label": "Damaged",
        "status": "open",
        "affected_quantity": 1,
        "exception_details": {
          "damage_area": "corner"
        },
        "notes": "Box crushed on arrival"
      },
      "open_exceptions": [
        {
          "id": "exception-uuid",
          "exception_type": "damaged",
          "exception_label": "Damaged",
          "status": "open",
          "affected_quantity": 1,
          "exception_details": {
            "damage_area": "corner"
          },
          "notes": "Box crushed on arrival"
        }
      ],
      "open_exception_count": 1
    }
  ],
  "inbound_deliveries": [
    {
      "id": "inbound-delivery-uuid",
      "delivery_code": "DEL1001A",
      "direction": "inbound",
      "status": "delivered"
    }
  ],
  "outbound_deliveries": [
    {
      "id": "outbound-delivery-uuid",
      "delivery_code": "DEL1001B",
      "direction": "outbound",
      "status": "draft"
    }
  ],
  "handoff": null
}
```

### Response 200 Open Receiving Order - Execute Outbound

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "action": "open_receiving_order",
  "entity_id": "receiving-order-uuid",
  "reason": "outbound_ready_to_execute",
  "message": "The package is received and the outbound delivery is ready to execute",
  "barcode": "DEL1001",
  "warehouse_context": {
    "staff_id": "warehouse-staff-uuid",
    "staff_role": "receiver",
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001",
      "status": "active"
    }
  },
  "receiving_order": {
    "id": "receiving-order-uuid",
    "reference_number": "RO-1001",
    "status": "received",
    "default_receiving_location": null
  },
  "items": [],
  "inbound_deliveries": [],
  "outbound_deliveries": [
    {
      "id": "outbound-delivery-uuid",
      "delivery_code": "DEL1001B",
      "direction": "outbound",
      "status": "draft"
    }
  ],
  "handoff": null
}
```

### Response 200 Open Handoff

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "action": "open_handoff",
  "entity_id": "handoff-uuid",
  "reason": "handoff_ready",
  "message": "The package is ready for warehouse handoff",
  "barcode": "DEL1001",
  "warehouse_context": {
    "staff_id": "warehouse-staff-uuid",
    "staff_role": "receiver",
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH001",
      "status": "active"
    }
  },
  "receiving_order": {
    "id": "receiving-order-uuid",
    "reference_number": "RO-1001",
    "status": "received"
  },
  "items": [],
  "inbound_deliveries": [],
  "outbound_deliveries": [
    {
      "id": "outbound-delivery-uuid",
      "delivery_code": "DEL1001B",
      "direction": "outbound",
      "status": "pending"
    }
  ],
  "handoff": {
    "id": "handoff-uuid",
    "status": "pending",
    "delivery_id": "outbound-delivery-uuid",
    "picking_order_id": "picking-order-uuid",
    "driver_id": null,
    "warehouse_staff_id": null,
    "initiated_at": "2026-06-10T10:00:00Z",
    "completed_at": null
  }
}
```

### Response 200 Open Failed Delivery Return

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "action": "open_receiving_order",
  "entity_id": "receiving-order-uuid",
  "reason": "failed_delivery_return_ready_to_receive",
  "barcode": "DEL1001",
  "receiving_order": {
    "id": "receiving-order-uuid",
    "status": "pending",
    "default_receiving_location": null,
    "receiving_details": {
      "source": "failed_delivery_return",
      "custody_state": "awaiting_warehouse_receipt"
    }
  },
  "failed_delivery_return": {
    "exception_id": "exception-uuid",
    "reason": "not_home",
    "attempt": {
      "number": 1,
      "allowed": 2
    },
    "custody_state": "awaiting_warehouse_receipt"
  }
}
```

### Response 404 Error - Barcode Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Barcode not found"
}
```

### Response 403 Error - Warehouse Access

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "This package is not assigned to one of your active warehouses"
}
```

### Response 409 Error - Waybill Custody Transferred

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "action": "show_error",
  "entity_id": null,
  "reason": "receiving_order_custody_transferred",
  "message": "This parcel has left this warehouse and must be handled at the destination warehouse",
  "barcode": "DEL1001",
  "receiving_order": {
    "id": "origin-receiving-order-uuid",
    "receiving_details": {
      "execution_block_reason": "receiving_order_custody_transferred",
      "execution_warehouse_id": "destination-warehouse-uuid"
    }
  }
}
```

### Response 409 Error - Missing Handoff

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "action": "show_error",
  "entity_id": null,
  "reason": "package_handoff_missing",
  "message": "The outbound delivery was executed but its handoff record is missing",
  "barcode": "DEL1001",
  "warehouse_context": {},
  "receiving_order": {},
  "items": [],
  "inbound_deliveries": [],
  "outbound_deliveries": [],
  "handoff": null
}
```

## GET /warehousing/exceptions — List Warehouse Exceptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-exceptions#get-list-warehouse-exceptions)

Lists warehouse item exceptions. Authentication requires the normal warehousing view access. Use filters for item, receiving order, picking order, warehouse, status, and type. Omit `status` to return both open and resolved history, including resolution details, resolver, and resolution time. Open damaged exceptions return the exact `package_item_id` and `available_actions` only when the item has current available unreserved stock in the exception warehouse. This includes stock created after the exception when `inventory_stock_id` is null; an explicit stock ID must still match exactly. The action endpoint revalidates stock and delivery state atomically. Receiving provenance does not control action visibility.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/exceptions?tenant_id={{tenant_id}}&status=open&page=1&limit=20

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string | No | UUID of the tenant. Optional when available from auth context. | {{tenant_id}} |
| status | query string | No | Optional: open \| resolved | open |
| inventory_item_id | query string | No | Optional item filter |  |
| receiving_order_id | query string | No | Optional receiving order filter |  |
| picking_order_id | query string | No | Optional picking order filter |  |
| warehouse_id | query string | No | Optional warehouse filter |  |
| exception_type | query string | No | Optional exception type code filter |  |
| page | query string |  |  | 1 |
| limit | query string |  |  | 20 |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "exceptions": [
    {
      "id": "open-exception-uuid",
      "inventory_item_id": "item-uuid",
      "receiving_order_id": "receiving-order-uuid",
      "exception_type": "damaged",
      "status": "open",
      "package_item_id": "package-item-uuid",
      "available_actions": [
        {
          "key": "return_to_origin",
          "exception_id": "open-exception-uuid",
          "package_item_id": "package-item-uuid"
        }
      ],
      "notes": "Box crushed on arrival",
      "reported_by_user": {
        "id": "reporter-uuid",
        "full_name": "Warehouse Receiver"
      },
      "resolved_by_user": null,
      "affected_quantity": 1,
      "exception_details": {
        "damage_area": "outer_box"
      },
      "item": {
        "id": "item-uuid",
        "name": "Product Name",
        "sku": "SKU-123"
      },
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Main Warehouse",
        "code": "WH-001"
      }
    },
    {
      "id": "resolved-exception-uuid",
      "inventory_item_id": "item-uuid",
      "receiving_order_id": "receiving-order-uuid",
      "exception_type": "wrong_item",
      "status": "resolved",
      "package_item_id": "package-item-uuid",
      "available_actions": [],
      "resolution_details": {
        "note": "Correct item confirmed"
      },
      "resolved_by": "resolver-uuid",
      "resolved_at": "2026-08-22T07:45:00Z",
      "resolved_by_user": {
        "id": "resolver-uuid",
        "full_name": "Warehouse Supervisor"
      },
      "affected_quantity": 1,
      "exception_details": {},
      "item": {
        "id": "item-uuid",
        "name": "Product Name",
        "sku": "SKU-123"
      },
      "warehouse": {
        "id": "warehouse-uuid",
        "name": "Main Warehouse",
        "code": "WH-001"
      }
    }
  ],
  "stats": {
    "totalExceptions": 2,
    "openCount": 1,
    "resolvedCount": 1
  },
  "page": 1,
  "limit": 20,
  "total": 2
}
```

## GET /warehousing/exceptions/available — Available Warehouse Exception Types

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-exceptions#get-available-warehouse-exception-types)

Returns exception type options available for a specific item/context. Type configuration comes from tenant override `tenant_settings.warehouse_exception_types` or system setting `warehouse_exception_types`. Each type returns `write_off_stock`; this behavior is configured by the tenant and must not be inferred from its code or label.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/exceptions/available?tenant_id={{tenant_id}}&inventory_item_id={{inventory_item_id}}&receiving_order_id={{receiving_order_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |
| inventory_item_id | query string | Yes | Required inventory item id | {{inventory_item_id}} |
| inventory_stock_id | query string | No |  |  |
| receiving_order_id | query string | No | Optional receiving context | {{receiving_order_id}} |
| picking_order_id | query string | No |  |  |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "exception_types": [
    {
      "code": "missing",
      "label": "Missing",
      "description": "Expected item is missing during receiving or handling.",
      "write_off_stock": true
    }
  ],
  "all_exception_types": []
}
```

## POST /warehousing/exceptions — Create Warehouse Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-exceptions#post-create-warehouse-exception)

Creates one open warehouse exception for a specific inventory item. Mutation access requires `manage:operations:tenant`, `manage:module:warehouse`, system admin, or a warehouse role.

### Business rules
- `inventory_item_id` and `exception_type` are required.
- `exception_type` must exist in `warehouse_exception_types`.
- Context can be receiving (`receiving_order_id`), picking (`picking_order_id`), stock (`inventory_stock_id`), or general inventory. `receiving_order_id` is optional; when omitted, a valid receipt recorded in the inventory item's custody metadata is captured as audit provenance.
- An item may have multiple independent open exceptions in the same context.
- If one pending delivery already owns one completed picking order containing the item, creation captures that `picking_order_id` as preparation context; ambiguous candidates remain recordable and are rejected safely only if resolution would need to restore preparation.
- If the selected tenant-defined type has `write_off_stock=true` and the linked receipt item has positive `received_quantity`, `affected_quantity` defaults to 1 and must be positive. The exception insert atomically deducts available stock, releases any reservation that can no longer be satisfied, changes a zero-quantity stock row to `depleted`, reduces the receipt's `received_quantity`, records the authenticated exception actor as `received_by` when the receipt had no actor, and writes a `stock_adjustment` inventory transaction. A fully received order changes to `received_with_exceptions`; a partially received order remains `partially_received` so outstanding unreceived lines are not completed. This is the normal Lost/received-stock path.
- If the linked receipt item has `received_quantity=0`, or omits `received_quantity` because receiving has not started, the item has not entered warehouse stock. The exception is still created, no stock or receipt row is changed by the write-off trigger, unrelated stock for the same inventory item is not consumed, and the response returns `stock_written_off=false` with null write-off audit fields.
- The exception response records `stock_written_off`, `written_off_quantity`, `written_off_by`, `written_off_at`, and `written_off_by_user`.
- If positive received quantity exists but sufficient available stock cannot be written off, HTTP 409 returns code `warehouse_write_off_stock_not_available`.
- Resolving the exception later follows the existing resolution behavior.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/exceptions?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| inventory_item_id | string |  |  |  |
| receiving_order_id | string |  |  |  |
| warehouse_id | string |  |  |  |
| location_id | string |  |  |  |
| exception_type | string |  |  |  |
| affected_quantity | number |  |  |  |
| exception_details | object |  |  |  |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "inventory_item_id": "{{inventory_item_id}}",
  "receiving_order_id": "{{receiving_order_id}}",
  "warehouse_id": "{{warehouse_id}}",
  "location_id": "{{bin_location_id}}",
  "exception_type": "missing",
  "affected_quantity": 1,
  "exception_details": {},
  "notes": "Box could not be located"
}
```

### Response 201 Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "exception": {
    "id": "exception-uuid",
    "exception_type": "missing",
    "status": "open",
    "stock_written_off": true,
    "written_off_quantity": 1,
    "written_off_by": "user-uuid",
    "written_off_at": "2026-08-22T10:15:00Z",
    "written_off_by_user": {
      "id": "user-uuid",
      "full_name": "Warehouse Receiver"
    },
    "affected_quantity": 1,
    "exception_details": {},
    "notes": "Box could not be located",
    "item": {
      "id": "item-uuid",
      "name": "Product Name",
      "sku": "SKU-123"
    },
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH-001"
    }
  },
  "open_exception": {
    "id": "exception-uuid",
    "exception_type": "missing",
    "exception_label": "Missing",
    "status": "open",
    "affected_quantity": 1,
    "stock_written_off": true,
    "written_off_quantity": 1,
    "written_off_by": "user-uuid",
    "written_off_at": "2026-08-22T10:15:00Z",
    "exception_details": {},
    "notes": "Box could not be located"
  }
}
```

### Response 201 Created - Pre-receipt Missing Without Stock

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "exception": {
    "id": "exception-uuid",
    "exception_type": "missing",
    "status": "open",
    "stock_written_off": false,
    "written_off_quantity": null,
    "written_off_by": null,
    "written_off_at": null,
    "written_off_by_user": null,
    "affected_quantity": 1,
    "exception_details": {},
    "notes": "Box could not be located",
    "item": {
      "id": "item-uuid",
      "name": "Product Name",
      "sku": "SKU-123"
    },
    "warehouse": {
      "id": "warehouse-uuid",
      "name": "Main Warehouse",
      "code": "WH-001"
    }
  },
  "open_exception": {
    "id": "exception-uuid",
    "exception_type": "missing",
    "exception_label": "Missing",
    "status": "open",
    "affected_quantity": 1,
    "stock_written_off": false,
    "written_off_quantity": null,
    "written_off_by": null,
    "written_off_at": null,
    "exception_details": {},
    "notes": "Box could not be located"
  }
}
```

### Response 409 Conflict - Stock Not Available for Write-off

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "warehouse_write_off_stock_not_available",
  "message": "Insufficient stock"
}
```

## GET /warehousing/exceptions/{{warehouse_exception_id}} — Get Warehouse Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-exceptions#get-get-warehouse-exception)

Fetches one warehouse exception with item, stock, warehouse, location, optional receiving-order provenance, picking order, optional `inventory_audit_id`, reporter, resolver, and write-off audit context. For an open damaged stock-backed exception, `package_item_id` identifies the exact order box and `available_actions` contains `return_to_origin` when the mapping and available stock exist.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/warehousing/exceptions/{{warehouse_exception_id}}?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_exception_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |



### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "exception": {
    "id": "exception-uuid",
    "exception_type": "damaged",
    "status": "open",
    "package_item_id": "package-item-uuid",
    "available_actions": [
      {
        "key": "return_to_origin",
        "exception_id": "exception-uuid",
        "package_item_id": "package-item-uuid"
      }
    ]
  }
}
```

## POST /warehousing/exceptions/{{warehouse_exception_id}}/return-to-origin — Return Damaged Package to Origin

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-exceptions#post-return-damaged-package-to-origin)

Creates and prepares one zero-fee internal reverse delivery from the current warehouse to the original sender for the exact damaged package linked to this exception. Requires warehouse mutation access. Tenant and system administrators may act without a warehouse assignment; other warehouse actors require an active staff assignment at the warehouse holding the package. `notes` is optional.

### Business rules
- The exception must be open and `damaged`; the exact inventory/package mapping and positive unreserved warehouse stock are the custody authority. A receiving order is optional audit provenance, not a prerequisite.
- The inventory item must have an exact `delivery_package_item_id`; ambiguous legacy mappings are rejected.
- The customer-bound staged outbound delivery must still be draft and unassigned.
- At least one other eligible package must remain for the customer; otherwise use the existing whole-order return flow.
- The reverse delivery uses `return_requests.default_delivery_service_id`, `delivery_flow=operational_return`, `delivery_fee=0`, `cod_amount=0`, and `payment_status=paid`. It creates no customer charge or refund.
- The source package leaves customer dispatch immediately through `return_status=return_approved`, but its primary status remains `pending` until the reverse delivery reaches the sender. Only then does the source package become `returned`.
- The source package is excluded from the customer-bound driver detail package list and its package/actionable counts while the return is approved, in transit, or completed. The separate reverse delivery keeps its own actionable package.
- Each P&D pickup completes its existing assignment-owned handoff once; the legacy handoff fallback does not create a second handoff. Staged parent status updates do not create physical handoffs; the child legs own pickup. Customer-bound pickup and completion affect only customer-bound inventory, preserving the separate return package's stock, reservation, and custody.
- Sender delivery completion sets the exact inventory item linked by `delivery_package_item_returns` to `returned_to_sender` custody. It does not select inventory through the original outbound link or change sibling inventory.
- The parent/root and customer-bound outbound delivery statuses are never changed by this action. Other packages continue normally.
- The exception is resolved with the actor and timestamp. The action is idempotent for the same exception and returns HTTP 200 with `idempotent_replay=true` on replay.
- Package-return reverse deliveries can be retried or reassigned after an exception but cannot use generic cancellation, because cancelling would strand the required physical return.

### Frontend integration contract
- Show this action only when the exception/item response contains `available_actions[].key=return_to_origin`.
- Call this endpoint with the `exception_id`; do not call the whole-order return endpoint and do not send a package status.
- On success, use `data.return_delivery_id` to open the reverse delivery and `data.package_return_status` for its package-return state.
- HTTP 409 means the action is stale or ineligible; use the stable `code` for branching and show the localized `message`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/warehousing/exceptions/{{warehouse_exception_id}}/return-to-origin?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_exception_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| notes | string |  |  |  |



### Request body (raw)

```json
{
  "notes": "Box crushed during inbound handling"
}
```

### Response 201 Created

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "The damaged package was prepared for return to the sender",
  "data": {
    "success": true,
    "action_key": "return_to_origin",
    "exception_id": "exception-uuid",
    "exception_status": "resolved",
    "resolved_by": "user-uuid",
    "resolved_at": "2026-09-11T14:30:00Z",
    "package_item_id": "package-item-uuid",
    "package_item_status": "pending",
    "package_return_id": "package-return-uuid",
    "package_return_status": "return_approved",
    "return_delivery_id": "return-delivery-uuid",
    "return_delivery_code": 1050,
    "return_delivery_status": "pending",
    "picking_order_id": "picking-order-uuid",
    "root_delivery_id": "root-delivery-uuid",
    "root_delivery_status": "pending",
    "outbound_delivery_id": "outbound-delivery-uuid",
    "outbound_delivery_status": "draft",
    "handoff_id": null,
    "idempotent_replay": false
  }
}
```

### Response 409 Conflict - Last Eligible Package

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "partial_return_requires_remaining_package",
  "message": "Use the whole-order return flow when no other package can continue to the customer"
}
```

### Response 409 Conflict - Customer Dispatch Started

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "warehouse_package_return_outbound_already_started",
  "message": "This package cannot be returned after customer dispatch has started"
}
```

## PUT /warehousing/exceptions/{{warehouse_exception_id}} — Update Warehouse Exception

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/warehousing/warehouse-exceptions#put-update-warehouse-exception)

Updates or resolves a warehouse exception. Allowed statuses are `open` and `resolved`. Change a wrong reason with `exception_type`; if an open exception has not written off stock before and the corrected tenant-defined type has `write_off_stock=true`, the same atomic write-off is applied once. When that correction completes an in-progress receipt, the authenticated updater becomes `received_by`, the receipt becomes `received_with_exceptions`, and an item corrected to `received_quantity=0` creates no stock and requires no bin. Changing away from that type or changing it again never restores or repeats the write-off. Resolve either a completed issue or a mistaken flag with `status=resolved` and an optional short note in `resolution_details.note`; resolution records `resolved_by` and `resolved_at` and cannot be reopened. A prior configured write-off is reversed exactly once. If this is the item's final open marker and one pending delivery still owns one valid completed picking order, the same stock is reserved back to that pick and item metadata records `reservation_state=reserved`; delivery, assignment, picking-order, and handoff statuses do not advance. If no valid preparation remains, restored stock stays available. Ambiguous or quantitatively inconsistent active preparation returns HTTP 409 `warehouse_preparation_inconsistent` and leaves the exception open.

### Frontend integration contract
- Continue sending the same resolution payload; no new request field is required.
- On success, replace the local exception with the returned `exception`.
- A missing unit with no physical receipt is a valid success and must not prompt for a bin.
- Handle HTTP 409 `warehouse_write_off_stock_not_available` for real stock conflicts and `warehouse_preparation_inconsistent` when existing picking/stock records require correction before resolution can finish.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/warehousing/exceptions/{{warehouse_exception_id}}?tenant_id={{tenant_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| warehouse_exception_id | path string | Yes | Variable used inside the request path. |  |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  |  | {{tenant_id}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| status | string |  |  |  |
| resolution_details | object |  |  |  |
| resolution_details.note | string |  |  |  |



### Request body (raw)

```json
{
  "status": "resolved",
  "resolution_details": {
    "note": "Issue sorted out"
  }
}
```

### Response 200 Success

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "exception": {
    "id": "exception-uuid",
    "status": "resolved",
    "picking_order_id": "picking-order-uuid",
    "resolution_details": {
      "note": "Issue sorted out"
    },
    "resolved_by": "user-uuid",
    "resolved_at": "2026-08-22T07:45:00Z",
    "resolved_by_user": {
      "id": "user-uuid",
      "full_name": "Warehouse Supervisor"
    }
  }
}
```

### Response 409 Conflict - Warehouse Preparation Inconsistent

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "code": "warehouse_preparation_inconsistent",
  "message": "The parcel's warehouse preparation records are inconsistent"
}
```

## GET /payment/system/method-types — System • List Payment Method Types

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin#get-system-list-payment-method-types)

Lists active payment method types. The COD record keeps canonical name `Cash on Delivery`, type `COD`, and gateway_code `cod`; its localized display_name is Cash.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/system/method-types

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token (system admin) | Bearer {{access_token}} |



## GET /payment/system/gateway-configs — System • List System Gateway Configs

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin#get-system-list-system-gateway-configs)

Retrieves all system gateway configurations (e.g., NASS).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/system/gateway-configs

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token (system admin) | Bearer {{access_token}} |



## GET /payment/system/gateway-configs — System • List System Gateway Configs (safe)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin#get-system-list-system-gateway-configs-safe)

Lists system gateway configs with secrets redacted.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/system/gateway-configs

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token (system admin) | Bearer {{access_token}} |



## POST /payment/system/gateway-configs — System • Upsert System Gateway Config (NASS)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin#post-system-upsert-system-gateway-config-nass)

Create or update the system NASS configuration (system scope).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/payment/system/gateway-configs

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token (system admin) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| provider_code | string |  |  |  |
| display_name | string |  |  |  |
| api_keys | object |  |  |  |
| api_keys.username | string |  |  |  |
| api_keys.password | string |  |  |  |
| base_url | string |  |  |  |
| webhook_secret | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "provider_code": "nass",
  "display_name": "Nass (System)",
  "api_keys": {
    "username": "merchant_username",
    "password": "merchant_password"
  },
  "base_url": "https://uat-gateway.nass.iq:9746",
  "webhook_secret": "wh_secret",
  "is_active": true
}
```

## GET /payment/admin/payment-methods — Admin • List Payment Methods

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin#get-admin-list-payment-methods)

Returns payment method types with commission and activation flags. The COD record keeps canonical name `Cash on Delivery`, type `COD`, and gateway_code `cod`; its localized display_name is Cash.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/admin/payment-methods

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token (system admin) | Bearer {{access_token}} |



## PUT /payment/admin/payment-methods/:pm_id — Admin • Update Payment Method

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin#put-admin-update-payment-method)

Updates payment method type fields (commission_percent, is_active). If provided, also merges partial provider system configuration for the related gateway (by payment method's gateway_code):
- system_gateway_config.api_keys.username/password are individually optional and merged into existing credentials (others unchanged).
- base_url, webhook_secret, display_name, is_active update only if present.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/payment/admin/payment-methods/:pm_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token (system admin) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pm_id | path string | Yes |  | <payment_method_type_uuid> |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| commission_percent | number |  |  |  |
| is_active | boolean |  |  |  |
| icon_url | string |  |  |  |
| system_gateway_config | object |  |  |  |
| system_gateway_config.api_keys | object |  |  |  |
| system_gateway_config.api_keys.username | string |  |  |  |
| system_gateway_config.api_keys.password | string |  |  |  |
| system_gateway_config.base_url | string |  |  |  |
| system_gateway_config.webhook_secret | string |  |  |  |
| system_gateway_config.display_name | object |  |  |  |
| system_gateway_config.display_name.en | string |  |  |  |
| system_gateway_config.display_name.ar | string |  |  |  |
| system_gateway_config.is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "commission_percent": 2.5,
  "is_active": true,
  "icon_url": "https://cdn.example.com/icons/nass.png",
  "system_gateway_config": {
    "api_keys": {
      "username": "new_username_optional",
      "password": "new_password_optional"
    },
    "base_url": "https://gateway.example.com",
    "webhook_secret": "wh_secret_optional",
    "display_name": {
      "en": "NASS Gateway",
      "ar": "بوابة ناس"
    },
    "is_active": true
  }
}
```

## POST /payment/admin/system-gateway-configs — Admin • Create System Gateway Config

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin#post-admin-create-system-gateway-config)

Creates a system-level gateway configuration (separate from system scope upsert).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/payment/admin/system-gateway-configs

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  | JWT token (system admin) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| provider_code | string |  |  |  |
| display_name | string |  |  |  |
| api_keys | object |  |  |  |
| api_keys.username | string |  |  |  |
| api_keys.password | string |  |  |  |
| base_url | string |  |  |  |
| webhook_secret | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "provider_code": "nass",
  "display_name": "Nass (Ondi)",
  "api_keys": {
    "username": "u",
    "password": "p"
  },
  "base_url": "https://uat-gateway.nass.iq:9746",
  "webhook_secret": "wh_secret",
  "is_active": true
}
```

## GET /payment/tenant/payment-methods — Tenant Payment Methods

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin#get-tenant-payment-methods)

Returns effective tenant payment methods for the authenticated tenant.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/tenant/payment-methods

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



## PUT /payment/tenant/payment-methods/:pm_id — Update Tenant Payment Method (supports inline config)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin#put-update-tenant-payment-method-supports-inline-config)

Updates tenant payment method settings.

### Rules
- If mode='tenant' and enabled=true → a tenant gateway config must exist (in DB) or be provided inline via `tenant_gateway_config`.
- If disabling or mode='system' → config is optional.
- If both `tenant_gateway_config_id` and `tenant_gateway_config` are provided, the config with that id is updated with provided fields.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/payment/tenant/payment-methods/:pm_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pm_id | path string | Yes |  | <payment_method_type_uuid> |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| enabled | boolean |  |  |  |
| mode | string |  |  |  |
| tenant_gateway_config_id | string |  |  |  |
| tenant_gateway_config | object |  |  |  |
| tenant_gateway_config.api_keys | object |  |  |  |
| tenant_gateway_config.api_keys.username | string |  |  |  |
| tenant_gateway_config.api_keys.password | string |  |  |  |
| tenant_gateway_config.base_url | string |  |  |  |
| tenant_gateway_config.webhook_secret | string |  |  |  |
| tenant_gateway_config.settings | object |  |  |  |



### Request body (raw)

```json
{
  "enabled": true,
  "mode": "tenant",
  "tenant_gateway_config_id": "<optional-existing-config-id>",
  "tenant_gateway_config": {
    "api_keys": {
      "username": "...",
      "password": "..."
    },
    "base_url": "https://uat-gateway.nass.iq:9746",
    "webhook_secret": "wh_secret",
    "settings": {}
  }
}
```

## PUT /payment/tenant/payment-methods/:pm_id — Update Tenant Payment Method (partial api_keys: username)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin#put-update-tenant-payment-method-partial-api-keys-username)

### Partially updates tenant gateway api_keys

only username is updated.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/payment/tenant/payment-methods/:pm_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pm_id | path string | Yes |  | <payment_method_type_uuid> |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| enabled | boolean |  |  |  |
| mode | string |  |  |  |
| tenant_gateway_config | object |  |  |  |
| tenant_gateway_config.api_keys | object |  |  |  |
| tenant_gateway_config.api_keys.username | string |  |  |  |



### Request body (raw)

```json
{
  "enabled": true,
  "mode": "tenant",
  "tenant_gateway_config": {
    "api_keys": {
      "username": "new_username"
    }
  }
}
```

## PUT /payment/tenant/payment-methods/:pm_id — Update Tenant Payment Method (partial api_keys: password)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin#put-update-tenant-payment-method-partial-api-keys-password)

### Partially updates tenant gateway api_keys

only password is updated.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/payment/tenant/payment-methods/:pm_id

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| pm_id | path string | Yes |  | <payment_method_type_uuid> |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| enabled | boolean |  |  |  |
| mode | string |  |  |  |
| tenant_gateway_config | object |  |  |  |
| tenant_gateway_config.api_keys | object |  |  |  |
| tenant_gateway_config.api_keys.password | string |  |  |  |



### Request body (raw)

```json
{
  "enabled": true,
  "mode": "tenant",
  "tenant_gateway_config": {
    "api_keys": {
      "password": "new_password"
    }
  }
}
```

## POST /payment/tenant/tenant-gateway-configs — Create Tenant Gateway Config

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin#post-create-tenant-gateway-config)

Creates a tenant gateway configuration for the given PM type.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/payment/tenant/tenant-gateway-configs

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| payment_method_type_id | string |  |  |  |
| api_keys | object |  |  |  |
| api_keys.username | string |  |  |  |
| api_keys.password | string |  |  |  |
| base_url | string |  |  |  |
| webhook_secret | string |  |  |  |



### Request body (raw)

```json
{
  "payment_method_type_id": "<pm_type_uuid>",
  "api_keys": {
    "username": "merchant_username",
    "password": "merchant_password"
  },
  "base_url": "https://uat-gateway.nass.iq:9746",
  "webhook_secret": "wh_secret"
}
```

## GET /payment/user/payment-methods — User Payment Methods (enabled only)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/user#get-user-payment-methods-enabled-only)

Lists only enabled payment methods for the authenticated user's tenant. Optionally filter by method_type (e.g., GATEWAY, WALLET, COD). The COD record keeps canonical name `Cash on Delivery`, type `COD`, and gateway_code `cod`; its localized display_name is Cash.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/user/payment-methods?method_type=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| method_type | query string | No | Optional filter by payment method type (e.g., GATEWAY, WALLET, COD). |  |



## POST /payment/payments — Create Payment (NASS redirect)

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/user#post-create-payment-nass-redirect)

Creates a payment and returns redirect URL for NASS when gateway is active.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/payment/payments

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| order_id | string |  |  |  |
| tenant_id | string |  |  |  |
| user_id | string |  |  |  |
| payment_method_type_id | string |  |  |  |
| amount | number |  |  |  |
| currency | string |  |  |  |
| back_ref | string |  |  |  |
| metadata | object |  |  |  |
| metadata.note | string |  |  |  |



### Request body (raw)

```json
{
  "order_id": "order-123",
  "tenant_id": "{{tenant_id}}",
  "user_id": "{{user_id}}",
  "payment_method_type_id": "<pm_type_uuid_NASS>",
  "amount": 100,
  "currency": "IQD",
  "back_ref": "https://app.example.com/payment/return",
  "metadata": {
    "note": "checkout"
  }
}
```

## POST /payment/payments/:id/confirm — Confirm Payment

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/user#post-confirm-payment)

Confirms a payment and triggers wallet crediting when applicable.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/payment/payments/:id/confirm

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| id | path string | Yes |  | <payment_uuid> |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| gateway_transaction_id | string |  |  |  |
| gateway_response | object |  |  |  |
| status | string |  |  |  |



### Request body (raw)

```json
{
  "gateway_transaction_id": "<rrn_or_order>",
  "gateway_response": {},
  "status": "COMPLETED"
}
```

## POST /payment/payments/confirm — Confirm Payment by Gateway Txn

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/user#post-confirm-payment-by-gateway-txn)

Confirms a payment by gateway transaction id (e.g., NASS ORDER/RRN).

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/payment/payments/confirm

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| gateway_transaction_id | string |  |  |  |



### Request body (raw)

```json
{
  "gateway_transaction_id": "<rrn_or_order>"
}
```

## POST /payment/webhooks/nass — NASS Webhook

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/webhook#post-nass-webhook)

NASS webhook that confirms the payment internally using ORDER/rrn.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/payment/webhooks/nass

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| terminal | string |  |  |  |
| actionCode | string |  |  |  |
| responseCode | string |  |  |  |
| statusMsg | string |  |  |  |
| amount | string |  |  |  |
| currency | string |  |  |  |
| rrn | string |  |  |  |
| orderId | string |  |  |  |



### Request body (raw)

```json
{
  "terminal": "00053402",
  "actionCode": "0",
  "responseCode": "00",
  "statusMsg": "Approved",
  "amount": "5000",
  "currency": "368",
  "rrn": "505801383493",
  "orderId": "<ORDER_FROM_INITIATE>"
}
```

## GET /payment/system/method-types — List Payment Method Types

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-2#get-list-payment-method-types)

Retrieves all available payment method types in the system including COD, Wallet, and gateway payment methods. The COD record keeps canonical name `Cash on Delivery`, type `COD`, and gateway_code `cod`; its localized display_name is Cash.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/system/method-types

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for system admin users (required) | Bearer {{access_token}} |



## GET /payment/system/tenant-configs — List Tenant Gateway Configurations

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-2#get-list-tenant-gateway-configurations)

Retrieves all tenant gateway configurations across the system for monitoring and management.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/system/tenant-configs

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for system admin users (required) | Bearer {{access_token}} |



## POST /payment/system/tenant-configs — Create Tenant Gateway Configuration

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-2#post-create-tenant-gateway-configuration)

Creates a new payment gateway configuration for a specific tenant with validation and connectivity testing.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/payment/system/tenant-configs

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for system admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | string |  |  |  |
| payment_method_type_id | string |  |  |  |
| gateway_code | string |  |  |  |
| api_keys | object |  |  |  |
| api_keys.key_id | string |  |  |  |
| api_keys.key_secret | string |  |  |  |
| webhook_secret | string |  |  |  |
| base_url | string |  |  |  |
| settings | object |  |  |  |
| settings.currency | string |  |  |  |
| settings.capture_method | string |  |  |  |



### Request body (raw)

```json
{
  "tenant_id": "{{tenant_id}}",
  "payment_method_type_id": "razorpay-method-uuid",
  "gateway_code": "razorpay",
  "api_keys": {
    "key_id": "rzp_test_1234567890",
    "key_secret": "your_razorpay_secret_key"
  },
  "webhook_secret": "your_webhook_secret",
  "base_url": "https://api.razorpay.com/v1",
  "settings": {
    "currency": "USD",
    "capture_method": "automatic"
  }
}
```

## PUT /payment/system/tenant-configs/:configId — Update Tenant Gateway Configuration

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/system-admin-2#put-update-tenant-gateway-configuration)

Updates an existing tenant gateway configuration with validation and testing.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/payment/system/tenant-configs/:configId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for system admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| configId | path string | Yes | UUID of the configuration to update | config-uuid-1 |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| api_keys | object |  |  |  |
| api_keys.key_id | string |  |  |  |
| api_keys.key_secret | string |  |  |  |
| settings | object |  |  |  |
| settings.currency | string |  |  |  |
| settings.capture_method | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "api_keys": {
    "key_id": "rzp_live_1234567890",
    "key_secret": "updated_secret_key"
  },
  "settings": {
    "currency": "USD",
    "capture_method": "manual"
  },
  "is_active": true
}
```

## GET /payment/tenant/methods — List Payment Methods for Market

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin#get-list-payment-methods-for-market)

Retrieves payment methods configured for a specific market with their enabled status and settings.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/tenant/methods?market_id={{market_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| market_id | query string | Yes | Market ID to get payment methods for (required) | {{market_id}} |



## PUT /payment/tenant/methods/:methodId — Update Payment Method Settings

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin#put-update-payment-method-settings)

Updates payment method settings for a specific market including enabling/disabling and configuration.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/payment/tenant/methods/:methodId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| methodId | path string | Yes | UUID of the payment method to update | cod-method-uuid |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| market_id | string |  |  |  |
| is_enabled | boolean |  |  |  |
| settings | object |  |  |  |
| settings.maxAmount | number |  |  |  |
| settings.deliveryCharge | number |  |  |  |
| settings.instructions | object |  |  |  |
| settings.instructions.en | string |  |  |  |
| settings.instructions.ar | string |  |  |  |
| settings.instructions.ku | string |  |  |  |
| display_order | number |  |  |  |



### Request body (raw)

```json
{
  "market_id": "{{market_id}}",
  "is_enabled": true,
  "settings": {
    "maxAmount": 10000,
    "deliveryCharge": 100,
    "instructions": {
      "en": "Please have exact cash ready when the order is delivered",
      "ar": "يرجى إعداد المبلغ النقدي الدقيق عند تسليم الطلب",
      "ku": "تکایە پارەی ورد ئامادە بکە کاتێک داواکارییەکە دەگات"
    }
  },
  "display_order": 1
}
```

## GET /payment/tenant/transactions — List Payment Transactions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/tenant-admin#get-list-payment-transactions)

Retrieves payment transactions for the tenant with filtering and pagination support.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/tenant/transactions?page=1&limit=20&status=SUCCESS&market_id={{market_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for tenant admin users (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Items per page (max: 100) | 20 |
| status | query string |  | Filter by payment status | SUCCESS |
| market_id | query string |  | Filter by specific market | {{market_id}} |



## GET /payment/user/methods — Get Available Payment Methods

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/user#get-get-available-payment-methods-2)

Retrieves available payment methods for the authenticated user with real-time availability checking.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/user/methods?market_id={{market_id}}

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| market_id | query string | Yes | Market ID to get payment methods for (required) | {{market_id}} |



## POST /payment/user/initiate-payment — Initiate Payment

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/user#post-initiate-payment)

Initiates a payment for the authenticated user through the selected payment method.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/payment/user/initiate-payment

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| orderId | string |  |  |  |
| tenantId | string |  |  |  |
| marketId | string |  |  |  |
| paymentMethodTypeId | string |  |  |  |
| amount | number |  |  |  |
| currency | string |  |  |  |
| customerEmail | string |  |  |  |
| metadata | object |  |  |  |
| metadata.productId | string |  |  |  |
| metadata.campaignId | string |  |  |  |



### Request body (raw)

```json
{
  "orderId": "order-12345",
  "tenantId": "{{tenant_id}}",
  "marketId": "{{market_id}}",
  "paymentMethodTypeId": "razorpay-method-uuid",
  "amount": 299.99,
  "currency": "USD",
  "customerEmail": "user@example.com",
  "metadata": {
    "productId": "product-uuid-123",
    "campaignId": "campaign-summer2024"
  }
}
```

## GET /payment/user/status/:paymentId — Get Payment Status

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/user#get-get-payment-status)

Retrieves the current status of a payment for the authenticated user.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/payment/user/status/:paymentId

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string | Yes | JWT token for authenticated users (required) | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| paymentId | path string | Yes | UUID of the payment to check status | payment-uuid-123 |



## POST /payment/webhooks/:gateway_code — Payment Gateway Webhook

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/billing-subscriptions/webhook#post-payment-gateway-webhook)

Handles webhook notifications from payment gateways with signature verification and payment status updates.

Authentication: Authentication headers are not specified in this request example. Check the endpoint notes before integrating.

Request: POST https://staging-api.ondi.io/functions/v1/payment/webhooks/:gateway_code

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |
| X-Razorpay-Signature | header string |  | Razorpay webhook signature for verification | {{razorpay_signature}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| gateway_code | path string | Yes | Gateway identifier (razorpay, stripe, etc.) | razorpay |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| entity | string |  |  |  |
| account_id | string |  |  |  |
| event | string |  |  |  |
| contains | array<string> |  |  |  |
| payload | object |  |  |  |
| payload.payment | object |  |  |  |
| payload.payment.entity | object |  |  |  |
| payload.payment.entity.id | string |  |  |  |
| payload.payment.entity.entity | string |  |  |  |
| payload.payment.entity.amount | number |  |  |  |
| payload.payment.entity.currency | string |  |  |  |
| payload.payment.entity.status | string |  |  |  |
| payload.payment.entity.order_id | string |  |  |  |
| payload.payment.entity.method | string |  |  |  |
| payload.payment.entity.description | string |  |  |  |
| payload.payment.entity.email | string |  |  |  |
| payload.payment.entity.notes | object |  |  |  |
| payload.payment.entity.notes.order_id | string |  |  |  |
| payload.payment.entity.notes.customer_id | string |  |  |  |
| payload.payment.entity.created_at | number |  |  |  |
| created_at | number |  |  |  |



### Request body (raw)

```json
{
  "entity": "event",
  "account_id": "acc_BFQ7uQEaa4j3z7",
  "event": "payment.captured",
  "contains": [
    "payment"
  ],
  "payload": {
    "payment": {
      "entity": {
        "id": "pay_BFQ7uQEaa4j3z8",
        "entity": "payment",
        "amount": 29999,
        "currency": "USD",
        "status": "captured",
        "order_id": "order_BFQ7uQEaa4j3z9",
        "method": "card",
        "description": "Payment for order order-12345",
        "email": "user@example.com",
        "notes": {
          "order_id": "order-12345",
          "customer_id": "user-uuid-456"
        },
        "created_at": 1701234567
      }
    }
  },
  "created_at": 1701234567
}
```

## GET /webhooks/subscriptions — List Webhook Subscriptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/overview#get-list-webhook-subscriptions)

Get a paginated list of webhook subscriptions for the current tenant. Requires view:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/webhooks/subscriptions?page=1&limit=10&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of items per page | 10 |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook subscriptions retrieved successfully",
  "subscriptions": [
    {
      "id": "uuid",
      "module_id": "DELIVERY",
      "webhook_url": "https://example.com/webhook",
      "secret_header_key": "X-Webhook-Secret",
      "is_active": true,
      "created_at": "2023-08-01T10:00:00Z",
      "updated_at": "2023-08-01T10:00:00Z"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 403 Error - Insufficient Permissions

Forbidden

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Insufficient permissions"
}
```

## GET /webhooks/users/:user_id/subscriptions — List User Webhook Subscriptions

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/overview#get-list-user-webhook-subscriptions)

Get a paginated list of webhook subscriptions for a specific user. Requires view:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/webhooks/users/:user_id/subscriptions?page=1&limit=10&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_id | path string | Yes | ID of the user to list webhook subscriptions for | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of items per page | 10 |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook subscriptions retrieved successfully",
  "subscriptions": [
    {
      "id": "uuid",
      "module_id": "DELIVERY",
      "webhook_url": "https://example.com/webhook",
      "secret_header_key": "X-Webhook-Secret",
      "is_active": true,
      "user_id": "uuid",
      "created_at": "2023-08-01T10:00:00Z",
      "updated_at": "2023-08-01T10:00:00Z"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1,
  "user_id": "uuid"
}
```

## POST /webhooks/subscriptions — Create Webhook Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/overview#post-create-webhook-subscription)

Create a new webhook subscription for the current tenant. Requires manage:webhooks:tenant permission.

### Required Fields
- `module_id`: Identifier for the module generating events (e.g., 'DELIVERY')
- `webhook_url`: HTTPS URL to receive webhook events

### Optional Fields
- `secret_header_key`: Name of header to include webhook secret in
- `secret_header_value`: Secret value for webhook authentication
- `is_active`: Whether the subscription is active. An explicit `false` is preserved; when omitted, it defaults to `true`.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/webhooks/subscriptions?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| module_id | string |  |  |  |
| webhook_url | string |  |  |  |
| secret_header_key | string |  |  |  |
| secret_header_value | string |  |  |  |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "module_id": "DELIVERY",
  "webhook_url": "https://example.com/webhook",
  "secret_header_key": "X-Webhook-Secret",
  "secret_header_value": "your-secret-key-here",
  "is_active": false
}
```

### Response 201 Success Response

Created

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook subscription created successfully",
  "subscription": {
    "id": "uuid",
    "tenant_id": "uuid",
    "module_id": "DELIVERY",
    "webhook_url": "https://example.com/webhook",
    "secret_header_key": "X-Webhook-Secret",
    "is_active": false,
    "created_at": "2023-08-01T10:00:00Z",
    "updated_at": "2023-08-01T10:00:00Z"
  }
}
```

### Response 400 Error - Invalid URL

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Webhook URL must be HTTPS"
}
```

### Response 400 Error - Missing Required Field

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Field required: module_id"
}
```

### Response 400 Error - Subscription Already Exists

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Webhook subscription already exists"
}
```

## GET /webhooks/subscriptions/:subscriptionId — Get Webhook Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/overview#get-get-webhook-subscription)

Get details of a specific webhook subscription by ID. Requires view:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/webhooks/subscriptions/:subscriptionId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| subscriptionId | path string | Yes | ID of the webhook subscription to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook subscription retrieved successfully",
  "subscription": {
    "id": "uuid",
    "module_id": "DELIVERY",
    "webhook_url": "https://example.com/webhook",
    "secret_header_key": "X-Webhook-Secret",
    "is_active": true,
    "created_at": "2023-08-01T10:00:00Z",
    "updated_at": "2023-08-01T10:00:00Z"
  }
}
```

### Response 404 Error - Subscription Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Webhook subscription not found"
}
```

## GET /webhooks/users/:user_id/subscriptions/:subscriptionId — Get User Webhook Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/overview#get-get-user-webhook-subscription)

Get details of a specific webhook subscription by ID for a specific user. Requires view:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/webhooks/users/:user_id/subscriptions/:subscriptionId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_id | path string | Yes | ID of the user | uuid |
| subscriptionId | path string | Yes | ID of the webhook subscription to retrieve | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook subscription retrieved successfully",
  "subscription": {
    "id": "uuid",
    "module_id": "DELIVERY",
    "webhook_url": "https://example.com/webhook",
    "secret_header_key": "X-Webhook-Secret",
    "is_active": true,
    "user_id": "uuid",
    "created_at": "2023-08-01T10:00:00Z",
    "updated_at": "2023-08-01T10:00:00Z"
  },
  "user_id": "uuid"
}
```

## PUT /webhooks/subscriptions/:subscriptionId — Update Webhook Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/overview#put-update-webhook-subscription)

Partially updates the tenant-owned webhook subscription identified by subscriptionId. Requires manage:webhooks:tenant permission. module_id is not required and is not updated. Send only `is_active: true` to activate and `is_active: false` to deactivate. Updating webhook_url performs the existing reachability test before saving.

### Updatable Fields
- `webhook_url`: HTTPS URL to receive webhook events
- `secret_header_key`: Name of the webhook authentication header
- `secret_header_value`: Value of the webhook authentication header
- `is_active`: Boolean active state
- `user_id`: Optional business-user scope

### Frontend contract

Use the subscription `id` from GET /webhooks/subscriptions as subscriptionId. Show an Activate action when is_active is false and a Deactivate action when it is true. Submit an is_active-only body, disable the action while pending, and replace the row with the returned subscription after HTTP 200. A module-only list entry without an id is not an existing subscription and cannot be activated. Important errors: 400 for invalid input or a rejected updated URL, 404 when the tenant-owned subscription is not found, and 500 for an unexpected update failure.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/webhooks/subscriptions/:subscriptionId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| subscriptionId | path string | Yes | ID of the webhook subscription to update | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "is_active": true
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook Subscription Updated Successfully",
  "subscription": {
    "id": "uuid",
    "module_id": "DELIVERY",
    "webhook_url": "https://example.com/webhook",
    "secret_header_key": "X-Webhook-Secret",
    "is_active": true,
    "user_id": null,
    "created_at": "2023-08-01T10:00:00Z",
    "updated_at": "2023-08-01T11:00:00Z"
  }
}
```

### Response 404 Error - Subscription Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Webhook subscription not found"
}
```

### Response 400 Error - Invalid URL

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Webhook URL must be HTTPS"
}
```

## PUT /webhooks/users/:user_id/subscriptions/:subscriptionId — Update User Webhook Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/overview#put-update-user-webhook-subscription)

Partially updates an existing webhook subscription for the business user identified in the path. Requires manage:webhooks:tenant permission. module_id is not required and is not updated. Send only `is_active: true` to activate and `is_active: false` to deactivate.

### Updatable Fields
- `webhook_url`: HTTPS URL to receive webhook events
- `secret_header_key`: Name of the webhook authentication header
- `secret_header_value`: Value of the webhook authentication header
- `is_active`: Boolean active state

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: PUT https://staging-api.ondi.io/functions/v1/webhooks/users/:user_id/subscriptions/:subscriptionId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_id | path string | Yes | ID of the user | uuid |
| subscriptionId | path string | Yes | ID of the webhook subscription to update | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| is_active | boolean |  |  |  |



### Request body (raw)

```json
{
  "is_active": true
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook Subscription Updated Successfully",
  "subscription": {
    "id": "uuid",
    "module_id": "DELIVERY",
    "webhook_url": "https://example.com/webhook",
    "secret_header_key": "X-Webhook-Secret",
    "is_active": true,
    "user_id": "uuid",
    "created_at": "2023-08-01T10:00:00Z",
    "updated_at": "2023-08-01T11:00:00Z"
  }
}
```

## DELETE /webhooks/subscriptions/:subscriptionId — Delete Webhook Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/overview#delete-delete-webhook-subscription)

Delete a webhook subscription by ID. Requires manage:webhooks:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/webhooks/subscriptions/:subscriptionId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| subscriptionId | path string | Yes | ID of the webhook subscription to delete | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook subscription deleted successfully"
}
```

### Response 404 Error - Subscription Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Webhook subscription not found"
}
```

## DELETE /webhooks/users/:user_id/subscriptions/:subscriptionId — Delete User Webhook Subscription

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/overview#delete-delete-user-webhook-subscription)

Delete a webhook subscription by ID for a specific user. Requires manage:webhooks:tenant permission.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: DELETE https://staging-api.ondi.io/functions/v1/webhooks/users/:user_id/subscriptions/:subscriptionId?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| user_id | path string | Yes | ID of the user | uuid |
| subscriptionId | path string | Yes | ID of the webhook subscription to delete | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook subscription deleted successfully"
}
```

## POST /webhooks/test — Test Webhook URL

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/webhook-testing#post-test-webhook-url)

Tests if a webhook URL is valid and reachable. The endpoint must return a 2xx status code to be considered valid. You can provide optional secret_header_key and secret_header_value to test authentication.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/webhooks/test?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Response language | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| webhook_url | string |  |  |  |
| secret_header_key | string |  |  |  |
| secret_header_value | string |  |  |  |



### Request body (raw)

```json
{
  "webhook_url": "https://example.com/webhook",
  "secret_header_key": "X-Webhook-Secret",
  "secret_header_value": "your-secret-value-here"
}
```

## POST /webhooks/receive-test — Receive Test Delivery Update Webhook

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-subscriptions/webhook-testing#post-receive-test-delivery-update-webhook)

Test receiver and outbound contract example for delivery updates. OnDi keeps the existing `order_update` envelope and existing `status`, `delivery_id`, `delivery_code`, `customer_id`, and optional `driver` fields. `trigger`, `update_types`, current commercial values, `id`, and `previous` are additive. `update_types` is always an array and may contain `StatusUpdate`, `CODUpdate`, `PriceUpdate`, `PaymentSideUpdate`, and `PaymentMethodUpdate`. One delivery-row UPDATE produces at most one webhook and lists every tracked field changed by that statement. `previous` always contains the complete prior tracked snapshot. Receivers must inspect `update_types` instead of assuming every `order_update` is a status change. The real sender includes `Content-Type`, `X-Webhook-ID`, `X-Webhook-Timestamp`, and the subscription's configured secret header. The sender continues replacing outbound `data.delivery_id` with `data.delivery_code`. Any 2xx response is success; non-2xx responses are recorded as failed delivery attempts.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/webhooks/receive-test

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |
| X-Webhook-ID | header string |  |  | webhook-delivery-log-uuid |
| X-Webhook-Timestamp | header string |  |  | {{$isoTimestamp}} |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| event_type | string |  |  |  |
| module | string |  |  |  |
| tenant_id | string |  |  |  |
| data | object |  |  |  |
| data.status | string |  |  |  |
| data.delivery_id | string |  |  |  |
| data.delivery_code | string |  |  |  |
| data.customer_id | string |  |  |  |
| data.trigger | string |  |  |  |
| data.update_types | array<string> |  |  |  |
| data.id | string |  |  |  |
| data.codAmount | number |  |  |  |
| data.price | number |  |  |  |
| data.paymentSide | string |  |  |  |
| data.paymentMethod | string |  |  |  |
| data.previous | object |  |  |  |
| data.previous.status | string |  |  |  |
| data.previous.codAmount | number |  |  |  |
| data.previous.price | number |  |  |  |
| data.previous.paymentSide | string |  |  |  |
| data.previous.paymentMethod | string |  |  |  |



### Request body (raw)

```json
{
  "event_type": "order_update",
  "module": "delivery",
  "tenant_id": "tenant-uuid",
  "data": {
    "status": "delivered",
    "delivery_id": "1830",
    "delivery_code": "1830",
    "customer_id": "customer-uuid",
    "trigger": "Updated",
    "update_types": [
      "StatusUpdate",
      "CODUpdate",
      "PriceUpdate"
    ],
    "id": "1830",
    "codAmount": 5000,
    "price": 2500,
    "paymentSide": "sender",
    "paymentMethod": "wallet",
    "previous": {
      "status": "picked_up",
      "codAmount": 3000,
      "price": 2000,
      "paymentSide": "sender",
      "paymentMethod": "wallet"
    }
  }
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook received successfully",
  "timestamp": "2026-07-18T12:50:31.100Z"
}
```

## GET /webhooks/events/:subscriptionId — List Webhook Events

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-events#get-list-webhook-events)

Returns webhook delivery history for one subscription belonging to the authenticated tenant, newest first. Requires view:tenant or manage:tenant permission (system admins are also allowed), a tenant context, and subscriptionId as a valid UUID path parameter. subscriptionId is the saved webhook subscription id returned in GET /webhooks/subscriptions; it is not module_id. Module entries without an id do not have a saved subscription, so history must not be requested for them. The tenant_id and subscription_id database filters are always applied before exact counting, optional status filtering, sorting, and pagination. page defaults to 1; limit defaults to 25 and cannot exceed 100. payload is the inner event data placed under data in the outbound webhook envelope, not a captured full HTTP request. error_message contains the latest failure detail, retry_count currently counts failed delivery attempts, and created_at is when the event was queued. next_retry_time is an internal database field and is not returned by this API. Important errors: 400 when subscriptionId is invalid, pagination/status is invalid, or tenant context is missing; 403 for insufficient permission; and 500 for retrieval failure. Frontend contract: call this API only after selecting a saved subscription and put that subscription's id in the URL path; show event_type, status, created_at as Queued at, http_status_code, retry_count as Failure count, and a short error_message in the table. Open a details view for formatted payload and the full error_message. Use each event id as eventId for retry, use total with page and limit for pagination, and show a No webhook events yet state when events is empty.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/webhooks/events/:subscriptionId?page=1&limit=10&status=&language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| subscriptionId | path string | Yes | Required saved webhook subscription UUID. This is the subscription id, not module_id | {{webhook_subscription_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| page | query string |  | Page number for pagination | 1 |
| limit | query string |  | Number of items per page | 10 |
| status | query string |  | Filter by delivery status (pending, retrying, success, failed) |  |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook delivery history retrieved successfully",
  "events": [
    {
      "id": "443f197a-06b8-463c-a559-faa7b5e8e134",
      "subscription_id": "04938225-1beb-480e-b320-ded5fd75d648",
      "tenant_id": "a1b2c3d4-e5f6-4321-8901-123456789abc",
      "event_type": "order_update",
      "payload": {
        "delivery_code": "3339A",
        "status": "delivered",
        "update_types": [
          "StatusUpdate"
        ]
      },
      "status": "failed",
      "http_status_code": 403,
      "error_message": "Webhook responded with status 403",
      "retry_count": 1,
      "created_at": "2026-09-06T15:14:14.517Z"
    }
  ],
  "page": 1,
  "limit": 10,
  "total": 1
}
```

### Response 400 Error - Invalid Subscription ID

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Enter a valid ID"
}
```

## POST /webhooks/events/:eventId/retry — Retry Failed Webhook

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/webhooks/webhook-events#post-retry-failed-webhook)

Synchronously retries one failed webhook delivery for the authenticated tenant. Requires manage:tenant permission (system admins are also allowed) and a tenant context. The API atomically changes failed to retrying before invoking the existing webhook sender, so concurrent retry requests do not send duplicates. It preserves the original event id and X-Webhook-ID. HTTP 200 means the retry attempt finished; inspect status to see whether the subscriber accepted it. Important errors: 400 for an invalid event UUID or missing tenant context, 403 for insufficient permission, 404 when the event is outside the tenant or does not exist, 409 when it is not failed or another retry claimed it, and 502 when the sender could not be invoked. Frontend contract: POST with no request body, disable retry unless status is failed, keep the action disabled while pending, and refresh history after any 200 response.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/webhooks/events/:eventId/retry?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| eventId | path string | Yes | ID of the webhook event to retry | uuid |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string |  | Preferred language for response messages | en |



### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "Webhook delivered successfully on retry",
  "event_id": "443f197a-06b8-463c-a559-faa7b5e8e134",
  "status": "success",
  "retry_count": 1
}
```

### Response 404 Error - Event Not Found

Not Found

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Webhook event not found"
}
```

### Response 409 Error - Event Not Retryable

Conflict

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Only failed webhook deliveries can be retried"
}
```

### Response 502 Error - Sender Unavailable

Bad Gateway

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Webhook delivery retry could not be started"
}
```

## GET /audit-logs — List Audit Logs

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/audit-logs#get-list-audit-logs-2)

Retrieves a paginated list of audit logs with sorting, per-column filtering, and free-text search.

- Requires valid authentication token or API key
- Requires 'manage:operations:tenant' permission or system admin

### Sorting (`sort_by` + `sort_order`)
- Supported columns: `action`, `method`, `endpoint`, `actor_name`, `status_code`, `created_at`, `response_time_ms`
- `action` sorts by endpoint; `actor_name` sorts by the joined user's full_name
- Default: `created_at desc`

### Column filters (`column_filters`)
- URL-encoded JSON, AND semantics across all keys
- `action`: substring match across method+endpoint
- `method`: exact match (auto-uppercased)
- `endpoint`: case-insensitive substring match
- `actor_name`: case-insensitive substring match on the actor's full_name
- `status_code`: numeric equality
- `created_at`: `{ from?: string, to?: string }` range; plain `YYYY-MM-DD` values are expanded to full UTC day bounds, and `to` is inclusive through the end of that day

### Response fields
- `user`: joined actor (id, full_name)
- `action`: computed as "{method} {endpoint}"
- `tenant`: joined tenant (id, name)
- `response_time_ms`: existing handler duration
- `time_distribution`: flat JSON object of server-side durations in milliseconds. Keys are backend-controlled and end in `_ms`; a key is omitted when that stage did not execute. Nested totals such as `handler_ms` and `delivery_list_logic_ms` overlap with their child call timings and must not be summed.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/audit-logs?tenant_id=&page=1&limit=50&search=&sort_by=created_at&sort_order=desc&column_filters=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (required for tenant admins; system admins may omit to query across tenants) |  |
| page | query string |  | Page number for pagination (default: 1) | 1 |
| limit | query string |  | Number of items per page (default: 25, max: 200) | 50 |
| search | query string | No | Free-text search across endpoint/method/ip_address/user_agent (optional) |  |
| sort_by | query string |  | Column to sort by. Supported: action, method, endpoint, actor_name, status_code, created_at, response_time_ms (default: created_at) | created_at |
| sort_order | query string |  | Sort direction: asc or desc (default: desc) | desc |
| column_filters | query string |  | URL-encoded JSON object with per-column filters (AND semantics). Supported keys: action (string), method (string), endpoint (string), actor_name (string), status_code (number), created_at ({ from?: string, to?: string }). Plain YYYY-MM-DD values are normalized to whole-day UTC bounds, so `to` stays inclusive through 23:59:59.999Z. Example: {"method":"GET","status_code":200,"created_at":{"from":"2026-05-01","to":"2026-05-11"}} |  |
| actor_id | query string | No | Filter by actor user ID (optional) |  |
| start_date | query string | No | Legacy date filter — start date YYYY-MM-DD (optional; prefer column_filters.created_at.from) |  |
| end_date | query string | No | Legacy date filter — end date YYYY-MM-DD (optional; prefer column_filters.created_at.to) |  |
| endpoint | query string | No | Filter by exact endpoint (optional; prefer column_filters.endpoint for partial match) |  |
| endpoint_like | query string | No | Filter by endpoint substring (case-insensitive, optional) |  |
| method | query string | No | Filter by HTTP method GET/POST/PUT/DELETE (optional; prefer column_filters.method) |  |
| status_code | query string | No | Filter by HTTP status code (optional; prefer column_filters.status_code) |  |



### Response 200 Success Response



```json
{
  "success": true,
  "message": "Audit Logs Retrieved Successfully",
  "audit_logs": [
    {
      "id": "audit-log-uuid",
      "endpoint": "/delivery/deliveries?page=1&limit=10",
      "method": "GET",
      "status_code": 200,
      "response_time_ms": 842,
      "time_distribution": {
        "middleware_auth_ms": 184.25,
        "auth_get_user_call_ms": 92.11,
        "auth_context_call_ms": 74.38,
        "delivery_main_query_call_ms": 113.47,
        "delivery_children_query_call_ms": 96.82,
        "delivery_response_serialize_ms": 3.14,
        "handler_ms": 842.19,
        "server_processing_ms": 1044.72
      }
    }
  ],
  "page": 1,
  "limit": 25,
  "total": 1
}
```

## GET /audit-logs/{{audit_log_id}} — Get Audit Log By Id

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/reporting-audit-webhooks/audit-logs#get-get-audit-log-by-id)

Retrieves a single audit log by id (tenant-scoped).

- Requires valid authentication token or API key
- Requires 'manage:operations:tenant' permission or system admin

### Timing contract
- `time_distribution` is a flat JSON object containing backend-controlled server durations in milliseconds.
- Keys ending in `_call_ms` measure outbound service/database calls as observed by the Edge Function, including service and network time.
- Keys are omitted for stages that did not execute.
- Total/phase keys overlap with their component keys and must not be summed.

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: GET https://staging-api.ondi.io/functions/v1/audit-logs/{{audit_log_id}}?tenant_id=

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |
| Content-Type | header string |  |  | application/json |



### Path parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| audit_log_id | path string | Yes |  | {{audit_log_id}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| tenant_id | query string |  | UUID of the tenant (only honored for system admins; otherwise inferred from auth context) |  |



### Response 200 Success Response



```json
{
  "success": true,
  "message": "Audit Logs Retrieved Successfully",
  "audit_log": {
    "id": "audit-log-uuid",
    "response_time_ms": 842,
    "time_distribution": {
      "middleware_auth_ms": 184.25,
      "delivery_main_query_call_ms": 113.47,
      "handler_ms": 842.19,
      "server_processing_ms": 1044.72
    }
  }
}
```

## POST /upload-file — Upload File to Storage

[Open endpoint](https://docs-ondi.web.app/api-docs/reference/platform-access/file-upload#post-upload-file-to-storage)

Uploads a file to Supabase Storage and returns information about the uploaded file including its public URL.

Path within the bucket (subfolder)

### Business Rules
- Content-Type must be 'multipart/form-data'
- The file field is required and must be a valid file
- The bucket must exist in Supabase Storage
- Filename is automatically generated with timestamp prefix to avoid conflicts

Authentication: Send a bearer token in the Authorization header for an authenticated OnDi user session.

Request: POST https://staging-api.ondi.io/functions/v1/upload-file?language=en

### Headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Authorization | header string |  |  | Bearer {{access_token}} |



### Query parameters

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| language | query string | No | Language code for response localization (Optional, default: en) Language code for localized response messages (optional, default: en) | en |



### Request body fields

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| file | form file | Yes | The file to upload | /path/to/file.jpg |
| bucket | form text | Yes | Storage bucket name (defaults to 'public') | public |
| path | form text |  | Path within the bucket (subfolder) | uploads |



### Request body (formdata)

```json
{
  "file": "/path/to/file.jpg",
  "bucket": "public",
  "path": "uploads"
}
```

### Response 200 Success Response

OK

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": true,
  "message": "File uploaded successfully",
  "data": {
    "path": "uploads/1625097600000-example.jpg",
    "fullPath": "public/uploads/1625097600000-example.jpg",
    "publicUrl": "https://example.supabase.co/storage/v1/object/public/public/uploads/1625097600000-example.jpg",
    "fileName": "1625097600000-example.jpg",
    "originalName": "example.jpg",
    "contentType": "image/jpeg",
    "size": 102400
  }
}
```

### Response 400 Error - Invalid Content Type

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Invalid content type"
}
```

### Response 400 Error - Missing File

Bad Request

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "File is required"
}
```

### Response 500 Error - Upload Failed

Internal Server Error

### Response headers

| Name | Type | Required | Description | Example |
| --- | --- | --- | --- | --- |
| Content-Type | header string |  |  | application/json |



```json
{
  "success": false,
  "message": "Upload failed",
  "error": "Bucket non-existent-bucket does not exist"
}
```

### Response 200 Success (200 OK)



```json
{
  "success": true,
  "message": "File uploaded successfully",
  "data": {
    "path": "uploads/1625097600000-example.jpg",
    "fullPath": "public/uploads/1625097600000-example.jpg",
    "publicUrl": "https://[project-ref].supabase.co/storage/v1/object/public/public/uploads/1625097600000-example.jpg",
    "fileName": "1625097600000-example.jpg",
    "originalName": "example.jpg",
    "contentType": "image/jpeg",
    "size": 102400
  }
}
```

### Response 400 Validation Error (400 Bad Request)



```json
{
  "success": false,
  "message": "Field required: bucket"
}
```

### Response 500 Upload Failed (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Upload failed",
  "error": "Error message from storage service"
}
```

### Response 500 Server Error (500 Internal Server Error)



```json
{
  "success": false,
  "message": "Internal server error",
  "error": "Error details"
}
```
