Wallets
4 API calls in this section.
Endpoints for managing user wallet operations including topup, transfers, and transaction history.
Manual Customer Wallet Transaction
/users/:userId/wallet/manual-transactionCreates 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.
Path parameters
| Parameter | Type | Description |
|---|---|---|
userIdRequired | string | Customer user UUID, not Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
tenant_id | string | Selected tenant; authentication must resolve this tenant. Example: |
language | string | Response language: en, ar, fa, or ku. Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Idempotency-KeyRequired | string | Required UUID generated once per confirmed adjustment; reuse for retries. Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
type | string | — |
amount | number | — |
reason | string | — |
reference | string | — |
notes | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/users/${USERID}/wallet/manual-transaction?tenant_id=${TENANT_ID}&language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --header "Idempotency-Key: ${WALLET_ADJUSTMENT_KEY}" \5 --data-raw '{6 "type": "credit",7 "amount": 10000,8 "reason": "Customer cash payment received",9 "reference": "CASH-2026-001",10 "notes": "Received at the front desk."11}'Response examples
1{2 "success": true,3 "message": "Customer wallet transaction saved successfully.",4 "transaction": {5 "transaction_id": "550e8400-e29b-41d4-a716-446655440000",6 "new_balance": 15000,7 "amount": 10000,8 "type": "credit",9 "reason": "Customer cash payment received",10 "created_at": "2026-10-14T12:00:00Z",11 "actor": {12 "id": "550e8400-e29b-41d4-a716-446655440001",13 "name": "Tenant Admin"14 },15 "replayed": false,16 "reference": "CASH-2026-001",17 "notes": "Received at the front desk."18 }19}Top-up Wallet
/users/:userId/wallet/topupTop-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
Path parameters
| Parameter | Type | Description |
|---|---|---|
userIdRequired | string | UUID of the user whose wallet to top up Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
languageOptional | string | Language code for response localization (Optional, default: en) Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
amount | number | — |
currency | string | — |
description | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/users/${USERID}/wallet/topup?language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "amount": 100,6 "currency": "USD",7 "description": "Adding funds to wallet"8}'Other request examples (1)
1{2 "amount": 100.00, // required - Amount to add (min: 0.01)3 "currency": "USD", // optional - Currency code (default: USD)4 "description": "Adding funds to wallet" // optional - Description5}Response examples
1{2 "success": true,3 "wallet": {4 "transaction_id": "550e8400-e29b-41d4-a716-446655440000",5 "new_balance": 1000,6 "amount": 100,7 "currency": "USD",8 "timestamp": "2023-01-01T00:00:00Z"9 }10}Transfer Funds
/users/:userId/wallet/transferTransfer 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_idBalance validation
Atomic transaction
Tenant isolation
Transfer history tracking
Path parameters
| Parameter | Type | Description |
|---|---|---|
userIdRequired | string | UUID of the sender user Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
languageOptional | string | Language code for response localization (Optional, default: en) Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
Content-Type | string | Example: |
Body parameters
| Parameter | Type | Description |
|---|---|---|
amount | number | — |
recipient_identifier | string | — |
identifier_type | string | — |
description | string | — |
1curl --globoff --request POST "https://staging-api.ondi.io/functions/v1/users/${USERID}/wallet/transfer?language=en" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}" \3 --header "Content-Type: application/json" \4 --data-raw '{5 "amount": 50,6 "recipient_identifier": "recipient@example.com",7 "identifier_type": "email",8 "description": "Payment for services"9}'Response examples
1{2 "success": true,3 "transfer": {4 "transfer_id": "550e8400-e29b-41d4-a716-446655440000",5 "new_balance": 950,6 "amount": 50,7 "currency": "USD",8 "timestamp": "2023-01-01T00:00:00Z"9 }10}List Wallet Transactions
/users/:userId/wallet/transactionsList 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:tenantormanage:walletscan access wallets of users belonging to their authenticated tenant
Endpoint
GET /users/{userId}/wallet/transactions
Summary Rules
summary.total_creditedis the sum of positivewallet_transactions.amountvalues over the full filtered result set.summary.total_debitedis the sum of absolute negativewallet_transactions.amountvalues over the full filtered result set.Summary totals respect
type,search,from_date,to_date,source, andactor_name, but neverpageorlimit.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.
Path parameters
| Parameter | Type | Description |
|---|---|---|
userIdRequired | string | UUID of the user whose transactions to retrieve Example: |
Query parameters
| Parameter | Type | Description |
|---|---|---|
sourceOptional | string | Exact, case-sensitive saved source value selected from |
actor_nameOptional | string | 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 |
pageOptional | string | Page number for pagination (default: 1) (number, optional): Page number (default: 1) Example: |
limitOptional | string | Number of items per page (default: 20, max: 100) (number, optional): Items per page (default: 20, max: 100) Example: |
typeOptional | string | Filter by transaction type: topup, transfer, payment, earning (string, optional): Filter by type (topup, transfer, payment, earning) |
searchOptional | string | 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_dateOptional | string | Filter from date (ISO format: 2024-01-01T00:00:00Z) (string, optional): Filter from date (ISO format) |
to_dateOptional | string | Filter to date (ISO format: 2024-12-31T23:59:59Z) (string, optional): Filter to date (ISO format) |
languageOptional | string | Language code for response localization (Optional, default: en) Example: |
Headers
| Parameter | Type | Description |
|---|---|---|
Authorization | string | Example: |
1curl --globoff --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" \2 --header "Authorization: Bearer ${ONDI_ACCESS_TOKEN}"Response examples
1{2 "success": true,3 "message": "Wallet transactions retrieved successfully",4 "wallet": {5 "id": "550e8400-e29b-41d4-a716-446655440000",6 "balance": 15000,7 "reserved": 08 },9 "summary": {10 "current_balance": 15000,11 "total_credited": 10000,12 "total_debited": 013 },14 "transactions": [15 {16 "id": "550e8400-e29b-41d4-a716-446655440001",17 "type": "earning",18 "amount": 10000,19 "description": "Manual credit: Customer payment - Received at the front desk.",20 "reference_id": null,21 "reference_type": null,22 "created_at": "2026-10-14T12:00:00Z",23 "delivery": null,24 "source": "customer_manual_adjustment",25 "recorded_by": {26 "id": "550e8400-e29b-41d4-a716-446655440002",27 "name": "Amit"28 },29 "manual_transaction": {30 "type": "credit",31 "reason": "Customer payment",32 "actor": {33 "id": "550e8400-e29b-41d4-a716-446655440002",34 "name": "Amit"35 },36 "reference": "CASH-2026-001",37 "notes": "Received at the front desk."38 }39 }40 ],41 "page": 1,42 "limit": 20,43 "total": 1,44 "source_options": [45 "customer_manual_adjustment",46 "delivery_fee_refund",47 "wallet_delivery_fee"48 ]49}