> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cbpayapp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SEPA Instant payouts

> Send EUR payouts through the single Europe (EU) corridor with IBAN validation and beneficiary type

export const EnvUrls = ({lang = "en"}) => {
  const T = {
    en: {
      test: "Test",
      live: "Live",
      hint: "Same API on both environments — build against test first, then go live by swapping the base URL and the key.",
      guide: "Environments and testing",
      href: "/en/environment-testing"
    },
    es: {
      test: "Test",
      live: "Live",
      hint: "La misma API en ambos ambientes — construye primero contra test y pasa a live cambiando la URL base y la key.",
      guide: "Entorno y pruebas",
      href: "/es/environment-testing"
    },
    zh: {
      test: "Test",
      live: "Live",
      hint: "两个环境的 API 完全一致——先在 test 环境构建，再通过切换基础 URL 和密钥上线。",
      guide: "环境与测试",
      href: "/zh/environment-testing"
    }
  };
  const t = T[lang] || T.en;
  const row = (label, url, keyPattern, badgeCls) => <div className="flex flex-wrap items-center gap-2 px-3 py-2">
      <span className={"rounded px-1.5 py-0.5 text-xs font-semibold uppercase tracking-wide " + badgeCls}>
        {label}
      </span>
      <code className="text-xs">{url}</code>
      <span className="text-xs text-gray-400 dark:text-zinc-500">·</span>
      <code className="text-xs">{keyPattern}</code>
    </div>;
  return <div className="my-4 rounded-xl border border-gray-200 dark:border-zinc-700 divide-y divide-gray-200 dark:divide-zinc-700 text-sm not-prose">
      {row(t.test, "https://cryptobank.qbank.cl/platform", "pk_test_...", "bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300")}
      {row(t.live, "https://api.qbank.cl/platform", "pk_...", "bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300")}
      <p className="px-3 py-2 text-xs text-gray-500 dark:text-zinc-400 m-0">
        {t.hint} <a href={t.href}>{t.guide} →</a>
      </p>
    </div>;
};

<EnvUrls lang="en" />

Use this guide when your beneficiary receives EUR through the `sepa` payout
method. The catalog is provider-agnostic: it exposes the country, `EUR` and
`sepa`, while the account is debited using the normal payout pricing and
idempotency contract.

```mermaid theme={"theme":{"light":"github-light","dark":"github-dark"}}
sequenceDiagram
  participant App as Your app
  participant CB as CBPay
  participant Rail as EUR payout rail
  App->>CB: POST /v1/payouts (country + EUR + sepa)
  CB->>CB: Validate IBAN, beneficiary type and compliance
  CB->>Rail: Create order
  CB->>Rail: Explicit approve
  Rail-->>CB: Status or signed callback
  CB-->>App: payout_status_changed
```

## Routing corridor

V1 registers one routing corridor: `country=EU`, `currency=EUR`,
`method=sepa`. `EU` is the catalog code for Europe, not the beneficiary's
actual country. The actual country is taken from the first two letters of the
normalized IBAN; the IBAN must belong to the supported SEPA Instant set.

`GB` is excluded from SEPA Instant V1: a British IBAN fails fast with HTTP 400
before dispatch because `sepaInstReachable=false`. SEPA Credit Transfer for GB
is a separate future scope. `GET /v1/payouts/methods` returns the single
`EU`/`EUR`/`sepa` row when the corridor is enabled.

## Beneficiary contract

The `beneficiary` object must include:

| Field | Rule |
| - | - |
| `beneficiary_type` | Required closed value: `individual` or `corporate`. |
| `iban` | Required; spaces are normalized, ISO 13616 mod-97 is checked and the country must be in the SEPA membership set. |
| `bic` | Optional ISO 9362 BIC, 8 or 11 characters. |
| `given_name` + `first_surname` | Required structured name for an individual; both must be present. |
| `name` | Required for a corporate beneficiary. For an individual, a multi-word value may split into first name and surname; a mononym is rejected. |

Use one idempotency key for the whole operation. The `description` is optional
at the API boundary, but when present it is normalized for the SEPA wire:
diacritics are folded, the explicit European fold map below is applied, the
result must fit the provider's corporate charset, and it is capped at **140
Unicode code points**. If the text still contains a character outside that
charset, creation fails with `400 invalid_payload`. When omitted, the adapter
generates a valid internal SEPA description.

Names use the same deterministic fold before the provider charset check:

| Input | Wire fold |
| - | - |
| accents, diaereses and `ñ` | Latin base letter (`é` → `e`, `ñ` → `n`) |
| `ß`, `æ`, `œ`, `ø`, `å`, `ł`, `đ`, `þ`, `ð` | `ss`, `ae`, `oe`, `o`, `a`, `l`, `d`, `th`, `d` |
| Maltese `ħ`, `ċ`, `ġ`, `ż` | `h`, `c`, `g`, `z` |
| any remaining character outside the provider charset | `400 invalid_payload` |

An individual must resolve to both a first name and a last name. Mononyms are
not payable as `individual` in V1; send the structured `given_name` and
`first_surname` fields or a multi-word `name`.

## Create a payout

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X POST https://api.qbank.cl/platform/v1/payouts \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "country": "EU",
    "currency": "EUR",
    "method": "sepa",
    "amount": "100.00",
    "beneficiary": {
      "beneficiary_type": "individual",
      "given_name": "Elena",
      "first_surname": "Fuentes",
      "iban": "DE89370400440532013000",
      "bic": "COBADEFFXXX"
    },
    "description": "Invoice 2026-0916",
    "idempotency_key": "sepa-eu-20260917-001"
  }'
```

The account-facing response is the normal payout resource:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "payout_id": "7d5c2f0a-1e9b-4a6d-8f3c-0b2a1d9e8c7f",
  "country": "EU",
  "currency": "EUR",
  "method": "sepa",
  "local_amount": "100.00",
  "status": "processing",
  "status_code": "approved",
  "funds_debited": true,
  "bank_reference": ""
}
```

The provider create and explicit approval are internal steps. Do not create a
replacement payout while this one is `processing`; use the same idempotency
key for a client retry.

## EUR funding source and ordering identity

For a customer account, every `sepa` payout must resolve an allocated, active
virtual IBAN with purpose `funding_usdt`. This is the funding purpose for
customer payouts; EUR Banking withdrawals use the separate `banking_eur`
purpose.

Send `source_virtual_iban_id` at the top level when you need to select a
specific active `funding_usdt` virtual IBAN:

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "country": "EU",
  "currency": "EUR",
  "method": "sepa",
  "amount": "100.00",
  "source_virtual_iban_id": "2f8c1d4e-1111-4b22-8a33-000000000001",
  "beneficiary": {
    "beneficiary_type": "individual",
    "given_name": "Elena",
    "first_surname": "Fuentes",
    "iban": "DE89370400440532013000",
    "bic": "COBADEFFXXX"
  },
  "idempotency_key": "sepa-eu-20260922-001"
}
```

* With exactly one active `funding_usdt` virtual IBAN, omitting the parameter
  selects that account deterministically.
* With no active source, the API returns `422 funding_account_required`.
* With several active sources, the API returns
  `422 ambiguous_source_viban` until you send the selected UUID.
* An explicit UUID that is not owned by the account or is not active returns
  `404 not_found` or `422 funding_account_required`.

The funding purposes, how to request and read virtual IBANs, and how the
ordering identity is stamped and repaired all live in one place: see
[Euros: funding direction and EUR banking, in one place](/en/guides/banking#euros-funding-direction-and-eur-banking-in-one-place). This page keeps only the payout contract above.

There is no per-vIBAN amount cap. The payout debits the account's normal
settlement balance (the settlement asset (USD is the principal balance); `BANK_EUR` is used by EUR Banking operations, not customer
payouts.

<Note>
  Payout holds created before this source-account gate are grandfathered: they
  continue through their existing dispatch and reconciliation path and are not
  rejected retroactively.
</Note>

The source-account check runs before any debit or provider dispatch. If the
lookup is temporarily unavailable, retry with the same idempotency key.

## Idempotency and source selection edge cases

The source vIBAN is part of the payout intent. If the active-vIBAN set changes
between two sequential requests, retrying the same idempotency key still
returns the original payout. If a concurrent request observes a different
source set, it returns `409 idempotency_conflict`; both outcomes are safe and
do not create a second debit. Do not use a new key to guess the result.

The same key cannot represent both the default source and an explicit
`source_virtual_iban_id`: that replay returns `409 idempotency_conflict`.
This strict comparison prevents silently changing the ordering account.

## Statuses and returns

| Provider signal | Public payout result | Financial meaning |
| - | - | - |
| `created`, `pending` or unknown | `processing` | Keep open and reconcile; no automatic resend. |
| `settled` | `completed` | The hold is consumed. |
| `declined` or `canceled` | `failed` | The existing failed-payout refund path applies. |
| Return notification | `failed`, `status_code: reversed` | The already-completed payout is reversed through the existing refund path. |

Subscribe to `payout_status_changed`. SEPA does not add a new webhook event.
The provider callback is verified and bridged into the same payout status
pipeline; a late callback cannot resurrect a terminal row.

## Errors

| HTTP | Code or result | Solution |
| -: | - | - |
| 400 | `invalid_payload` | Correct EUR, IBAN/name fields or the SEPA charset rules for the description and beneficiary. |
| 400 | `payout_corridor_unsupported` | Confirm the single `EU`/EUR/`sepa` row is in the live catalog and the IBAN country is supported. A GB IBAN is rejected with 400 before dispatch. |
| 422 | payout `failed` / `core_rejected` | Fix the beneficiary and create a new operation with a new idempotency key. |
| 503 | `payout_provider_failed` or `channel_unavailable` | Read the existing payout first; if ambiguous, retry only with the same idempotency key. |

<AccordionGroup>
  <Accordion title="Is BIC mandatory?">
    No. The IBAN is mandatory; BIC is optional and is sent when supplied.
  </Accordion>

  <Accordion title="Can I send USD through this method?">
    No. `sepa` is EUR-only. A different currency is rejected before dispatch.
  </Accordion>

  <Accordion title="Does a return create a new webhook?">
    No. The existing `payout_status_changed` event carries the `reversed`
    status code and the normal refund outcome.
  </Accordion>
</AccordionGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.