# 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 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.

**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. Auto-funding is off by default and must be enabled by Fynex for the
> seller account. 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 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",
    "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://api.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://api.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`.

#### curl

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

#### JavaScript

```js
const res = await fetch(`${process.env.FYNEX_API}/payees/42/payout-methods`, {
  headers: { Authorization: `Bearer ${process.env.FYNEX_TOKEN}` },
});
const { payoutMethods } = await res.json();
```

#### Python

```python
import os, requests

data = requests.get(
    f"{os.environ['FYNEX_API']}/payees/42/payout-methods",
    headers={"Authorization": f"Bearer {os.environ['FYNEX_TOKEN']}"},
).json()
payout_methods = data["payoutMethods"]
```

   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**

#### curl

```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",
    "idempotencyKey": "'$(uuidgen)'"
  }'
```

#### JavaScript

```js
import { randomUUID } from 'node:crypto';

const res = await fetch(`${process.env.FYNEX_API}/payouts`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.FYNEX_TOKEN}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    walletId: 15,
    payoutMethodId: 501,
    amount: '1999.00', // €1,999.00
    currencyCode: 'EUR',
    idempotencyKey: randomUUID(),
  }),
});
const payout = await res.json();
```

#### Python

```python
import os, uuid, requests

res = requests.post(
    f"{os.environ['FYNEX_API']}/payouts",
    headers={
        "Authorization": f"Bearer {os.environ['FYNEX_TOKEN']}",
        "Content-Type": "application/json",
    },
    json={
        "walletId": 15,
        "payoutMethodId": 501,
        "amount": "1999.00",  # €1,999.00
        "currencyCode": "EUR",
        "idempotencyKey": str(uuid.uuid4()),
    },
)
payout = res.json()
```

   **Response (201 Created):**

   ```json
   {
     "id": 9001,
     "status": "processing",
     "amountMinor": 199900,
     "currencyCode": "EUR",
     "providerReference": "ref_9f2c1a4b",
     "bankAccountType": "uk_local"
   }
   ```

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

3. **Poll for the final status**

   There are no outbound webhook events for payouts. Poll `GET /payouts/\{id\}` until `status` reaches a terminal value.

#### curl

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

#### JavaScript

```js
const res = await fetch(`${process.env.FYNEX_API}/payouts/9001`, {
  headers: { Authorization: `Bearer ${process.env.FYNEX_TOKEN}` },
});
const { status } = await res.json();
```

#### Python

```python
import os, requests

data = requests.get(
    f"{os.environ['FYNEX_API']}/payouts/9001",
    headers={"Authorization": f"Bearer {os.environ['FYNEX_TOKEN']}"},
).json()
print(data["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. |
| `idempotencyKey` | `string` (UUID) | No | If omitted, a UUID is auto-generated. Replays with the same key return the original payout. |

\* 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`. |
| `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` | Typically `processing` on a fresh create |
| `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) |


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

The detail response adds timing fields:

| Field | Type | Description |
|-------|------|-------------|
| `id` | `int64` | |
| `status` | `string` | Current status |
| `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) |

| `failureCode` | `string` | Set when the payout fails (omitempty) |
| `failureMessage` | `string` | Declared on the response but is never populated by the current implementation |
| `requestedAt` | `string` | RFC3339 UTC |
| `processedAt` | `string` | RFC3339 UTC (omitempty) |
| `completedAt` | `string` | RFC3339 UTC (omitempty) |

> [!NOTE]
> `failureMessage` appears in the response schema but is never set by the server today. Use `failureCode` to detect failures; do not rely on `failureMessage` for message text.

---

## 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 |

---

## 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 does not cover `amountMinor`, the request returns `409 Conflict`:

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

Top up the wallet via incoming payments or a treasury transfer, then retry with the **same** `idempotencyKey`.

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 |
| `401` | `unauthorized` (plain text) | 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` | Wallet balance below requested amount |
| `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://api.fynex.ai/payments-api/v2/docs#tag/polling-sse)** — Poll payout status or subscribe to status events.
- **[Idempotency & retries](https://api.fynex.ai/payments-api/v2/docs#tag/idempotency)** — Safe retry patterns for payout creation.
- **[Split rules](https://api.fynex.ai/payments-api/v2/docs#tag/splits)** — Splitting incoming funds across multiple payees? Use Split rules.
- **[Virtual accounts](https://api.fynex.ai/payments-api/v2/docs#tag/virtual-accounts)** — Need a dedicated bank account for incoming payments? See Virtual accounts.
