purpose: "banking" to retain local fiat for payouts; default fondeo converts incoming fiat to your account’s principal asset (USD is the principal balance). Banking supports only BO/BOB/bank_transfer, MX/MXN/bank_transfer, and AR/ARS/bank_transfer.
Two kinds of local-fiat destinations
For local fiat corridors (BO/BOB, MX/MXN, AR/ARS), every payin
destination carries a purpose that decides where inbound money lands:
Create a second destination with
purpose: "banking" to hold local fiat for
payouts; destinations created without a purpose keep the historical fondeo
behavior. Banking purpose is only supported in BO/BOB/bank_transfer,
MX/MXN/bank_transfer and AR/ARS/bank_transfer.
Banking by currency
🇺🇸 USD — US dollar
Open a classic Banking customer account (ACH, Fedwire and SWIFT rails) with the profile → accounts → operations flow in this guide (open bank accounts). USD received through Banking is held for payouts (payouts); USD sent to your CBPay balances lands in your principal asset (USD is the principal balance).🇪🇺 EUR — euro
Everything euro goes through a virtual IBAN: request afunding_usdt
address to convert inbound euros to your principal asset, or a banking_eur
address to hold BANK_EUR for SEPA payouts
(funding direction and EUR banking).
Sending euros to third parties is covered in
SEPA Instant payouts.
🇧🇴 BOB — Bolivian boliviano
Receive bolivianos through deposit accounts with apurpose: fondeo (default) auto-converts the credit to your principal
asset (USD is the principal balance); banking retains BOB for local
payouts. See Funding for the funding flow.
🇲🇽 MXN — Mexican peso
Receive pesos through deposit accounts with apurpose: fondeo (default) auto-converts the credit to your principal
asset (USD is the principal balance); banking retains MXN for local
payouts (payouts).
🇦🇷 ARS — Argentine peso
Receive pesos through deposit accounts with apurpose: fondeo (default) auto-converts the credit to your principal
asset (USD is the principal balance); banking retains ARS for local
payouts (payouts).
Company multi-banking: operating accounts
Company accounts can create multiplepurpose: "banking" deposit accounts
in each enabled local corridor: BO/BOB, MX/MXN, and AR/ARS, always with
method: "bank_transfer". Send a distinct Idempotency-Key for each
destination; reusing it returns the same instrument and never provisions a
second bank account. The soft operational limit is 50 banking instruments
per company account and corridor (422 deposit_account_limit_reached).
Person accounts retain one banking instrument per corridor.
Create and label an operating destination
201:
label is a local nickname only and is optional; it is limited to 60
characters. Change or clear it with:
AR alias lifecycle
For a company AR/ARS destination, the CVU can be created withoutbank_alias. It is then operationally alias_pending: creation succeeds,
but balance and movements return 409 alias_pending until the company sets
the alias. Complete it with
PUT /v1/payins/deposit-accounts/{instrumentID}/bank-alias. Person accounts
keep the existing 400 missing_alias creation rule.
Balance and movements
Onlypurpose: "banking" instruments expose these operating views:
funds_model is retained for BO and centralized for MX/AR. Movements
are a paginated union of payins and payouts, with kind, id, asset,
amount_minor, direction, status, optional local_amount, and
created_at.
Banking fees come in two shapes. Standalone fixed fees
(
banking_customer, banking_account, banking_operation) are debited
from your default settlement asset balance — USD is the principal balance — when each operation executes and refunded
automatically if it fails. Transactional rail fees
(banking_deposit, banking_transfer_ach, banking_transfer_swift,
banking_transfer_wire, banking_transfer_sepa) are a percentage plus a
fixed amount charged in the operation currency (your BANK_USD /
BANK_EUR balance) — see rail fees.
With a fee of 0 (the default) the service is free. The banking_fee and
banking_fee_asset fields on each response show what was charged and in
which currency.The full flow
- Create your banking profile (
POST /v1/banking/customer) — once. - Upload verification documents and submit for review.
- Once
approved, open accounts per currency. - Register beneficiaries (counterparties) for third-party payments.
- Send payments: quote with
prepare, execute withoperations.
banking_customer_status_changed and
banking_operation_status_changed webhooks (webhooks).
1. Create your banking profile
Once per account. If you omittype, name or email, they are filled
from your CBPay account:
201:
409 banking_customer_exists.
Durable idempotency. The platform derives a deterministic core claim for
this account/profile. The first successful request returns
201. Repeating
the same request returns 201 with idempotency_hit: true and never creates
a second profile or charges the profile fee twice. A different payload for
the same account returns 409 idempotency_conflict. If the upstream/core
outcome is ambiguous, the API returns 503 banking_recovery_pending; do not
retry with a new key. Operations must reconcile the durable claim first.Application review. If your organization enabled banking application review, this request can answer
202 Accepted with {"status":"in_review","kind":"banking_application","review_id":"…"} instead of 201 — the profile is created only when compliance approves the review. The banking profile fee is charged when the application is held and refunded automatically if it is rejected. Track the result with the webhook txn_review_status_changed or in Transaction reviews.EUR uses direct vIBAN; generic onboarding is USD-only. Request
{"purpose":"banking_eur"} from Operar EUR without a prior customer; approval creates the local linkage. Generic EUR returns 400 banking_eur_requires_viban.2. Documents and verification
Upload each document as base64 (free):draft → submitted → under_review → approved or
rejected. The banking_customer_status_changed webhook notifies each
change — for your own profile (customer_kind: self) and for the third
parties you register (customer_kind: third_party, with their
third_party_id):
3. Open bank accounts
With the profileapproved, create the classic Banking customer account for USD only (ACH/Fedwire/SWIFT rails). EUR is not opened here; request a banking_eur virtual IBAN directly in the EUR virtual IBAN flow.
201 — data carries the details to receive funds (account
number/IBAN, routing, bank):
GET /v1/banking/accounts/{id}) returns the account
LIVE — name, currency, status, and under data the requirements to
receive funds (wire and local rails: bank, account number/IBAN,
routing). Use it to show the deposit instructions of a specific account
without walking the list:
source tells you where the detail came from: live (the bank answered
in real time) or mirror (the bank could not serve the account at that
moment and the last known snapshot is returned — deposit requirements
remain available).
The list exposes only the accounts enabled for your operation
according to the corridor configuration. A non-enabled account does not
appear in the list and its by-id queries respond
404 not_found.Limit for person accounts: a person account can hold at most 1 bank
account. Attempting a second one returns
409 banking_account_limit.
Company accounts have no limit.Third-party users (companies only)
If your account is a company, besides your own accounts you can register third-party banking users — your end clients (persons or companies) — each with their own identity and verification and bank accounts in their name. No limit on third parties or accounts per third party.Registering the third party
Registration requires theverification_id of an approved KYC/KYB
verification of the third party — their single identity
inside CBPay. The type comes from the verification kind (KYC ⇒
INDIVIDUAL, KYB ⇒ COMPANY), the data (name, email, address) auto-fills
from the verified profile (whatever you send explicitly wins), and the
already-validated documents are re-delivered automatically to the
banking provider. The banking profile fee is charged (refunded if the
registration fails):
201:
documents_synced counts the verification documents that were loaded
automatically into the third party’s banking profile. If one could not be
synced (or the bank requests additional categories), upload it through the
manual document flow below and then submit.Application review. With banking application review enabled, registering a third party can also answer
202 Accepted (kind: banking_application): the third party is registered only when the review is approved, and the registration fee is refunded automatically on rejection. Track it via txn_review_status_changed or Transaction reviews.third_party_id: every third-party route uses it. List and fetch
(the GET carries the live verification status):
Third-party verification (free)
Same as your own profile, but on the third party:Third-party accounts
Once the third party is approved, open accounts for them (samebanking_account fee) and operate just like your own:
- Each third party belongs to you and only you: another CBPay account can
never see or operate it (it gets
404). - A person account attempting to create third parties receives
403 company_required. - Without
verification_id(or with a non-approved verification) the registration answers422 verification_required/422 verification_not_approved. If you send atypethat does not match the verification kind,422 verification_kind_mismatch. Third parties created before this rule keep operating normally. - Registered third parties feed the “new users” metric of your account summary.
4. Register beneficiaries
To pay third parties, first register the beneficiary with their banking details (free; it goes through moderation before it can be used):GET /v1/banking/counterparties and attach more accounts
to an existing beneficiary with
POST /v1/banking/counterparties/{id}/accounts.
5. Send payments
Two operation types:
Quote first (free, moves no money):
banking_operation fee when the rail has no configuration — is charged
here):
202:
banking_fee and banking_fee_asset only appear when a fee was charged.
With a per-rail fee the asset is the operation currency (BANK_USD /
BANK_EUR); with the default fallback it is your default settlement asset.
- The final state arrives through the
banking_operation_status_changedwebhook (completed/failed); you can also pollGET /v1/banking/operations/{id}. Once the operation reaches a final state, the webhook includes itsreceipt_urland you can download the PDF receipt withGET /v1/banking/operations/{id}/receipt(receipts). - Retries with the same
Idempotency-Keyreturn the original operation (idempotency_hit: true) without charging the fee again.
Complete traceability. Every banking operation is recorded on your
account: it shows up in the
banking_operations section of the
statement, its money reconciles in the
BANK_USD/BANK_EUR mirror balances (assets section), and its volume
adds to the gross_volume in analytics. The
authoritative balance remains the bank’s: the mirror is reconciled
periodically.Every banking operation — including inbound deposits and bank fees
discovered automatically from the bank — exposes its
direction (in /
out), net amount, currency, counterparty and reference whenever
the bank reports them. These fields are optional and appear in
GET /v1/banking/operations and GET /v1/banking/operations/{id}.Banking destinations for verified third parties
Company accounts can request a banking destination for an approved third-party KYC or KYB submission. The request is naturally idempotent per submission and corridor; approval triggers the destination automatically.201 for a new intent and 200 for a replay:
pending, fulfilled, and abandoned. Approval creates the
instrument and changes the intent to fulfilled; a rejected verification
changes it to abandoned. A new request for an abandoned intent returns
409 intent_abandoned; an own-onboarding verification returns
422 verification_invalid.
The banking_operation_status_changed webhook stays lightweight by design:
it carries the identifiers and the new status only, never the enriched
fields. When it fires, fetch the operation detail to read the direction,
amount, counterparty and reference. See webhooks.
Euros: funding direction and EUR banking in one place
Everything euro lives in this section. A virtual IBAN is an addressing and routing instrument, not a separate provider balance, and comes in two purposes:Request a virtual IBAN
The platform builds the provider registrant from the approved KYC/KYB profile. Request the default funding address with an idempotency key:incorporation_country / incorporationCountry and
incorporation_date / incorporationDate. These values are used only to
complete the verified registrant; they do not bypass KYC/KYB.
The request is accepted into the manual approval queue — every allocation is
manually approved by organization operations before the core calls the
banking rail:
banking_eur is requested explicitly with the same call and
{"purpose":"banking_eur"} as the body (same idempotency contract).
List the account’s requests with the mandatory date window:
{page, page_size, virtual_ibans}. Supported statuses are
pending_approval, pending, active, rejected, failed, disabled and
closed. Account-level responses show the full IBAN when one has been
assigned; the organization-admin read surface masks it.
Read, close and per-address operations
Read one request, close it, or list the operations attributed to it (from/to are required on the operations listing):
Virtual IBANs for third parties
Company accounts can request abanking_eur vIBAN for an owned third party
after the third party has accepted the invitation and completed approved
KYC/KYB. The third party is the balance owner; the company remains the
relationship manager:
banking_eur and follows the same manual approval
and status lifecycle as an account-owned request.
BANK_EUR balance
For abanking_eur request, query the reconciled balance explicitly:
409 balance_not_available for funding_usdt, because that
purpose converts to your principal asset through the payin credit chain instead of holding BANK_EUR.
Sending euros out
Both outbound paths resolve an allocated, active virtual IBAN as their source, each with its own purpose:
Select the source with
source_virtual_iban_id at the top level:
422 funding_account_required; multiple active sources without an explicit
UUID return 422 ambiguous_source_viban. A malformed
source_virtual_iban_id returns 400 invalid_request; a valid UUID that is
not found or does not belong to the account returns 404 not_found. A
temporary source lookup failure returns 503 funding_account_unavailable.
The source check runs before any debit or provider dispatch.
The platform strips caller-supplied ordering-party fields and stamps payer
server-side from the selected vIBAN’s registrant (unless a more-specific
server-side company-wallet payer is present). Individual registrants provide
first and last name; corporate registrants provide the registered company
name and registration data. For active earlier rows without a usable
registrant, the platform derives the ordering names from the current
verified profile. If that profile cannot be read, the request returns
503 funding_account_unavailable; if the resulting identity is incomplete,
it returns 422 registrant_incomplete.
There is no per-vIBAN amount cap. source_virtual_iban_id is part of the
operation intent: if the active source set changes between sequential
retries, the same idempotency key returns the original operation. A
concurrent in-flight request returns 409 operation_in_progress; retry with
the same key. The key is the identity of the original operation — the core
Banking replay does not compare payloads, so do not reuse a key for a
materially different payload.
Operation holds created before this source gate are grandfathered: they
continue through their existing dispatch and reconciliation path.
Receiving euros (inbound)
When the banking provider sends a terminal inbound event, the core normalizes it intobanking_virtual_iban_inbound. The platform resolves
the owning vIBAN and applies its purpose:
funding_usdtcreates abanking_fundingpayin in EUR, applies the marked EUR→USDT rate and the configured funding fee/spread, then follows the normal payin firewall and credit chain.banking_eurcreates an inbound Banking mirror inBANK_EUR, uses the Banking ledger path, emits KYT/operation status and queues the banking receipt email.
completed, success, settled, credited or
succeeded) apply the financial effect. Pending or unknown statuses remain
reconcilable and do not credit a balance; failed and rejected are
terminal negative outcomes that never create a credit (an uncredited funding
payin is marked failed without a refund loop).
The account operation detail can expose payment_rail: "SEPA_INSTANT",
purpose (funding_usdt or banking_eur) and the associated virtual IBAN
reference when reported by the banking rail, preserving the product purpose
through the mirror, statement and analytics.
Returns and recalls are handled as terminal
returned, recalled or
reversed events. A return before credit prevents a later completion replay
from crediting. A credited funding_usdt payin creates the deterministic
chargeback/refund path; a ledgered banking_eur operation receives an
append-only reverse entry. A later completion event cannot resurrect a
returned operation.EUR errors
EUR FAQ
Does requesting a virtual IBAN create a BANK_EUR balance?
Does requesting a virtual IBAN create a BANK_EUR balance?
No. The purpose is recorded on the request, but a virtual IBAN is not an
independent provider balance. Only
banking_eur flows touch BANK_EUR;
funding_usdt converts to your principal asset through the payin credit chain.Rail fees (deposits and transfers)
On top of the standalone fixed fees, banking supports transactional fees per rail — a percentage plus a fixed amount, always charged in the operation currency (BANK_USD / BANK_EUR), never in USDT:
For transfers the available balance must cover
amount + fee — if it
does not, the API answers 402 insufficient_funds and the operation is
not created. If the operation is definitively rejected right after
dispatch, the fee is refunded automatically (same discipline as the
default fee).
Fallback: if the rail has no specific configuration (neither at account
nor at platform level), the default banking_operation fee (fixed, debited from your default settlement asset)
applies. A rail configured with 0% + 0 fixed is explicitly free — it
does not fall back to the default fee.
Operation statuses
Errors
The general error catalog lives in Errors.
FAQ
Does banking money show up in my USD balance?
Does banking money show up in my USD balance?
A
funding_usdt inbound converts to your principal asset through the funding payin chain (USD is the principal balance). A banking_eur inbound is credited to the Banking EUR mirror after the terminal event and ledger processing. The provider remains authoritative for the bank balance. Per-rail fees are charged in the
operation currency (your BANK_USD/BANK_EUR balance); only the default
banking_operation fallback fee is debited from your default settlement asset balance.What happens to the fee if an operation fails?
What happens to the fee if an operation fails?
It is refunded automatically — profile, account and operation fees alike,
including per-rail fees (refunded on the definitive synchronous rejection).
A retry with the same
Idempotency-Key returns the original operation
(idempotency_hit: true) and never charges twice.How many bank accounts can I open?
How many bank accounts can I open?
One per currency (USD, EUR). Additionally, person accounts can hold at
most 1 bank account in total (
409 banking_account_limit); company
accounts have no limit.Why doesn't one of my accounts appear in the list?
Why doesn't one of my accounts appear in the list?
The list only exposes accounts enabled for your operation per the
corridor configuration. A non-enabled account does not appear and its
by-id queries answer
404 not_found — contact your CBPay team if you need
it enabled.Can a person account register third-party users?
Can a person account register third-party users?
No — third parties are a company feature (
403 company_required).
Registration also requires the verification_id of an approved KYC/KYB
verification of the third party.How do I know when a payment reached its final state?
How do I know when a payment reached its final state?
Subscribe to
banking_operation_status_changed: it fires on completed /
failed and includes the receipt_url once final. You can also poll
GET /v1/banking/operations/{id}.Banking creation claims and recovery
Creation claims are durable records, not permission to send a second provider request. The platform storesbanking_creation_claims with kind,
idempotency_key, request_hash, status, attempts and refund state. Migration
105 creates this table and its single-flight lock; migration 106 stores
transaction-firewall fee-refund recovery. Migration 074 persists the resolved
core provider on customer idempotency claims.
Recovery rules
- A completed account recovery compares the provider account ownership with
the claimed customer’s
ProviderID. - A terminal provider authentication response (
401or403) does not by itself prove that the creation failed; the claim remains recoverable. - Reusing the same idempotency key with a different request hash is rejected.
- A completed claim can be replayed with HTTP
200andidempotency_hit: true; recovery does not call the provider again. POST /v1/org/banking/creation-claims/refundretries a pending fee refund forkind=customerorkind=account.POST /v1/org/txn-firewall/fee-refunds/{resourceID}/retryretries the durable firewall-fee refund. A completed refund replays as HTTP200.
POST /v1/ops/banking/account-creations/reconcile verifies the provider
account and finalizes the local mirror. The org-admin routes list claims,
reconcile customer claims, and retry refunds. These operations are locked so
concurrent reconciliation cannot originate duplicate money movement.