# Payouts

A **payout** moves money from one of your seller wallets to a payee's registered bank account. Payouts settle asynchronously through the banking provider using the route selected from the payout currency and destination identifiers.

> [!IMPORTANT]
> Supported routes are **EUR IBAN → SEPA**, **GBP UK local account → Faster Payments**, and **GBP/USD IBAN → cross-border transfer**. UK local methods must use `currency: GBP`, `bankAccountType: uk_local`, `bankCountry: GB`, an 8-digit account number, and a 6-digit sort code. US-local and SWIFT payout methods can be registered but are rejected before funds are held.

> [!NOTE]
> **Amount: decimal preferred.** Send the decimal `amount` as a string (e.g. `"19.99"`) — the same convention as the rest of the API. The legacy integer `amountMinor` (e.g. `1999`) is still accepted as a fallback for older integrations; when both are sent, `amount` wins. Exactly one is required.

> [!NOTE]
> **Idempotency precedence: body over header.** `POST /payouts` reads `idempotencyKey` from the request body; the `Idempotency-Key` HTTP header is used as a fallback when the body field is empty. When both are set, the body value wins.

---

## Quick path: onboard and pay a new payee

Starting from scratch? The convenience endpoints collapse the full flow into a
short, self-contained sequence using a dedicated **cashout_balance** wallet —
funded directly from your balance, isolated from split earnings. The same
supported bank routes and Bearer auth apply as for the primitive payout flow.

> [!IMPORTANT]
> This flow provisions a `cashout_balance` wallet, so on a Live account it is
> **gated per seller** by `walletCreationEnabled` (off by default). If it is not
> enabled for your account, `POST /payees/setup` returns `403 wallet creation is
> not enabled for this account; contact support`. Contact support to enable it.
> A Demo account needs no switch: wallet creation is always on there.
> `GET /account` reports the effective value as `features.walletCreation`.

**1. Create the payee, its payout method, and a cashout wallet** — `POST /payees/setup` (one atomic call):

```bash
curl -sS -X POST "$FYNEX_API/payees/setup" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "displayName": "Acme Supplies Ltd",
    "role": "contractor",
    "payoutMethod": { "currency": "EUR", "iban": "DE89370400440532013000" }
  }'
```

Response (`201 Created`) returns all three records — keep `payee.id`, `payoutMethod.id`, and `cashoutWallet.id`:

```json
{
  "payee": { "id": 101, "role": "Contractor" },
  "payoutMethod": { "id": 501, "status": "active" },
  "cashoutWallet": { "id": 139, "type": "cashout_balance", "currency": "EUR", "status": "active" }
}
```

**2. Fund the cashout wallet from your main balance** — `POST /wallets/{cashout_wallet_id}/fund` (internal, same-currency):

```bash
curl -sS -X POST "$FYNEX_API/wallets/139/fund" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "amount": "12.50", "idempotencyKey": "'$(uuidgen)'" }'
```

Moves funds from your seller `main` wallet (same currency) into the payee's cashout wallet. Your main wallet must hold sufficient EUR.

> [!NOTE]
> **This step is optional for sellers enabled for payee payout auto-funding.**
> On `POST /payees/{payee_id}/payouts`, Fynex atomically moves only the
> cashout wallet's shortfall from the seller's same-currency `main` wallet and
> then holds the payout amount. If either operation fails, neither movement is
> committed. On a Live account auto-funding is off by default and must be enabled
> by Fynex; on a Demo account it is always on. It does not apply to the
> primitive `POST /payouts` endpoint.
> If the payout is definitively rejected or cancelled, the auto-funded
> shortfall is returned to the seller's main wallet in the same transaction as
> the payout hold release. An ambiguous provider outcome stays held until it is
> reconciled, so the platform never returns funds that the provider may still
> settle. When both `payeePayoutAutoFundingEnabled` and
> `walletCreationEnabled` are enabled for the seller (both are always on for a
> Demo account) and the payee has no
> `cashout_balance` wallet in the payout method's currency, this endpoint first
> provisions one automatically and then auto-funds it. This also applies when
> the payee already has an ordinary `other` or `tax` wallet, because those
> split-destination wallets are intentionally not debited by auto-funding.
> The seller's own `Itself` payee is excluded: its payouts keep using the
> existing seller `main` wallet and never trigger cashout provisioning.
> Provisioning is idempotent and remains separate from the money-movement
> transaction, so a payout failure may leave the empty wallet available for a
> safe retry but cannot leave a partial ledger movement.

**3. Pay the payee** — `POST /payees/{payee_id}/payouts` (wallet auto-resolved):

```bash
curl -sS -X POST "$FYNEX_API/payees/101/payouts" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "payoutMethodId": 501,
    "amount": "12.50",
    "description": "Invoice 2026-091 September commission",
    "idempotencyKey": "'$(uuidgen)'"
  }'
```

You don't pass a `walletId` — the payout draws from the payee's `cashout_balance` wallet (resolution prefers it). Response is the same `201 Created` payout body as `POST /payouts` (status `pending`). Poll `GET /payouts/{id}` for the final status (see step 3 of the primitive flow below).

> [!NOTE]
> `payoutMethodId` is **required** — there is no implicit default. The convenience layer auto-resolves the `walletId`; holds, idempotency, and banking-provider settlement use the same payout engine as `POST /payouts`. Unless the seller account is enabled for payee payout auto-funding, a payout before the cashout wallet is funded returns `409 insufficient balance`.

The four primitive endpoints below still work unchanged when you need finer control (an existing payee, a specific source wallet, multiple payout methods, or the split-funded `other` wallet).

---

## Prerequisites

- A seller wallet with sufficient balance in the target currency.
- A **registered payee** — create one with `POST /payments-api/v1/payees` (see [Payees](https://docs.fynex.ai/payments-api/v2/docs#tag/payees)) if you don't have one yet.
- That payee must have **at least one active payout method** (bank account). Create one with `POST /payments-api/v1/payees/{payee_id}/payout-methods` (see [Payout Methods](https://docs.fynex.ai/payments-api/v2/docs#tag/payout-methods)), then use `GET /payees/{payee_id}/payout-methods` to look up the available methods and their ids.

> [!NOTE]
> The step-by-step below assumes the payee and payout method already exist. If you are starting from scratch, use the [quick path](#quick-path-onboard-and-pay-a-new-payee) above, or follow the full order: **create payee → create payout method → request payout**.

---

## Step-by-step

1. **Find the payee's payout methods**

   `payee_id` is the numeric payee ID from `GET /payees`.

```bash
curl -sS "$FYNEX_API/payees/42/payout-methods" \
  -H "Authorization: Bearer $FYNEX_TOKEN" | jq
```

   The response lists bank accounts with their `id`, `currency`, `bankAccountType`, and destination identifiers. Note the `payoutMethodId` you want to pay to, and the `walletId` you want to debit (confirm it has enough balance).

2. **Create the payout**

```bash
curl -sS -X POST "$FYNEX_API/payouts" \
  -H "Authorization: Bearer $FYNEX_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "walletId": 15,
    "payoutMethodId": 501,
    "amount": "1999.00",
    "currencyCode": "EUR",
    "description": "Invoice 2026-091 September commission",
    "idempotencyKey": "'$(uuidgen)'"
  }'
```

   **Response (201 Created):**

   ```json
   {
     "id": 9001,
     "status": "processing",
     "amountMinor": 199900,
     "currencyCode": "EUR",
     "providerReference": "ref_9f2c1a4b",
     "bankAccountType": "uk_local",
     "description": "Invoice 2026-091 September commission"
   }
   ```

   Save the `id` — you will need it to poll status.

3. **Poll for the final status**

   Poll `GET /payouts/\{id\}` until `status` reaches a terminal value, or subscribe a webhook endpoint to the opt-in `PayoutCompleted` and `PayoutFailed` events (see [Webhooks](https://docs.fynex.ai/payments-api/v2/docs#tag/webhooks)).

```bash
curl -sS "$FYNEX_API/payouts/9001" \
  -H "Authorization: Bearer $FYNEX_TOKEN" | jq .status
```

   Poll every 30–60 seconds until status is `completed`, `failed`, or `cancelled`.

---

## Request body fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `walletId` | `int64` | Yes | Internal numeric id of the seller wallet to debit. **Not** an IBAN or any external bank-account identifier — look up the id from the dashboard's wallets list (contact support if you do not yet have dashboard access). Must belong to the authenticated seller. |
| `payoutMethodId` | `int64` | **Yes** | Explicit payout method id from `GET /payees/{id}/payout-methods`. Must belong to the wallet's payee and be active. **There is no implicit default** — a missing or zero value is rejected with `400`. (This removed the old wallet-default → payee-default → first-method fallback so a payout can never silently re-route.) |
| `amount` | `string` | Yes\* | Decimal amount in the payout currency, e.g. `"19.99"`. Preferred over `amountMinor`. |
| `amountMinor` | `int64` | Yes\* | **Deprecated.** Amount in minor units (e.g. `1999` for €19.99). Fallback when `amount` is omitted; ignored when `amount` is set. |
| `currencyCode` | `string` | Yes | `EUR`, `GBP`, or `USD`, matching both the wallet and payout method. The destination shape selects SEPA, Faster Payments, or cross-border routing. |
| `description` | `string` | No (will become required) | What the beneficiary sees on their bank statement, sent to the bank as remittance information. Send it on every payout now: it will become required. When supplied, leading/trailing whitespace is trimmed and internal runs collapse to one space; the result must be 1–140 characters of `a-z A-Z 0-9 / - ? : ( ) . , ' +` and space. Anything else — including an empty or blank string — is rejected with `400`, never transliterated. It is split into up to four 35-character lines at word boundaries where possible. |
| `idempotencyKey` | `string` (UUID) | No | If omitted, a UUID is auto-generated. Replays with the same key return the original payout; a replay with a different `description` returns `409`. |

\* Exactly one of `amount` or `amountMinor` is required.

> [!NOTE]
> The `Idempotency-Key` HTTP header is read as a fallback when the body's `idempotencyKey` is empty. When both are set, the body wins.

## SEPA payouts — mandatory parameters

EUR payouts are sent as **SEPA credit transfers** through the banking provider (payment scheme `SEPA`, charge bearer `SHA`). The required parameters span two calls — registering the destination **payout method** and creating the **payout** itself.

**1. Payout method (destination)** — `POST /payees/{id}/payout-methods` (or via the dashboard):

| Field | Required | Notes |
|-------|----------|-------|
| `payeeId` | **Yes** | Payee that owns the destination. Over REST it comes from the `{payee_id}` path segment (not the body); over GraphQL it is a body field. |
| `currency` | **Yes** | Must be `EUR`. |
| `iban` | **Yes** | Destination IBAN — the only hard-required bank field. A payout to a method with no IBAN is rejected (`payout method has no IBAN`). The creditor country is derived from the IBAN prefix. |
| `bic` | No | Recommended. Forwarded to the banking provider as the creditor institution only when present. |
| `accountName` | No | Recommended — used as the SEPA creditor name. Falls back to `bankName`, then `Payee {id}` when omitted. |

The payout method `type` is `bank_account` — the only supported value.

**2. Payout request** — `POST /payouts`:

| Field | Required | Notes |
|-------|----------|-------|
| `walletId` | **Yes** | Seller EUR wallet to debit; must hold sufficient EUR. |
| `payoutMethodId` | **Yes** | The EUR IBAN method from step 1. Must belong to the wallet's payee and be active — there is no implicit default. |
| `amount` | **Yes**\* | Decimal in the payout currency (`"1999.00"` = €1,999.00). Preferred over `amountMinor`. |
| `amountMinor` | **Yes**\* | Deprecated minor-units fallback (`199900` = €1,999.00). \*Exactly one of `amount`/`amountMinor`. |
| `currencyCode` | **Yes** | Must be `EUR`. |
| `description` | No (will become required) | 1–140 characters of `a-z A-Z 0-9 / - ? : ( ) . , ' +` and space; sent as the SEPA remittance information. |
| `idempotencyKey` | No | UUID; auto-generated if omitted. |

> [!NOTE]
> The banking provider's payment scheme (`SEPA`), payment reference, requested execution date, charge bearer (`SHA`), creditor country, and the unstructured remittance line are all set by Fynex — you do not send them.

The payout transitions through `pending` → `processing` → `completed` once the banking provider confirms the SEPA settlement.

## GBP Faster Payments — mandatory parameters

GBP payouts to a UK local account are sent through the banking provider's Faster Payments rail. The client supplies domestic bank identifiers; Fynex normalizes them and applies the banking provider's national-clearing-code representation at dispatch.

**1. Payout method (destination)** — `POST /payees/{id}/payout-methods` (or via the dashboard):

| Field | Required | Notes |
|-------|----------|-------|
| `currency` | **Yes** | Must be `GBP`. |
| `bankAccountType` | **Yes** | Must be `uk_local`. |
| `bankCountry` | **Yes** | Must be `GB`. |
| `accountNumber` | **Yes** | Eight-digit UK account number. Spaces are accepted and removed. |
| `sortCode` | **Yes** | Six-digit UK sort code. Spaces and hyphens are accepted and removed. |
| `accountName` | No | Recommended; used as the creditor name. |

**2. Payout request** — provide a GBP `walletId`, the UK-local `payoutMethodId`, the amount, and `currencyCode: GBP`. Bank identifiers stay on the payout method and are not repeated in the payout request.

> [!NOTE]
> The banking provider's payment scheme and the `SC` national-clearing-code prefix are set by Fynex. Clients send the six sort-code digits only.

> [!CAUTION]
> **Card payouts (push-to-card) are not supported.** Payouts settle only to bank accounts through the banking provider. There is no card-out / OCT endpoint.

---

## Response fields

### POST /payouts (201 Created)

The create response is intentionally minimal:

| Field | Type | Description |
|-------|------|-------------|
| `id` | `int64` | Internal payout ID — use this for `GET /payouts/\{id\}` |
| `status` | `string` | `pending` with `approvalStatus: "awaiting_approval"` when the payout needs review (the default on a Live account — see **Approval** below); otherwise usually `processing` |
| `approvalStatus` | `string` | Review state: `approved`, `awaiting_approval`, `compliance_review` or `rejected` — see **Approval** below |
| `amountMinor` | `int64` | Minor units |
| `currencyCode` | `string` | |
| `providerReference` | `string` | Upstream provider's reference for this payout. Opaque — match, do not parse (omitempty) |
| `bankAccountType` | `string` | Destination account identifier format: `iban`, `uk_local`, `us_local`, `swift`. Empty means unknown (omitempty) |
| `description` | `string` | The normalized description sent to the bank. Omitted for payouts created before descriptions were required (omitempty) |

### GET /payouts/\{id\} (200 OK)

The detail response adds timing fields:

| Field | Type | Description |
|-------|------|-------------|
| `id` | `int64` | |
| `status` | `string` | Current status |
| `approvalStatus` | `string` | Review state — see **Approval** below |
| `rejectionReason` | `string` | Set when the payout was rejected in review; always the same support message, never the reviewer's note (omitempty) |
| `approvedAt` | `string` | RFC3339 UTC, when the payout was approved (omitempty) |
| `amountMinor` | `int64` | |
| `currencyCode` | `string` | |
| `providerReference` | `string` | Upstream provider's reference for this payout. Opaque — match, do not parse (omitempty) |
| `bankAccountType` | `string` | Destination account identifier format: `iban`, `uk_local`, `us_local`, `swift`. Empty means unknown (omitempty) |
| `description` | `string` | The normalized description sent to the bank. Omitted for payouts created before descriptions were required (omitempty) |
| `failureCode` | `string` | Set when the payout fails (omitempty) |
| `failureMessage` | `string` | Seller-safe explanation of `failureCode`, suitable to show as-is. Present on `failed` payouts and on `cancelled` ones — "payout cancelled before approval" when you cancelled it, "Our system paused your payout. Please contact support." when it was rejected in review — both with no `failureCode`; absent otherwise (omitempty) |
| `requestedAt` | `string` | RFC3339 UTC |
| `processedAt` | `string` | RFC3339 UTC (omitempty) |
| `completedAt` | `string` | RFC3339 UTC (omitempty) |

> [!NOTE]
> Branch on `failureCode`, display `failureMessage`. The message is the fixed catalogue text for the
> code — never raw bank or operator notes — so it is stable enough to show to a seller but not
> meant to be parsed.

---

## Status values

| Status | Meaning |
|--------|---------|
| `pending` | Created in Fynex, queued for processing |
| `processing` | Submitted to the payment network |
| `completed` | Funds left the wallet and reached the destination |
| `failed` | Network rejected the payment; balance returned to wallet |
| `cancelled` | Payout was cancelled before processing |

### Approval

Payouts above your account's auto-approve limit — or held by another control on your account — wait for review by Fynex before they are processed. Such a payout is returned with `status: "pending"` and `approvalStatus: "awaiting_approval"`; `approvalStatus` is a separate field, not a `status` value, and it is present on every new payout. Unless Fynex has set an auto-approve limit for your account, the limit is `0`, so **every payout on a Live account starts awaiting approval**.

**On a Demo account every payout is approved automatically**, whatever its amount and wherever it was created, so a test payout runs to completion with nobody at Fynex involved. A payout to a marketplace vendor can still be held by the vendor limit.

The same holds for **payout methods**: on a Demo account a bank account you add is `approved` at creation, so you can pay out to it at once. On a Live account a new or re-routed payout method starts `pending` unless Fynex has enabled auto-approval for your account. When a Demo account goes Live, every payout method nobody at Fynex reviewed returns to `pending` and must be approved before it pays out.

| `approvalStatus` | Meaning |
|------------------|---------|
| `approved` | Released for processing — either approved automatically or after review (`approvedAt` is set on the detail response). |
| `awaiting_approval` | Waiting for review. You can still cancel it with `POST /payouts/{id}/cancel`; it then becomes `cancelled` with `failureMessage` "payout cancelled before approval". |
| `compliance_review` | Held for an automated compliance check. It cannot be cancelled (`409`) and resolves on its own. |
| `rejected` | Rejected in review: the payout becomes `cancelled` with `failureMessage` "Our system paused your payout. Please contact support." |

---

## Listing payouts

```bash
curl -sS "$FYNEX_API/payouts?limit=20&offset=0" \
  -H "Authorization: Bearer $FYNEX_TOKEN" | jq
```

Returns payouts ordered by `requestedAt` descending. Query params `limit` (1–100, default 20) and `offset` (default 0).

---

## Insufficient balance

If the wallet's **withdrawable** balance does not cover `amountMinor`, the request returns `409 Conflict`:

```json
{ "error": "insufficient balance" }
```

The check is against `withdrawableBalanceMinor`, not `availableBalanceMinor`. Settled funds stay inside a **T+N maturity hold** (the seller's `payoutHoldDays`, 3 business days by default) before a payout can take them, so this `409` often means the money *is* in the wallet but has not matured yet — common for a marketplace paying out shortly after a split. A Demo account has no hold: its settled funds are withdrawable at once. Check the wallet first (see [Wallets](https://docs.fynex.ai/payments-api/v2/docs#tag/wallets)):

- `maturingBalanceMinor` covers the shortfall → wait until those funds mature, then retry with the **same** `idempotencyKey`. A top-up is not held and would also unblock it, but it is unnecessary.
- Otherwise the wallet really is short → top it up via incoming payments or a treasury transfer, then retry with the same key.

For an auto-funded payee payout (`POST /payees/{id}/payouts`), the check covers the cashout wallet plus your main wallet's withdrawable balance, so check the main wallet's `maturingBalanceMinor` too. A separate `409 merchant balance cannot cover the payout fee` means the payout fits but its fee does not.

Replaying a **successful** create with the same `idempotencyKey` returns the original payout — it does not create a second one and does not return `409`.

---

## Common errors

| Status | Body | Cause |
|--------|------|-------|
| `400` | `walletId is required` | Missing or zero `walletId` |
| `400` | `payoutMethodId is required` | Missing or zero `payoutMethodId` (no implicit default) |
| `400` | `amount must be positive` | Zero or negative amount |
| `400` | `currencyCode is required` | Missing currency |
| `400` | `description is required` | `description` supplied as an empty or whitespace-only string |
| `400` | `description must be at most 140 characters` | `description` longer than 140 characters after whitespace normalization |
| `400` | `description contains unsupported characters; allowed: letters, digits, space and / - ? : ( ) . , ' +` | `description` contains any other character (accents, `&`, `_`, quotes, emoji, …) |
| `409` | `idempotency key was already used with a different description` | Same `idempotencyKey` replayed with a different `description` |
| `401` | `authorization token is required` / `invalid authorization token` (`code` `missing_api_key` / `invalid_api_key`) | Missing or invalid bearer token |
| `403` | `wallet does not belong to seller account` | `walletId` belongs to another seller |
| `404` | `wallet not found for this currency` | `walletId` exists but currency mismatches |
| `404` | `payee not found` | `payee_id` doesn't exist or belongs to another seller |
| `409` | `insufficient balance` | Withdrawable balance below the requested amount — funds missing, or still inside the maturity hold |
| `409` | `merchant balance cannot cover the payout fee` | The main wallet's withdrawable balance does not cover the payout fee |
| `422` | `currency not supported for payouts` | `currencyCode` has no enabled payout rail |
| `422` | `this payout account is saved, but payouts for its account format are not enabled yet` | The payout method identifier format is not enabled for that currency, or its required bank identifiers are invalid |
| `422` | `payout method does not belong to this wallet's payee` | `payoutMethodId` belongs to a different payee than the wallet |
| `422` | provider rejection message | The banking provider rejected the payment at create time; check `failureCode` via `GET /payouts/{id}` |

## See also

- **[Polling & SSE](https://docs.fynex.ai/payments-api/v2/docs#tag/polling-sse)** — Poll payout status or subscribe to status events.
- **[Idempotency & retries](https://docs.fynex.ai/payments-api/v2/docs#tag/idempotency)** — Safe retry patterns for payout creation.
- **[Split rules](https://docs.fynex.ai/payments-api/v2/docs#tag/splits)** — Splitting incoming funds across multiple payees? Use Split rules.
- **[Virtual accounts](https://docs.fynex.ai/payments-api/v2/docs#tag/virtual-accounts)** — Need a dedicated bank account for incoming payments? See Virtual accounts.
