The Fiat Payout API lets an organization send USD from its Infini USD balance to a third-party bank account.

All endpoints use this prefix:

`/v2/payouts`

## Minimum API surface

| Method | Endpoint | Purpose |
|  --- | --- | --- |
| `POST` | `/v2/payouts/quotes` | Validate bank details and obtain a time-limited payout quote |
| `POST` | `/v2/payouts` | Create and immediately start a direct payout |
| `POST` | `/v2/payouts/status/batch` | Query payout statuses by creation-time range with pagination |
| `GET` | `/v2/payouts/{payout_id}` | Query current payout status |


There is no recipient resource in this release. Bank destination details are submitted with the quote and are immutably bound to `quote_id`. `POST /v2/payouts` cannot replace or modify that destination.

## Choose the amount to control

When creating a quote, choose whether the recipient amount or your total debit must remain exact. `fee_paid_by` changes how Infini calculates both amounts; it is not a display-only field. The first release supports the following two options.

### Recipient receives an exact amount

Use this option when the recipient must receive a specific amount, such as an invoice payment.

```json
{
  "amount": "1000.00",
  "amount_mode": "receive",
  "fee_paid_by": "PAYER"
}
```

The recipient receives exactly `1000.00 USD`. `PAYER` uses an external fee deduction: payout fees are added to the amount debited from your Infini USD balance.

- `receiving_amount` is `1000.00`.
- `sending_amount = receiving_amount + transfer_fee_amount + bank_fee_amount`.
- `transfer_fee_amount = sending_amount × fee_rate_decimal`; Infini grosses up `sending_amount` until the recipient amount remains exact after all fees.


For example, with a `0.001` transfer-fee rate and a `50.00 USD` bank fee, Infini calculates `sending_amount=1051.05`, `transfer_fee_amount=1.05`, and `receiving_amount=1000.00`.

### Debit an exact total amount

Use this option when the payout must not debit more than a fixed budget.

```json
{
  "amount": "1000.00",
  "amount_mode": "send",
  "fee_paid_by": "BENEFICIARY"
}
```

Infini debits exactly `1000.00 USD`. `BENEFICIARY` uses an internal fee deduction: payout fees are deducted from that amount, so the recipient receives less.

- `sending_amount` is `1000.00`.
- `receiving_amount = sending_amount - transfer_fee_amount - bank_fee_amount`.
- `transfer_fee_amount = sending_amount × fee_rate_decimal`.


For example, with a `0.001` transfer-fee rate and a `50.00 USD` bank fee, Infini calculates `sending_amount=1000.00`, `transfer_fee_amount=1.00`, and `receiving_amount=949.00`.

Use only the field pairs shown above. Other `amount_mode` and `fee_paid_by` combinations are not supported in the first release. If you change `fee_paid_by`, request a new quote; `quote_id` locks the fee responsibility and calculated amounts used by the payout.

### Read the quote amounts

- `sending_amount` is the total amount debited from your Infini USD balance.
- `receiving_amount` is the amount sent to the recipient's bank account.
- `transfer_fee_amount` is the Infini service fee for processing the payout. It does not include any bank or rail fee.
- `bank_fee_amount` is the banking fee associated with the selected bank and payout rail, such as SWIFT, ACH, or wire.
- `billing.total_fee` is the sum of all fee components and is provided in the itemized billing breakdown.
- `fee_rate_decimal` is the quoted fee rate, and `rate` is the exchange rate.


For the quoted payout, use `sending_amount` as the outgoing amount in your reconciliation records.

## Direct execution

API payouts do not enter the dashboard approval workflow. `POST /v2/payouts` runs the authoritative pre-execution checks and starts execution immediately.

A bank payout is asynchronous. A successful create response with `status=processing` means the request passed Infini's checks and execution started. It does not mean the recipient bank account has already been credited.

`completed` means the payout completed successfully. A receiving bank can still reject and return a previously completed transfer. The original payout remains `completed`; an independent compensating refund is exposed through the optional `return` object.

## Quote versus execution checks

`POST /v2/payouts/quotes` validates the destination and performs preliminary route, amount, compliance, and limit checks. It returns a time-limited snapshot of the destination, amounts, rate, fees, and fee responsibility. A quote:

- does not reserve organization balance;
- does not approve or create a payout;
- cannot be reused after expiry;
- does not guarantee that current limits, compliance state, balance, or provider availability will remain unchanged.


`POST /v2/payouts` repeats every execution-critical check immediately before creating the canonical payout, debiting the balance, and starting bank execution.

## Idempotency

`client_reference_id` is merchant-scoped and identifies one logical payout:

- a retry with the same reference and identical business request returns the existing resource;
- the response sets `is_duplicate=true` for an idempotent payout retry;
- reusing a reference with different business data returns `409 idempotency_conflict`.


The `quote_id` and all payout-defining fields participate in the idempotency comparison.

## Concurrent submissions

An organization can have only one `POST /v2/payouts` submission entering execution at a time. If another payout submission is already in progress, the API rejects the concurrent request before consuming the quote, creating a payout, or debiting the organization's balance:

```json
{
  "code": 108015,
  "message": "Another payout submission is in progress. Please retry shortly.",
  "data": {
    "error": "payout_submission_in_progress",
    "field": null,
    "retryable": true
  }
}
```

Retry after a short randomized delay. For the same logical payout, keep the original `client_reference_id` so that idempotency remains effective.

## Error codes

Application errors use the following envelope:

```json
{
  "code": 108006,
  "message": "Insufficient payout balance",
  "data": {
    "error": "insufficient_balance",
    "field": "amount",
    "retryable": false
  }
}
```

Use `code` or `data.error` for program control. Do not match on `message`, which is intended for troubleshooting. When `data.retryable=true`, retry only after the indicated transient condition is resolved. HMAC requests rejected directly by the gateway can return only a `message` field because they do not reach the application.

Fiat Payout-specific business error codes use the reserved `108001`-`108099` range. The HTTP status and response-body `code` serve different purposes: use the HTTP status for the error category and `code` for the stable business reason.

| HTTP | `code` | `data.error` | Returned by | Retryable | Meaning and action |
|  --- | --- | --- | --- | --- | --- |
| 400 | `30013` | `invalid_parameter` | Quote, Create, Get, List | No | A request field is missing, malformed, unsupported, or inconsistent. Correct `data.field` and submit again. |
| 401 | `401` | `authentication_failed` | All endpoints | No | HMAC or application authentication failed. Check the API key, signature, date, digest, and signed request target. |
| 403 | `403` | `permission_denied` | All endpoints | No | The API key lacks the required permission, the IP policy rejected the request, or the organization cannot access the resource. |
| 403 | `108010` | `payout_source_account_not_found` | Quote, Create | No | The organization does not have a USD payout account. |
| 404 | `404` | `resource_not_found` | Create, Get | No | The `quote_id` or `payout_id` does not exist for the authenticated organization. |
| 409 | `108012` | `idempotency_conflict` | Create | No | The `client_reference_id` was already used with different business data. Use a new reference for a different logical payout. |
| 409 | `108013` | `quote_already_used` | Create | No | The quote has already been consumed. Request a new quote. |
| 409 | `108014` | `quote_not_active` | Create | No | The quote is no longer active. Request a new quote. |
| 409 | `108015` | `payout_submission_in_progress` | Create | Yes | Another payout submission is executing for the organization. Retry with a short randomized delay. |
| 409 | `108011` | `payout_source_account_invalid` | Quote, Create | No | The USD payout account configuration is inconsistent. Contact Infini support. |
| 422 | `108001` | `invalid_fee_mode` | Quote | No | The selected `amount_mode` and `fee_paid_by` combination is unsupported. |
| 422 | `108002` | `quote_expired` | Create | Yes | The quote expired. Request a new quote; do not retry the expired `quote_id`. |
| 422 | `108003` | `quote_unavailable` | Quote, Create | Yes | Quote calculation or its bound destination snapshot is temporarily unavailable. Request a new quote after a short delay. |
| 422 | `108004` | `amount_exceeds_limit` | Quote | No | The amount exceeds the supported payout amount. Lower the amount. |
| 422 | `108005` | `amount_too_small` | Quote | No | `sending_amount` must be greater than `100 USD`. Increase the amount. |
| 422 | `108006` | `insufficient_balance` | Create | No | The organization's available USD balance is insufficient for `sending_amount`. Add funds before creating another payout. |
| 422 | `108007` | `daily_limit_exceeded` | Create | No | The payout would exceed the account's shared daily withdrawal limit. Wait for the next limit window or lower the amount. |
| 503 | `108009` | `payout_service_unavailable` | Create | Yes | Payout execution or provider liquidity is temporarily unavailable. Retry later with the same `client_reference_id` for the same logical payout. |
| 500 | `500` | `internal_error` | All endpoints | Yes | An unexpected internal error occurred. Retry safely; if it persists, contact Infini support with the request ID. |


## Sensitive bank data

The quote request contains sensitive bank details. Responses return only a masked account number. Clients must not put raw bank-account values in URLs, logs, `client_reference_id`, `statement_reference`, or other free-text metadata.

## Status and reconciliation

| Payout status | Meaning |
|  --- | --- |
| `processing` | Execution started and final settlement is pending |
| `completed` | The banking partner reported payout completion at the latest known state |
| `failed` | Payout failed before successful bank completion |


There is intentionally no approval-pending status for API payouts.

Use `POST /v2/payouts/status/batch` to reconcile payouts created within an inclusive Unix-second time range. Results are ordered by creation time descending. The endpoint uses page-based pagination: `page` defaults to `1`, `page_size` defaults to `20`, and `page_size` cannot exceed `100`.

### Bank return and refund

If the receiving bank later returns a completed payout, `status` remains `completed`, `failure` remains `null`, and `return` contains the compensating refund:

```json
{
  "status": "completed",
  "return": {
    "status": "refunded",
    "reason_code": "recipient_bank_rejected",
    "reason": "Beneficiary bank rejected the transfer",
    "returned_amount": "1000.00",
    "refund_amount": "1000.00",
    "retained_fee": "51.00",
    "currency": "USD",
    "returned_at": 1786674271
  }
}
```

- `returned_amount` is the amount reported as returned by the banking partner.
- `refund_amount` is the amount actually credited back to the organization's balance.
- `retained_fee` is the original payout fee that was not refunded.
- `returned_at` is the time the compensating refund was recorded.


For payouts without a bank return, `return` is `null`.

Use `GET /v2/payouts/{payout_id}` for one known payout or `POST /v2/payouts/status/batch` for time-window reconciliation. Both are authoritative public reconciliation interfaces.