Skip to main content

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:

HTTP
GET /v1/banking/corridors?currency=EUR&country=DE&entity_type=business
QueryRequiredNotes
currencyYesISO 4217 of the destination account
countryYesISO 3166-1 alpha-2 of the destination bank
entity_typebusiness or individual — corridors ask different things of each
payment_methodbank_account (default) or e_transfer (Interac, CAD)
JSON
{
"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 — which account.scheme values this corridor takes.
  • fields — every extra field, with required, pattern, example, allowed_values for a dropdown, min_length/max_length, required_for_rails and conditional_on. Send their values in corridor_fields, keyed by key.
  • payment_purposes — the purpose codes a payout to this corridor may carry, and reference_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:

JSON
{
"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​

HTTP
POST /v1/banking/beneficiaries
Idempotency-Key: beneficiary-kettlewick
Content-Type: application/json
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"}
}
FieldRequiredNotes
beneficiary_typeYesbusiness or individual
nameYesThe account holder's name exactly as their bank holds it — it is what the payee-name check compares
currency, country_codeYes
accountYesOne coordinate, selected by scheme (below)
bank_name, address
corridor_fieldsExtra fields the corridor asked for
account.schemeFields
ibaniban, optional bic
scansort_code, account_number (UK)
us_routingrouting_number, account_number
ca_routinginstitution_number, transit_number, account_number
swift_bicbic, account_number
bank_routingrouting_scheme (ifsc, bsb, branch_code, ncc, cnaps, clabe), account_number, routing_number
interachandle
cxc_handlehandle — another CashXChain business's username; paid on our partner's own books

201 Created:

JSON
{
"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​

HTTP
GET /v1/banking/beneficiaries
JSON
{"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​

HTTP
POST /v1/banking/beneficiaries/{beneficiary_id}/verify
JSON
{"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:

outcomemay_payMeaning
matchedtrueThe name matches.
not_checkedtrueNo 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_matchfalseNearly matches. account_holder is the name the bank holds — show it to the payer.
no_matchfalseThe name does not belong to this account. The real holder is deliberately not revealed.
invalidfalseThe 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:

JSON
{
"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"
}
}
Outcomeresolve_byTo proceed
close_matchaccept-verificationShow the payer account_holder. If they confirm, record their acceptance (below) and resend the payout under the same Idempotency-Key.
no_matchaccept-verificationTell the payer the name does not match. If they still want to pay, record their acceptance, then resend the payout under the same key.
invalidbeneficiaryNothing 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​

HTTP
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:

JSON
{
"outcome": "no_match",
"accepted": true,
"verification_id": "7a1c…",
"acceptance_id": "9e4b…",
"partner_acknowledged": true
}
  • outcome is the verdict the acceptance was taken against, from the check just run. account_holder is included on a close match only.
  • accepted: false with matched or not_checked means there was nothing to accept — pay. That is every answer in the sandbox, whose rail has no name check.
  • partner_acknowledged says 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_request without payment_idempotency_key; 404 not_found for a beneficiary that is not yours; 422 payee_name_mismatch with resolve_by: "beneficiary" for an invalid destination, 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.