# Piaxis External API Integration Guide

This is the AI-friendly companion to the public external payment docs at `https://api.gopiaxis.com/api/docs/`.

It is intentionally scoped to the public `/api` payment surface documented there:

- authentication
- OTP
- direct payments
- escrows
- direct disbursements
- escrow disbursements
- webhook integration
- official Python and TypeScript SDKs

This file is written for engineers and AI agents that need one canonical text reference before building an integration.

## Machine-Friendly Retrieval

Use any of these stable URLs:

- Primary markdown: `https://api.gopiaxis.com/api/docs/piaxis-api-integration-guide.md`
- Short markdown alias: `https://api.gopiaxis.com/api/docs/index.md`
- Alternate markdown alias: `https://api.gopiaxis.com/api/docs/api-reference.md`
- AI discovery file: `https://api.gopiaxis.com/api/docs/llms.txt`
- HTML docs root: `https://api.gopiaxis.com/api/docs/`

## Scope And Source Of Truth

There are two layers of public docs today:

1. Long-form HTML guides under `/api/docs`
2. The live FastAPI route and schema surface in `paymentAPI/views.py` and `paymentAPI/schemas.py`

This guide is designed to be integration-safe, so it does two things:

- preserves the public `/api/docs` scope
- reconciles that scope with the currently exposed public routes and typed request/response models

If the HTML guides show a richer example than the live public schema currently accepts, this guide says so explicitly instead of pretending the extra fields are guaranteed.

## Public Base URL

All public external payment endpoints are served under:

```text
https://api.gopiaxis.com/api
```

## SDKs

### Python SDK

- Package name: `piaxis-sdk`
- Import namespace: `piaxis_sdk`
- Package page: `https://pypi.org/project/piaxis-sdk/`
- Repository: `https://github.com/piaxepay/python-sdk`

Install:

```bash
pip install piaxis-sdk
```

Important naming detail:

- install with `piaxis-sdk`
- import from `piaxis_sdk`

### TypeScript SDK

- Package name: `@piaxis/sdk`
- Package page: `https://www.npmjs.com/package/@piaxis/sdk`
- Repository: `https://github.com/piaxepay/typescript-sdk`

Install:

```bash
npm install @piaxis/sdk
```

### SDK vs REST

Use the SDK first when:

- you are integrating from Python or TypeScript
- you want the fastest path to common flows
- you want less boilerplate around auth and request handling

Use raw REST when:

- you are integrating from another language
- you need a public field that is documented but not yet wrapped in the SDK
- you need complete control over retries, idempotency, or webhook handling

## The Most Important Naming Differences

These are the differences integrators trip over most often:

| Flow                | Field name for currency | Field name for the receiving Piaxis account                        |
| ------------------- | ----------------------- | ------------------------------------------------------------------ |
| Direct payment      | `currency`              | authenticated merchant account from `api-key`                      |
| Escrow              | `currency_code`         | authenticated merchant account by default (`receiver_id` optional) |
| Direct disbursement | `currency`              | `recipients[].recipient_id`                                        |
| Escrow disbursement | `currency`              | `recipients[].recipient_id`                                        |

This means:

- direct payment does **not** use `currency_code`
- escrow does **not** use `recipient_id`
- escrow still uses the `receiver_id` field name when sent, but new integrations can omit it
- disbursements put the target identity inside each recipient object

One more important semantic difference:

- merchant direct payment credits the authenticated merchant collector by default
- public escrow collections also default `receiver_id` to that same authenticated merchant account

## Route Reality Check

Some long-form HTML examples under `/api/docs` still show older or broader conceptual paths. For live integration against `https://api.gopiaxis.com/api`, prefer this reconciliation table.

| Topic                          | HTML docs may show                                 | Live public route to prefer                                              |
| ------------------------------ | -------------------------------------------------- | ------------------------------------------------------------------------ |
| OAuth token exchange           | `/api/oauth/token`                                 | `POST /api/token`                                                        |
| OAuth refresh                  | `/api/oauth/refresh`                               | No public `paymentAPI` refresh route is currently exposed                |
| OTP request                    | `/api/auth/request-otp`                            | `POST /api/request-otp`                                                  |
| OTP verify                     | `/api/auth/verify-otp`                             | No public standalone verify route; escrow OTP is sent in `user_info.otp` |
| Payment list                   | `GET /api/payments`                                | `GET /api/merchant-payments`                                             |
| Refunds                        | `POST /api/payments/{id}/refund`                   | Not currently exposed by the public `paymentAPI` router                  |
| Webhook endpoint configuration | `POST /api/webhooks/endpoints`                     | Not currently exposed by the public `paymentAPI` router                  |
| Escrow detail                  | `/api/escrows/{id}` and `/api/escrows/{id}/status` | Both exist and return different detail shapes                            |

## Authentication

### Merchant API Key

Use for merchant-controlled server-to-server operations.

Common header:

```text
api-key: YOUR_MERCHANT_API_KEY
```

Binding model:

- one `api-key` resolves to one merchant profile and one merchant account
- that merchant account id is the merchant's own Piaxis account id
- `merchant_id`, `receiver_id`, and `recipient_id` still refer to Piaxis `Account.id` values whenever those fields appear
- public collection routes derive the merchant collector from the authenticated `api-key`
- do not send `MerchantProfile.id` or `UserProfile.id` in those fields
- the public payment API does not currently expose a dedicated merchant `whoami` route for API-key callers because standard collections do not need one

Typical use cases:

- direct payment creation
- escrow creation for merchant-initiated flows
- disbursements
- escrow disbursements
- merchant-side escrow actions

### OAuth2 Bearer Token

Use when acting on behalf of a registered user, especially for `piaxis_external` flows.

Common header:

```text
Authorization: Bearer YOUR_ACCESS_TOKEN
```

### OTP

OTP is used in the public docs for unregistered or sensitive flows.

Important distinction:

- the public OTP request route is `POST /api/request-otp`
- escrow money-affecting operations for unregistered external payers pass the OTP inside `user_info.otp`
- `POST /api/payments/create` also accepts an optional `mfa_code` parameter at the route level

## Canonical Flows

### Canonical Mobile-Money Escrow Flow

For unregistered external payer escrow:

1. `POST /api/request-otp`
2. user receives OTP
3. `POST /api/escrows/` with `user_info.email`, `user_info.phone_number`, and `user_info.otp`
4. later escrow actions such as fulfill, release, reverse, or dispute use matching `user_info` again when the payer is unregistered

### Canonical Direct Payment Flow

For merchant server-to-server direct payments:

1. `POST /api/payments/create`
2. persist the returned `payment_id`
3. poll `GET /api/payments/{payment_id}` or consume webhooks
4. optionally list historical transactions with `GET /api/merchant-payments`

Important:

- on this merchant route, the authenticated merchant is the credited collector
- omitting `recipient_id` still creates a valid merchant collection
- if `recipient_id` is sent, it must match the authenticated merchant `Account.id`
- the direct-payment route does not use `recipient_id` to choose a different collector or payout target

### Canonical `piaxis_external` Flow

1. redirect the user through `GET /api/authorize`
2. exchange the returned code through `POST /api/token`
3. send `Authorization: Bearer ...` on `piaxis_external` calls

### Canonical Marketplace Flow

For a platform such as JetsLab that serves many businesses:

1. keep your own mapping table from platform business id to the merchant account that should collect
2. for direct collection, authenticate as that merchant and let that merchant collect into its own Piaxis wallet
3. for escrow collection, authenticate as that merchant and let the public escrow route default `receiver_id` to that same merchant account
4. after fulfillment or release, create a disbursement for the final beneficiary
5. if the beneficiary has a Piaxis account, send that account id as `recipients[].recipient_id`
6. if the beneficiary is not a Piaxis user, omit `recipient_id` and send the external payout phone number with the chosen payout `payment_method`
7. never let end-user input choose raw Piaxis recipient ids directly

Example mapping table:

| Your platform business id    | Piaxis account id                      | Use in API                                    |
| ---------------------------- | -------------------------------------- | --------------------------------------------- |
| `biz_jetslab_restaurant_001` | `096b723a-45c5-4957-94d7-747835136265` | authenticated merchant account for collection |
| `biz_jetslab_store_002`      | `8d1d6c7e-6ad6-40a3-9714-d1e6ab63e3aa` | `recipients[].recipient_id` for payouts       |

### How To Choose The Right Flow

- Direct collection: the authenticated merchant collects immediately and there is no later escrow fulfillment step.
- Escrow collection: the authenticated merchant still collects, but release waits for payer and merchant actions such as delivery confirmation or meeting verification.
- Direct disbursement: the merchant pays a beneficiary immediately from the collected balance.
- Escrow disbursement: the merchant creates payout escrows and the payout beneficiary becomes the escrow receiver that must satisfy terms before payout.

Concrete marketplace rule:

- if your rule is `collect from buyer B now, pay user C later`, keep the collection on `POST /api/payments/create` or `POST /api/escrows/`, let the merchant collect first, and then create a direct disbursement or escrow disbursement for user C

## Required And Recommended Headers

Required on most JSON requests:

```text
Content-Type: application/json
api-key: YOUR_MERCHANT_API_KEY
```

Required on merchant-secured escrow, merchant-payments, direct-disbursement, and
escrow-disbursement calls:

```text
X-piaxis-Client-ID: YOUR_MERCHANT_CLIENT_ID
```

Store the same value in your server environment as `PIAXIS_CLIENT_ID`.
If you omit it on those merchant-secured routes, the API returns `MISSING_CLIENT_ID`.

Recommended on all writes:

```text
X-Idempotency-Key: unique_request_id
X-Request-ID: your_trace_id
User-Agent: YourApp/1.0
```

## Endpoint Reference

This section describes the live public route surface and the guaranteed request/response shapes.

## 1. Authorization

### `GET /api/authorize`

Purpose:

- start the OAuth authorization flow

Query parameters used by the public route:

- `merchant_id`
- `external_user_id`
- `redirect_uri`

Outcome:

- on normal browser use, redirects to your callback URL with `?code=...`
- on test requests, the route can return a JSON helper payload describing the next token exchange step

### `POST /api/token`

Purpose:

- exchange an authorization code for access and refresh tokens

Parameters:

- `grant_type`
- `code`
- `redirect_uri`
- `client_id`
- `client_secret`

Guaranteed response fields:

- `access_token`
- `token_type`
- `expires_in`
- `refresh_token`
- `user_id`
- `merchant_id`
- optional `external_user_id`
- optional extra claims such as `scope`

Important:

- the canonical live route is `/api/token`
- some HTML examples still show `/api/oauth/token`

Connected-wallet mapping:

- the token response includes `user_id` for the connected Piaxis account
- persist that `user_id` if you want to pay that beneficiary later into a Piaxis wallet
- use that stored value as `recipients[].recipient_id` on direct disbursements

## 2. OTP

### `POST /api/request-otp`

Purpose:

- request an OTP for public external flows

Request body:

```json
{
  "email": "user@example.com",
  "phone_number": "+256700000000"
}
```

Live request model:

- `email`: optional
- `phone_number`: optional E.164 format
- at least one should be supplied in practical use

Guaranteed response fields from the current implementation:

```json
{
  "verification_methods": {
    "email_sent": true,
    "phone_sent": false
  },
  "expires_in": 300,
  "otp": null
}
```

Notes:

- `otp` is only returned in development environments
- some HTML docs show a separate `/api/auth/verify-otp` flow, but that is not currently exposed in the public `paymentAPI` router

## 3. Direct Payments

### `POST /api/payments/create`

Purpose:

- create a direct payment

Guaranteed request body fields from the live schema:

```json
{
  "amount": "50000.00",
  "currency": "UGX",
  "payment_method": "mtn",
  "user_info": {
    "email": "customer@example.com",
    "phone_number": "+256700000000"
  },
  "products": [],
  "customer_pays_fees": true
}
```

Route-level optional parameter:

- `mfa_code`

Guaranteed response model:

```json
{
  "payment_id": "uuid",
  "status": "pending",
  "amount": "50,000.00",
  "currency": "UGX"
}
```

Collection semantics on this route:

- the authenticated merchant is the credited collector
- new integrations should omit `recipient_id`
- if sent, it must match the authenticated merchant `Account.id`
- if you need to send money onward to a seller, driver, or service provider, use a later disbursement after collection or escrow release

Important mismatch to call out clearly:

- the HTML docs show additional example fields such as `reference`, `description`, `metadata`, `callback_url`, `send_sms`, `send_email`, and `card_info`
- those fields are **not** currently present in the live `ApiPaymentCreate` schema
- do not assume they are guaranteed to be accepted unless the public route schema is expanded

### `GET /api/payments/{payment_id}`

Purpose:

- fetch payment details

Guaranteed response fields:

- `id`
- `status`
- `amount`
- `currency`
- `payment_method`
- `created_at`
- `reference`
- `receipt`
- `merchant_details`
- `recipient_details`
- `product_details`
- `chain_payment_details`
- `transaction_details`

### `GET /api/merchant-payments`

Purpose:

- list merchant payments with filters

Query parameters:

- `status`
- `payment_method`
- `from_date`
- `to_date`
- `limit`
- `offset`

Guaranteed response shape:

```json
{
  "total": 1,
  "offset": 0,
  "limit": 50,
  "results": [
    {
      "payment_id": "uuid",
      "status": "completed",
      "amount": "50,000.00",
      "currency": "UGX",
      "payment_method": "mtn",
      "date": "2026-01-01T00:00:00+00:00",
      "payer": {
        "id": "uuid",
        "email": "payer@example.com",
        "type": "registered"
      },
      "recipient": {
        "id": "uuid",
        "email": "recipient@example.com"
      }
    }
  ]
}
```

Important:

- some HTML docs show `GET /api/payments`
- the current public route is `GET /api/merchant-payments`

## 4. Escrows

### `POST /api/escrows/`

Purpose:

- create an escrow with one or more terms

Guaranteed request body fields from the live schema:

```json
{
  "amount": "50000.00",
  "currency_code": "UGX",
  "payment_method": "mtn",
  "terms": [
    {
      "type": "delivery_confirmation",
      "data": {
        "description": "Confirm delivery"
      }
    }
  ],
  "external_user_id": "required-for-piaxis_external",
  "user_info": {
    "email": "buyer@example.com",
    "phone_number": "+256700000000",
    "otp": "123456"
  },
  "user_location": {
    "latitude": 0.3476,
    "longitude": 32.5825
  }
}
```

Guaranteed response fields:

- `id`
- `amount`
- `currency_code`
- `status`
- `payment_method`
- `sender_id`
- `receiver_id`
- `created_at`
- `updated_at`
- `terms`
- `products`
- `location`
- `required_actions`
- `external_user_id`

Receiver requirement:

- public escrow collections default `receiver_id` to the authenticated merchant account
- if `receiver_id` is sent, it must match the authenticated merchant `Account.id`
- do not use this public collection route to point escrow release at a different merchant or beneficiary account
- if you later need to pay an internal or external beneficiary, release into the merchant wallet first and then use disbursements

Business roles on this public collection route:

- payer or customer = escrow sender
- authenticated merchant collector = escrow receiver
- if you want a downstream seller, driver, or beneficiary to become the protected receiver instead, use escrow disbursements rather than trying to route collection escrow directly to that user

Important mismatch:

- the HTML escrow guide shows fields like `description` and `products`
- the live `ApiEscrowCreate` schema currently guarantees only the fields shown above
- extra fields may be ignored unless the public schema is expanded

### `GET /api/escrows/{escrow_id}`

Purpose:

- fetch escrow details using the `ApiEscrowResponse` shape

Use when you want:

- the normalized escrow response model
- terms
- required actions
- mapped external user id if present

### `GET /api/escrows/{escrow_id}/status`

Purpose:

- fetch a status-focused escrow view

Guaranteed response shape includes:

- `escrow_id`
- `status`
- `amount`
- `currency`
- `payment_method`
- `created_at`
- `buyer_info`
- `terms`
- `all_terms_met`

This route is useful when you want fulfillment state and buyer-facing identity information.

### `POST /api/escrows/{escrow_id}/release`

Request body:

```json
{
  "force": false,
  "reason": "optional reason",
  "user_info": {
    "email": "buyer@example.com",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}
```

Guaranteed response:

```json
{
  "status": "released",
  "escrow_id": "uuid",
  "force": false,
  "reason": "optional reason"
}
```

### `POST /api/escrows/{escrow_id}/reverse`

Request body:

```json
{
  "reason": "Product not as described",
  "user_info": {
    "email": "buyer@example.com",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}
```

Guaranteed response:

```json
{
  "status": "reversed",
  "escrow_id": "uuid",
  "reason": "Product not as described"
}
```

### `POST /api/escrows/{escrow_id}/disputes`

Request body:

```json
{
  "reason": "Goods damaged on delivery",
  "initiator_role": "sender",
  "user_info": {
    "email": "buyer@example.com",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}
```

Guaranteed response:

```json
{
  "message": "Dispute initiated successfully",
  "dispute_id": "uuid",
  "initiator_role": "sender"
}
```

### `POST /api/escrows/{escrow_id}/terms/{term_id}/fulfill`

Guaranteed request shape:

```json
{
  "term_id": "uuid",
  "term_type": "delivery_confirmation",
  "data": {},
  "user_info": {
    "email": "buyer@example.com",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}
```

Important: the live schema keeps `data` generic, but the handler branches by `term_type`, so the keys inside `data` are term-specific.

Collection-escrow actor rules on this route:

- buyer-side confirmations must come from the original payer
- seller-side confirmations must come from the authenticated merchant whose account matches `receiver_id`
- use `GET /api/escrows/{escrow_id}` and inspect `required_actions` before rendering a fulfill UI

Common `required_actions` values returned by the live escrow response include:

- `delivery_confirmation_required` with `confirmation_method`
- `delivery_meeting_required` with `max_distance_km`
- `agreement_required` with `agreement_options` and current status
- `proof_required` with `proof_type` and optional `deadline`

Delivery confirmation payloads:

- send `confirmation_method` plus either `buyer_device_info` or `seller_device_info`
- a generic `confirmed: true` flag is not enough for this term type
- use `user_info` for an unregistered payer; use bearer auth for registered `piaxis` or `piaxis_external` payers; use the authenticated merchant API key for seller-side confirmation

Buyer confirmation example:

```json
{
  "term_id": "uuid",
  "term_type": "delivery_confirmation",
  "data": {
    "confirmation_method": "signature",
    "buyer_device_info": {
      "device_id": "buyer-device-001",
      "platform": "android",
      "app_version": "1.0.0"
    }
  },
  "user_info": {
    "email": "buyer@example.com",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}
```

Seller confirmation example:

```json
{
  "term_id": "uuid",
  "term_type": "delivery_confirmation",
  "data": {
    "confirmation_method": "signature",
    "seller_device_info": {
      "device_id": "merchant-ops-01",
      "platform": "web"
    }
  }
}
```

Meeting-delivery payloads:

- buyer confirmation sends `buyer_latitude` and `buyer_longitude`
- seller confirmation sends `seller_latitude` and `seller_longitude`
- both sides may include `device_info`
- the route records caller IP automatically and only completes the term after both sides submit coordinates within the allowed proximity

Buyer meeting example:

```json
{
  "term_id": "uuid",
  "term_type": "meeting_delivery",
  "data": {
    "buyer_latitude": 0.3476,
    "buyer_longitude": 32.5825,
    "device_info": {
      "device_id": "buyer-phone-01",
      "platform": "android"
    }
  },
  "user_info": {
    "email": "buyer@example.com",
    "phone_number": "+256700000000",
    "otp": "123456"
  }
}
```

Seller meeting example:

```json
{
  "term_id": "uuid",
  "term_type": "meeting_delivery",
  "data": {
    "seller_latitude": 0.3477,
    "seller_longitude": 32.5826,
    "device_info": {
      "platform": "web"
    }
  }
}
```

Other supported `term_type` payloads on the live route:

- `agreement`: send `agreement_type` as `release`, `reverse`, or `split`; when using `split`, also send `split_percentage`; this flow currently requires a registered account actor
- `location`: send `latitude` and `longitude`
- `meeting`: send `latitude` and `longitude`
- `password`: send `password`
- `proof`: submit `files` first, then later send `verify: true` to approve the proof
- `custom`: send the rule-specific `data`
- `return_period`: send `return_requested`

Response notes:

- the response varies by term type
- meeting and delivery confirmation return specialized success payloads with `requires_other_party`
- other term types may return:
  - `{"status":"pending","message":"Term fulfilled, waiting for other conditions"}`
  - `{"status":"success","message":"All terms met and funds released"}`
  - or a fulfillment summary with `term_id` and `term_type`

## 5. Direct Disbursements

### `POST /api/disbursements`

Guaranteed request shape:

```json
{
  "recipients": [
    {
      "recipient_id": "optional-uuid",
      "email": "recipient@example.com",
      "phone_number": "+256700123456",
      "amount": "1500000.00",
      "reference": "SALARY-JUNE-001"
    }
  ],
  "currency": "UGX",
  "payment_method": "mtn",
  "description": "June salary payments"
}
```

Recommended sequence:

- call `POST /api/disbursements/quote` first
- confirm `total_fee_amount` and `total_debit_amount`
- then submit `POST /api/disbursements`

Recipient resolution rules:

- if `recipients[].recipient_id` is present, that item is treated as an internal Piaxis transfer
- if `recipients[].recipient_id` is omitted, that item is treated as an external payout through the batch `payment_method`
- when present, `recipients[].recipient_id` must be a Piaxis `Account.id`, not a profile id
- for MTN and Airtel external payouts, `phone_number` is the routing field that matters
- `email` is optional metadata and should not be treated as a substitute for the mobile-money destination number

When `recipient_id` is needed:

- use it only when the beneficiary should receive funds into a Piaxis wallet
- a common way to obtain it is the public OAuth flow:
  1. send the beneficiary through `GET /api/authorize`
  2. exchange the code through `POST /api/token`
  3. persist the returned `user_id`
  4. use that stored `user_id` as `recipients[].recipient_id`
- if the beneficiary should receive on MTN or Airtel mobile money, omit `recipient_id` and provide the payout phone number instead

Batch payment-method rules:

- `payment_method` is a top-level batch field
- all external recipients in the batch use that same payout rail
- if you need some recipients paid through MTN and others through Airtel, create separate disbursement batches
- internal `recipient_id` items can coexist in the same batch because they do not use the external payout rail

Guaranteed response fields:

- `disbursement_id`
- `status`
- `total_amount`
- `total_fee_amount`
- `total_debit_amount`
- `internal_total_amount`
- `external_total_amount`
- `currency`
- `recipient_count`
- `successful_count`
- `failed_count`
- `pending_count`

Important mismatch:

- the HTML guide shows `notify_recipients`, `callback_url`, and top-level `reference`
- those are not currently present in the live `DisbursementCreate` schema

### `POST /api/disbursements/quote`

Purpose:

- preview recipient-level payout fees and the merchant wallet debit before submitting the batch

Guaranteed response fields:

- `currency`
- `payment_method`
- `recipient_count`
- `internal_recipient_count`
- `external_recipient_count`
- `total_amount`
- `internal_total_amount`
- `external_total_amount`
- `total_fee_amount`
- `total_debit_amount`
- `items`

Each quote item includes:

- `recipient_id`
- `email`
- `phone_number`
- `amount`
- `payout_type`
- `fee_amount`
- `total_debit_amount`
- `reference`
- `fee_breakdown`

Default external payout fees:

- up to `100000 UGX`: `500 UGX`
- above `100000 UGX` up to `1000000 UGX`: `1000 UGX`
- above `1000000 UGX`: `1500 UGX`

These are fallback defaults. Admin-configured disbursement fee rules can override them.

Merchant balance model:

- collections and released escrows credit the merchant or platform wallet
- direct disbursements debit that same merchant-funded balance
- there is no separate payout wallet required for the public payout flow

Marketplace example: collect -> release -> payout

Collect to merchant or platform:

```json
{
  "amount": "50000.00",
  "currency": "UGX",
  "payment_method": "mtn",
  "user_info": {
    "email": "buyer@example.com",
    "phone_number": "+256700000000"
  }
}
```

If you use escrow instead of direct collection, the public route defaults `receiver_id` to the authenticated merchant or platform account. Release there, then pay out.

Disburse the released or settled balance to a non-Piaxis payee phone number:

```json
{
  "recipients": [
    {
      "phone_number": "+256701234567",
      "amount": "45000.00",
      "reference": "ORDER-1001-NET-PAYOUT"
    }
  ],
  "currency": "UGX",
  "payment_method": "airtel",
  "description": "Net payout after platform fee"
}
```

If the beneficiary is a registered Piaxis user instead:

```json
{
  "recipients": [
    {
      "recipient_id": "096b723a-45c5-4957-94d7-747835136265",
      "amount": "45000.00",
      "reference": "ORDER-1001-WALLET-PAYOUT"
    }
  ],
  "currency": "UGX",
  "payment_method": "airtel",
  "description": "Internal wallet payout for connected beneficiary"
}
```

In that case, `recipient_id` is needed because the payee is receiving into a Piaxis wallet rather than external mobile money.

### `GET /api/disbursements/{disbursement_id}`

Guaranteed response fields:

- `disbursement_id`
- `status`
- `total_amount`
- `total_fee_amount`
- `total_debit_amount`
- `internal_total_amount`
- `external_total_amount`
- `currency`
- `payment_method`
- `description`
- `created_at`
- `completed_at`
- `cancelled_at`
- `cancellation_reason`
- `items`

### `GET /api/disbursements`

Query parameters:

- `status`
- `from_date`
- `to_date`
- `limit`
- `offset`

Guaranteed response:

```json
{
  "total": 1,
  "offset": 0,
  "limit": 50,
  "results": [
    {
      "disbursement_id": "uuid",
      "status": "processing",
      "total_amount": "2700000.00",
      "total_fee_amount": "2500.00",
      "total_debit_amount": "2702500.00",
      "internal_total_amount": "0.00",
      "external_total_amount": "2700000.00",
      "currency": "UGX",
      "recipient_count": 2,
      "successful_count": 0,
      "failed_count": 0,
      "pending_count": 2
    }
  ]
}
```

### `POST /api/disbursements/{disbursement_id}/cancel`

Request body:

```json
{
  "reason": "Incorrect recipient information"
}
```

Guaranteed response:

- same top-level fields as `DisbursementResponse`

## 6. Escrow Disbursements

### `POST /api/escrow-disbursements`

Guaranteed request shape:

```json
{
  "recipients": [
    {
      "recipient_id": "optional-uuid",
      "email": "driver@delivery.com",
      "phone_number": "+256700111000",
      "amount": "25000.00",
      "terms": [
        {
          "type": "location_verification",
          "data": {
            "latitude": 0.347596,
            "longitude": 32.58252,
            "radius": 100
          }
        }
      ],
      "reference": "DELIVERY-001"
    }
  ],
  "currency": "UGX",
  "payment_method": "mtn",
  "description": "Delivery batch",
  "user_location": {
    "latitude": 0.3476,
    "longitude": 32.5825
  }
}
```

Guaranteed response fields:

- `disbursement_id`
- `status`
- `total_amount`
- `currency`
- `recipient_count`
- `successful_count`
- `failed_count`
- `pending_count`
- `escrow_items`
- `description`
- `created_at`

Business role difference:

- each escrow item is created with the merchant or platform as sender
- the payout beneficiary becomes the escrow receiver
- use escrow disbursement when the beneficiary should become the protected payout recipient that must satisfy terms before release
- if your requirement is still `collect now, pay later`, use collection or collection escrow first and create the payout escrow afterward

Important mismatch:

- the HTML guide shows top-level `auto_release` and `metadata`
- those are not currently present in the live `EscrowDisbursementCreate` schema

### `GET /api/escrow-disbursements/{disbursement_id}`

Guaranteed response:

- same top-level fields as create response
- `escrow_items[]` includes:
  - `recipient_id`
  - `email`
  - `phone_number`
  - `amount`
  - `escrow_id`
  - `status`
  - `terms_count`
  - `reference`

### `GET /api/escrow-disbursements`

Query parameters:

- `limit`
- `offset`
- `status`

Guaranteed response:

```json
{
  "total": 1,
  "offset": 0,
  "limit": 20,
  "results": [
    {
      "disbursement_id": "uuid",
      "status": "processing",
      "total_amount": "25000.00",
      "currency": "UGX",
      "recipient_count": 1,
      "successful_count": 0,
      "failed_count": 0,
      "pending_count": 1,
      "description": "Delivery batch",
      "created_at": "2026-01-01T00:00:00+00:00"
    }
  ]
}
```

### `POST /api/escrow-disbursements/{disbursement_id}/release`

Request body:

```json
{
  "force": true,
  "reason": "Batch force release approved",
  "escrow_ids": ["uuid"]
}
```

Guaranteed response fields:

- `disbursement_id`
- `force`
- `reason`
- `released_count`
- `skipped_count`
- `failed_count`
- `released`
- `skipped`
- `failed`

### `POST /api/escrow-disbursements/{disbursement_id}/cancel`

Request body:

```json
{
  "reason": "Batch entered incorrectly"
}
```

Guaranteed response fields:

- `disbursement_id`
- `status`
- `total_amount`
- `currency`
- `recipient_count`
- `successful_count`
- `failed_count`
- `pending_count`
- `cancelled_at`
- `cancellation_reason`

## 7. Webhooks

The docs cover two related webhook surfaces:

- public `/api` webhook consumption for payment, escrow, and disbursement flows
- deployment-specific merchant/developer webhook management outside the default public `/api` router

For merchant/developer webhook management, current private routes commonly include:

- `POST /users/webhooks`
- `GET /users/merchant/webhooks`
- `PATCH /users/merchant/webhooks/{webhook_id}`
- `POST /users/merchant/webhooks/{webhook_id}/test`
- `DELETE /users/merchant/webhooks/{webhook_id}`

These routes are not part of the guaranteed public `paymentAPI` surface.

### Merchant/developer event catalog

- `order.created`
- `order.updated`
- `order.completed`
- `payment.succeeded`
- `payment.failed`
- `refund.processed`
- `escrow.released`
- `dispute.opened`
- `dispute.closed`

### Merchant/developer payload shape

Current merchant/developer deliveries use a top-level event envelope:

```json
{
  "event": "payment.succeeded",
  "data": {
    "merchant_id": "6c15f7e8-335a-4c62-a993-fb06a27b7787",
    "payment_id": "3a0da2c0-a8b2-4c76-9f40-1dbf7ee03dcf",
    "reference": "test_9f7d1bd7b5aa",
    "currency": "UGX",
    "amount": 150000,
    "status": "succeeded",
    "test": true
  },
  "timestamp": "2026-04-14T10:30:00.000000",
  "webhook_id": "0e4102fa-3119-42aa-8a9c-d92ef4fa83af"
}
```

The private test-delivery route sends the first subscribed event for that webhook and sets `data.test = true`.

### Public payment helper event examples

The separate public payment helper flow may still emit subsystem-specific names inside `data.event`, such as:

- `disbursement.status_updated`
- `disbursement.cancelled`

### Webhook security

Verify:

```text
X-piaxis-Signature
X-piaxis-Event
```

When a signing secret is configured, verify the HMAC against the raw JSON body exactly as received.

Minimum webhook rules:

- accept JSON `POST`
- verify the signature before trusting the body
- route by the top-level `event` field for merchant/developer webhooks or `data.event` for the public helper flow
- respond with `2xx` after successful handling
- handle duplicate deliveries idempotently
- keep processing fast
- use HTTPS in production

### Secret lifecycle

- merchant/developer webhook creation accepts an optional custom secret
- if you omit it, Piaxis generates one for you
- the full signing secret is only returned once, immediately after creation or rotation
- later profile/list responses expose only secret metadata such as a hint suffix

Important reconciliation note:

- webhook configuration is not currently exposed in the default public `paymentAPI` router in this app
- for now, treat webhook configuration as dashboard-managed unless your deployment exposes an additional private webhook config router

## Feature Coverage vs Public Route Schema

This is the most practical section for SDK and AI users.

### Direct payment: guaranteed live request fields

- `amount`
- `currency`
- `payment_method`
- `recipient_id`
- `user_info`
- `products`
- `customer_pays_fees`
- optional route parameter `mfa_code`

### Direct payment: shown in HTML docs but not guaranteed by the live request schema

- `reference`
- `description`
- `metadata`
- `callback_url`
- `send_sms`
- `send_email`
- `card_info`

### Escrow: guaranteed live request fields

- `receiver_id`
- `amount`
- `currency_code`
- `payment_method`
- `terms`
- `external_user_id`
- `user_info`
- `user_location`

### Escrow: shown in HTML docs but not guaranteed by the live request schema

- `description`
- `products`

### Direct disbursement: guaranteed live request fields

- `recipients`
- `currency`
- `payment_method`
- `description`

### Direct disbursement: guaranteed live fee and payout preview fields

- `POST /api/disbursements/quote`
- `total_fee_amount`
- `total_debit_amount`
- `internal_total_amount`
- `external_total_amount`
- `items[].fee_amount`
- `items[].fee_breakdown`

Current default UGX external payout tier when no merchant-specific fee rule is configured:

- up to `100000 UGX`: `500 UGX`
- above `100000 UGX` up to `1000000 UGX`: `1000 UGX`
- above `1000000 UGX`: `1500 UGX`

These defaults are overrideable through Piaxis fee configuration. Internal-recipient transfers do not use this external payout fee tier.

### Direct disbursement: shown in HTML docs but not guaranteed by the live request schema

- `notify_recipients`
- `callback_url`
- top-level `reference`
- top-level `metadata`

### Escrow disbursement: guaranteed live request fields

- `recipients`
- `currency`
- `payment_method`
- `description`
- `user_location`

### Escrow disbursement: shown in HTML docs but not guaranteed by the live request schema

- `auto_release`
- `metadata`

## Integration Advice For AI-Generated Code

If you give this guide to an AI assistant, ask it to:

- use only the live public route table in this file
- respect the field-name differences exactly
- treat “HTML docs only” fields as optional or non-guaranteed unless you explicitly verify them
- implement idempotency, retries, and webhook verification
- persist your own platform references for reconciliation

Prompt template:

```text
Use the attached Piaxis External API Integration Guide.
Integrate the live public Piaxis payment API in [language].
Support [mtn|airtel|card|piaxis_external].
Use only the live routes and guaranteed request/response fields from the guide.
Implement idempotency, logging, retries, and webhook verification.
```

## Final Recommendations

- For new integrations, start from the live route/schema sections in this Markdown file.
- Use the HTML docs for broader narrative examples and webhook payload walkthroughs.
- If your integration depends on a field shown in the HTML docs but not listed as guaranteed here, confirm that field against the live route schema before shipping.
