Beneficiaries
A payout goes to a beneficiary — a destination you register once and pay many times. Registering it first lets the corridor's rules be checked before any money is involved, and lets the receiving bank's name check run before you commit a payment.
All routes are on the money API: secret key, master key or dashboard session,
or a restricted key with the scope — accounts:read to ask a corridor or list
payees, beneficiaries:write to register and verify one.
What a corridor needs
Local rails are not alike. An INR payout needs an IFSC code, a CAD one an institution and transit number, a BRL one a tax id. Ask the corridor, and build the form from the answer:
GET /v1/banking/corridors?currency=EUR&country=DE&entity_type=business
| Query | Required | Notes |
|---|---|---|
currency | Yes | ISO 4217 of the destination account |
country | Yes | ISO 3166-1 alpha-2 of the destination bank |
entity_type | business or individual — corridors ask different things of each | |
payment_method | bank_account (default) or e_transfer (Interac, CAD) |
{
"currency": "EUR",
"country_code": "DE",
"accepted_schemes": ["iban"],
"rails": ["sepa", "sepa_instant"],
"fields": [
{"key": "bic", "label": "BIC / SWIFT code", "required": false,
"pattern": "^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$", "example": "MOCKDEFFXXX", "group": "account"}
]
}
accepted_schemes— whichaccount.schemevalues this corridor takes.fields— every extra field, withrequired,pattern,example,allowed_valuesfor a dropdown,min_length/max_length,required_for_railsandconditional_on. Send their values incorridor_fields, keyed bykey.payment_purposes— the purpose codes a payout to this corridor may carry, andreference_max_length, when the corridor has them.
GET /v1/banking/payout-options lists every currency a payout can be made in
and every destination country with its default currency.
A corridor nobody offers is 422 corridor_unsupported — a standing answer,
not an outage, so do not retry it:
{
"success": false,
"error": "corridor_unsupported",
"message": "INR payouts to BR are not offered. Retrying will not change this: choose another currency or destination.",
"details": {"corridor": {"type": "payout", "currency": "INR", "country": "BR"}}
}
Registering a beneficiary and sending a payout answer the same code for the
same corridor. A corridor that is offered by a rail that is down right now
is 503 service_unavailable: retry with backoff.
Registering
POST /v1/banking/beneficiaries
Idempotency-Key: beneficiary-kettlewick
Content-Type: application/json
{
"beneficiary_type": "business",
"name": "Kettlewick Trading GmbH",
"currency": "EUR",
"country_code": "DE",
"account": {"scheme": "iban", "iban": "DE89370400440532013000"},
"address": {"line1": "Hafenstraße 12", "city": "Hamburg", "postal_code": "20457", "country_code": "DE"}
}
| Field | Required | Notes |
|---|---|---|
beneficiary_type | Yes | business or individual |
name | Yes | The account holder's name exactly as their bank holds it — it is what the payee-name check compares |
currency, country_code | Yes | |
account | Yes | One coordinate, selected by scheme (below) |
bank_name, address | ||
corridor_fields | Extra fields the corridor asked for |
account.scheme | Fields |
|---|---|
iban | iban, optional bic |
scan | sort_code, account_number (UK) |
us_routing | routing_number, account_number |
ca_routing | institution_number, transit_number, account_number |
swift_bic | bic, account_number |
bank_routing | routing_scheme (ifsc, bsb, branch_code, ncc, cnaps, clabe), account_number, routing_number |
interac | handle |
cxc_handle | handle — another CashXChain business's username; paid on our partner's own books |
201 Created:
{
"id": "mockben_7d2f19c4e8a04b6f9e1c3a5b7d9f0e21_4da118ece0f5b6bd",
"beneficiary_type": "business",
"name": "Kettlewick Trading GmbH",
"currency": "EUR",
"country_code": "DE",
"account": {"scheme": "iban", "iban": "DE89370400440532013000"}
}
Validation against the corridor runs before the beneficiary is created:
400 bad_request with "corridor validation failed for EUR/DE: …", or
"scheme "iban" requires field "iban"".
Retries. Idempotency-Key is required. On the sandbox rail a retry with
the same key returns the same beneficiary. In production the key is not
honoured by the partner, and a retried create can register a second, identical
beneficiary. If a create times out, list beneficiaries before retrying.
A cxc_handle payee that you have already registered in the same currency is
returned as it is, with 200 instead of 201. Such a beneficiary carries
usable_at, about a minute after registration: on the production rail a
payout sent to it earlier is accepted and then fails. Nothing holds the payout
back for you — wait until usable_at.
Listing
GET /v1/banking/beneficiaries
{"beneficiaries": [{"id": "…", "name": "Kettlewick Trading GmbH", "…": "…"}]}
If a rail is unreachable, its beneficiaries are served from our last good copy
and the rail appears in degraded_partners. A rail you have no relationship
with is not asked, and never appears there. There is no route to fetch or
delete a single beneficiary.
In the sandbox, the simulated rail keeps beneficiaries in memory. After a
sandbox restart they can drop out of this list — with no degraded_partners
entry — and verify answers 404 for them, while payouts to them still work.
Registering one again with its original Idempotency-Key brings back the same
id.
The payee-name check
Since October 2026, UK and EU payments are checked against the name the receiving bank holds for the account — Confirmation of Payee for GBP Faster Payments and CHAPS, Verification of Payee for EUR. A payment to a name that does not match is rejected by the bank after it has been accepted, which the payer experiences as a payment that simply fails. So we ask first.
Ask
POST /v1/banking/beneficiaries/{beneficiary_id}/verify
{"outcome": "close_match", "may_pay": false, "account_holder": "Kettlewick Trading Ltd", "check": "COP"}
The check is one synchronous call to the receiving bank's scheme — there is no
pending state and nothing to poll. outcome is one of:
outcome | may_pay | Meaning |
|---|---|---|
matched | true | The name matches. |
not_checked | true | No check applies to this destination, or none produced an answer. Most corridors in the world have no check; in the UK it covers Faster Payments and CHAPS, in the EU euro destinations other than payments on our partner's own books. Not a failure. |
close_match | false | Nearly matches. account_holder is the name the bank holds — show it to the payer. |
no_match | false | The name does not belong to this account. The real holder is deliberately not revealed. |
invalid | false | The bank rejected the account details themselves. |
check names the scheme: COP, VOP or PAYEE.
The sandbox rail has no name check: every beneficiary there answers
not_checked. You cannot exercise the other four outcomes in the sandbox;
build their handling from this table and the error below.
Pay, or don't
POST /v1/banking/payouts runs the same check itself, before it commits any
money. When the outcome is not a pass, it refuses with
422 payee_name_mismatch and tells you what would clear it:
{
"success": false,
"error": "payee_name_mismatch",
"message": "The receiving bank says this account is held by Kettlewick Trading Ltd. …",
"details": {
"outcome": "close_match",
"account_holder": "Kettlewick Trading Ltd",
"check": "COP",
"partner_detail": null,
"resolve_by": "accept-verification",
"accept_url": "/v1/banking/beneficiaries/mockben_…/accept-verification"
}
}
| Outcome | resolve_by | To proceed |
|---|---|---|
close_match | accept-verification | Show the payer account_holder. If they confirm, record their acceptance (below) and resend the payout under the same Idempotency-Key. |
no_match | accept-verification | Tell the payer the name does not match. If they still want to pay, record their acceptance, then resend the payout under the same key. |
invalid | beneficiary | Nothing clears it: the bank rejected the account details themselves, and accept_url is null. Correct the details and register a new beneficiary. |
One route forward for every outcome a payer can clear, as Wise has one. An acceptance is a statement that a person saw the bank's answer and chose to pay anyway. Recording it without showing the payer anything defeats the protection — and the payer's protection against misdirected-payment fraud with it.
Accept the bank's verdict
POST /v1/banking/beneficiaries/{beneficiary_id}/accept-verification
Content-Type: application/json
{"payment_idempotency_key": "payout-INV-0042"}
The check runs again, so what is accepted is what the bank says now, not what
your client believed. The acceptance is scoped to one payment — the
Idempotency-Key the payout will carry — the way Wise scopes acceptance to a
single quote: consent to pay a mismatched payee once is not consent to pay
them again. 200:
{
"outcome": "no_match",
"accepted": true,
"verification_id": "7a1c…",
"acceptance_id": "9e4b…",
"partner_acknowledged": true
}
outcomeis the verdict the acceptance was taken against, from the check just run.account_holderis included on a close match only.accepted: falsewithmatchedornot_checkedmeans there was nothing to accept — pay. That is every answer in the sandbox, whose rail has no name check.partner_acknowledgedsays whether the bank's consent record was also written. A hard mismatch needs it once per payee; whether it was already in place is our problem, not the caller's.400 bad_requestwithoutpayment_idempotency_key;404 not_foundfor a beneficiary that is not yours;422 payee_name_mismatchwithresolve_by: "beneficiary"for aninvaliddestination, which no consent can clear.
Once a hard mismatch has been acknowledged with the bank, the bank reports the
payee as not_checked from then on. The API keeps answering no_match for
that payee from its own record, so that one acceptance does not become a
standing exemption for every later payment to it.
If the name check itself cannot run when you pay — the bank's scheme is down —
the payout proceeds as not_checked rather than failing.