> For the complete documentation index, see [llms.txt](https://developers.unigox.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developers.unigox.com/tutorials/third-party-payouts.md).

# Third-party payouts

Use this flow when a KYC-verified customer sends money to another person or business. The customer is the **sender** and the party receiving money is the **recipient**.

One Partner API user must represent one real sender. Never route multiple real customers through a shared shell user. Unigox evaluates recipient fan-out, payment value and velocity, relationship/purpose coherence, and screening results for every sender.

### Funding: you pre-fund, we debit

Payouts are funded from **your own crypto balance**, not from your customer's.

Your Unigox partner account is itself an account with a wallet. You top that wallet up with crypto in advance, and every payout your customers make draws down that balance: the wallet signs the transfer into each order's escrow. Your customer never sends crypto to you through Unigox — how they pay you, if at all, is outside this API.

The practical consequence: **an empty balance stops every payout**, not just the next one. Keep the wallet funded ahead of demand.

The address is the one on your Unigox wallet page — it does not change, and it is the same address that appears as `sender_address` on any order's transfer authorization parameters. Send only the token and chain shown there; a transfer on another chain cannot fund an order and is not recoverable.

### End-to-end flow

1. Pre-fund your wallet (above). Crypto must be there before you initiate.
2. Create one partner user for the real sender and complete KYC.
3. Register a partner-scoped recipient identity.
4. Add a validated payout destination to the recipient.
5. Request an off-ramp quote with the sender, recipient, destination, relationship, and purpose.
6. Initiate the quote. Unigox creates the order and applies the same compliance controls used by Portal payouts.
7. Wait for a liquidity provider to accept. A new order starts at `awaiting_liquidity_provider` and has no escrow yet, so `transfer-authorization-parameters` answers `409 INVALID_STATUS` ("no liquidity provider has accepted it"). Poll `GET /orders/{order_id}` — or take a webhook — until `next_action` becomes `authorize_crypto_transfer`.
8. Fund the order's escrow from your balance: `GET /api/v1/partner/orders/{order_id}/transfer-authorization-parameters`, sign the returned ForwardRequest, then `POST /api/v1/partner/orders/{order_id}/authorize-crypto-transfer`. `sender_address` is your wallet; `recipient_address` is the escrow deployed for this order.
9. Read the order and compliance state.

Recipient identity and destination values are versioned. The quote freezes the exact execution values it validated. Later edits never alter an existing quote or order.

### 1. Register a recipient

```http
POST /api/v1/partner/recipients
X-API-Key: <api-key>
Content-Type: application/json
```

```json
{
  "recipient_kind": "business",
  "recipient_name": "Shenzhen Example Trading Co Ltd",
  "recipient_native_name": "深圳示例贸易有限公司",
  "recipient_country": "CN",
  "recipient_business_registration_number": "91440300EXAMPLE"
}
```

The recipient belongs to the authenticated partner, not to one sender. Store the returned `recipient.id`.

### 2. Add a payout destination

```http
POST /api/v1/partner/recipients/{recipient_id}/destinations
X-API-Key: <api-key>
Content-Type: application/json
```

```json
{
  "country_code": "CN",
  "currency": "CNY",
  "rail": "cnaps",
  "institution_id": "china-construction-bank",
  "details": {
    "beneficiary_type": "business",
    "bank_name": "China Construction Bank",
    "account_number": "6222021234567890123",
    "company_name": "Shenzhen Example Trading Co Ltd",
    "company_name_native": "深圳示例贸易有限公司",
    "mobile_number": "13800138000"
  }
}
```

`rail` and `institution_id` are partner-facing slugs — take `rail` from `/api/v1/supported/payment-rails` and `institution_id` from `/api/v1/supported/institutions`. Unigox resolves them to the platform's payment network and payment method IDs and stores those, so the route is a real reference rather than a string.

`/api/v1/supported/institutions` is **paginated**: `limit` defaults to `20` and caps at `100` (a larger value is a `400`, not a silent clamp), and `offset` defaults to `0` — read `pagination.total` to know how far to page. On a corridor like CNY/CNAPS, which lists hundreds of banks, do not conclude a bank is missing from the first page: filter with `search` (matches name or slug, case-insensitive partial), `code` (the institution's own bank code), or `institution_id` (exact slug) instead of paging through everything. Only institutions that are live on the queried rail are listed, so an institution absent from the response cannot be paid — creating a destination on it is refused.

#### `details` field names come from the rail

`details` is validated against the selected rail's configuration, and its keys are that rail's own field names — the same ones `/api/v1/supported/payment-rails` advertises for the format you are using. Do not invent generic names such as `account_holder_name`: they are rejected.

**Pick the format from the institution, not from a table.** A rail can carry several formats, and the one your `details` is validated against is decided by the institution you chose:

1. take the institution from `/api/v1/supported/institutions` — each carries an `institution_type`;
2. find the rail format whose `institution_types` contains that type;
3. send that format's `fields`.

Formats also carry `has_liquidity`. A format with `has_liquidity: false` cannot currently be settled in that corridor — do not build against it. On `cnaps`/CNY today the two bank formats are liquid and the `ewallet` format is not: no `mobile-wallets` institution is active on this corridor, so Alipay and WeChat Pay destinations cannot be created. Check the endpoint rather than this sentence.

#### Paying a company: `beneficiary_type`

A format may have a business sibling (`cnaps-bank` ↔ `cnaps-bank_business`) that applies to the **same** institutions but collects a company's details instead of a person's. The sibling is not selected by the institution — it is selected by a reserved routing key inside `details`:

```json
"beneficiary_type": "business"
```

Omit it, or send `"individual"`, and the individual format is used.

Send it whenever you send company fields. Without it, `company_name` is validated against the individual format and rejected with `unknown field 'company_name' is not allowed for this payment network`.

For `rail: "cnaps"`, institutions of type `traditional-banks`:

| Paying    | `beneficiary_type`     | Required `details`                                                                                                              |
| --------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| a company | `business`             | `bank_name`, `account_number`, `company_name`, `company_name_native`, `mobile_number`                                           |
| a person  | `individual` (or omit) | `bank_name`, `account_number`, `id_number`, `first_name`, `last_name`, `native_first_name`, `native_last_name`, `mobile_number` |

`mobile_number` must be an 11-digit Chinese mobile number (`13800138000`) — an international prefix such as `+8613800138000` is rejected.

A missing or malformed field returns `400` with `code: "INVALID_REQUEST"`, and the message names the offending field. Store the returned `data.id`.

#### What reads return

Reads mask the values that identify an account or a person, and return the rest in full so you can tell two destinations apart:

* masked to last-4 (`•••• 1234`): `recipient_id_number`, `recipient_business_registration_number`, and inside `details` `account_number`, `bank_account_number`, `iban`, `card_number`, `id_number`, `national_id`, `tax_id`, `mobile_number`, `phone_number`, `msisdn`;
* returned in full: names, `bank_name`, `province`, `branch`, routing codes, `beneficiary_type`, and every identifier we issued (`id`, `payment_method_id`, `payment_network_id`, `created_at`).

`kind` comes back lowercase (`individual` | `business`), matching the `recipient_kind` you send.

### 3. Request a quote

A third-party payout is routed by `recipient_destination_id`, so `payment_details_id` must be **omitted**. Sending both is rejected with `400 INVALID_REQUEST` rather than one silently winning.

```http
POST /api/v1/partner/offramp/quote
X-API-Key: <api-key>
Content-Type: application/json
```

```json
{
  "user_uuid": "550e8400-e29b-41d4-a716-446655440000",
  "sender_id": "550e8400-e29b-41d4-a716-446655440000",
  "recipient_id": "eb83f57f-814e-4da0-84e8-902d1c60204a",
  "recipient_destination_id": "881b2818-90dd-4e80-a77d-6ce67b6b95a7",
  "sender_recipient_relationship": "supplier",
  "purpose_of_payment": "goods_and_services",
  "purpose_details": "Invoice INV-2026-0042",
  "crypto_currency": "USDT",
  "fiat_currency": "CNY",
  "fiat_amount": "5000",
  "rail": "cnaps"
}
```

`crypto_currency` and the amount are not decoration: a pair with no vendor liquidity, or an amount above what the corridor can currently serve, returns `409 NO_OFFERS_AVAILABLE`. Call `/api/v1/partner/liquidity` or `/api/v1/partner/offramp/estimate` first — both are public — instead of discovering the ceiling from a failed quote.

`user_uuid` and `sender_id` must identify the same real, KYC-verified sender. The destination currency must exactly equal `fiat_currency`; this flow does not support cross-currency recipients. A CNY destination receives CNY.

At quote time Unigox verifies tenant ownership, lifecycle, screening, route, currency, sender eligibility, relationship, and purpose. Trades stores an internal immutable snapshot and integrity hash.

### 4. Initiate the payout

Initiate the returned quote through the standard off-ramp initiate endpoint. Unigox revalidates the frozen quote, applies sender-level controls, and sends reviewable payouts to the shared Compliance queue before execution.

The response includes:

* `recipient_context`: partner-visible sender, recipient, destination, relationship, purpose, and the screening status the payout was authorized against;
* `compliance.case_id`: the compliance record for this payout. It is the `quote_id`, so a partner and a Unigox operator refer to the same record;
* `compliance.status`: `authorized` in v1;
* `compliance.risk_decision`: `allow` in v1;
* `compliance.requires_review`: `false` in v1.

The same two blocks are returned by `GET /api/v1/partner/orders/{order_id}` for the life of the order, so you do not have to persist the initiate response to show a payout's compliance state later.

API-created and Portal-created payouts produce the same record through the same path.

### Compliance&#x20;

Controls are enforced BEFORE the order exists, not as a partner-visible review state machine. A payout that fails a control does not come back as a "held" order — the request is rejected, and you act on the error:

<table><thead><tr><th>Error code</th><th width="92.28125">HTTP</th><th>Meaning</th><th>Partner action</th></tr></thead><tbody><tr><td><code>KYC_NOT_CLEARED</code></td><td>422</td><td>The sender's KYC is not cleared for this partner.</td><td>Complete the sender's KYC; do not substitute another user.</td></tr><tr><td><code>THIRD_PARTY_CONTEXT_INVALID</code></td><td>422</td><td>The recipient/destination is not usable: not found for this partner, archived, screening not <code>cleared</code>, incomplete route, or <code>sender_id</code> ≠ <code>user_uuid</code>.</td><td>Read the message; re-check the recipient, or wait for screening.</td></tr><tr><td><code>INVALID_REQUEST</code></td><td>400</td><td>The payload is wrong — a <code>details</code> field the rail does not accept, a missing required third-party field, <code>payment_details_id</code> sent alongside <code>recipient_destination_id</code>, a destination currency that is not <code>fiat_currency</code>, or a currency no third-party-enabled payout route serves (<code>third-party recipient payout is not available for {CURRENCY}</code>). The message names what to fix.</td><td>Fix the request.</td></tr><tr><td><code>RECIPIENT_NOT_FOUND</code></td><td>404</td><td>No recipient with that id belongs to your partner account, or it was archived.</td><td>Re-create the recipient, or use one from <code>GET /api/v1/partner/recipients</code>.</td></tr><tr><td><code>RECIPIENT_SERVICE_UNAVAILABLE</code></td><td>503</td><td>The recipient directory could not be reached. Nothing about your request was wrong.</td><td>Retry with backoff.</td></tr><tr><td><code>NO_OFFERS_AVAILABLE</code></td><td>409</td><td>No vendor can currently serve this corridor and amount.</td><td>Retry later or use a different amount.</td></tr><tr><td><code>THIRD_PARTY_PAYOUT_AGENT_NOT_READY</code></td><td>409</td><td>The deployed payout agent has not confirmed support for per-payment relationship and purpose, so no third-party CNY order may be created. Your quote is untouched and stays valid.</td><td>Do not retry in a loop — this clears on our side, not yours. Contact support if it persists.</td></tr><tr><td><code>RAIL_ROUTE_MISMATCH</code></td><td>400</td><td>The rail you asked for does not match the route the destination resolves to.</td><td>Send the <code>rail</code> the destination was created with, or omit it.</td></tr><tr><td><code>THIRD_PARTY_PAYOUT_UNDER_REVIEW</code></td><td>422</td><td>Returned on initiate. Compliance put this payout in review — someone looks at it on our side.</td><td>Do not retry the same payout; wait for the outcome.</td></tr><tr><td><code>THIRD_PARTY_PAYOUT_DECLINED</code></td><td>422</td><td>Returned on initiate. Compliance refused it outright, with nothing pending: a breached sender limit, or another initiation for the same sender still in flight.</td><td>An identical retry fails identically. Change the payout, or retry the in-flight case after the other one settles.</td></tr></tbody></table>

`THIRD_PARTY_PAYOUT_AGENT_NOT_READY` is a deliberate stop, not a fault: the relationship and purpose carried per payment on this corridor are only mapped correctly by an agent that has confirmed the capability, and an order created before that would be settled against hardcoded values.

Operator review happens on the Unigox side, over the same records, in the compliance queue. Value/velocity and fan-out holds surfaced to partners as order states are not part of v1 — do not build against the states listed in earlier drafts of this page.

### Updates and deletion

* Reuse an active recipient instead of creating duplicates.
* Update identity with `PATCH /api/v1/partner/recipients/{recipient_id}`. This bumps `version` and resets `screening_status` to `pending`, because the identity that was screened is no longer the identity on file. A quote for that recipient returns `THIRD_PARTY_CONTEXT_INVALID` until screening clears again, so do not PATCH immediately before quoting.
* List destinations with `GET /api/v1/partner/recipients/{recipient_id}/destinations`.
* Archive a recipient with `DELETE /api/v1/partner/recipients/{recipient_id}`.
* Existing orders retain their frozen snapshots after an update or archive.

Partner-facing fields use `recipient_*`, `sender_recipient_relationship`, and `purpose_of_payment`. Vendor-specific terminology is translated only inside vendor integrations.
