Skip to main content
Approved identity is required before account-authenticated collection. A person account needs approved KYC and a company account needs approved KYB before it can create a payin. This applies to POST /v1/payins (QR, card, checkout, bank_transfer and fintoc), POST /v1/payins/collect/otp, POST /v1/payins/collect, and POST /v1/payins/deposit-accounts, as well as stored-card charges and POST /v1/subscriptions. Until approval, these requests return HTTP 403 verification_required.
This gate applies to authenticated account creation. Public payment pages for links that already exist remain payable, passive deposits already received remain processable, org-admin assignment remains operational, and system-created deposit instruments are not blocked.
A payin is a fiat collection: your customer pays in local currency and your account is credited directly in USD cents. When another asset is configured, the net follows the post-credit conversion flow at your payin rate (payin_rate in GET /v1/rates) minus the fixed payin fee. In USDT v1, payin_rate_source tells you whether that rate is backed by enabled payin FIFO inventory (lot) or spot (spot). The rate is fixed before the asynchronous payment is credited. Whatever the mode, every path ends the same way — automatic credit + webhook:

1. Discover the available corridors

The available countries, currencies and collection modes are defined by CBPay. Always check the catalog:
delivery describes how the payment is confirmed on CBPay’s side (bank notification, polling or both) — it changes nothing in your integration: you always receive the payin_credited webhook. Collection corridors and modes:
Venezuela (VES): active collection is available through c2p. The payer generates a claveDinamica in their bank app and gives it to your integration; send that value in the otp field of the collect request. There is no preceding /collect/otp request for C2P. debito_inmediato is not a live corridor. Outgoing payments to Venezuela (pago_movil, bank_transfer) are unchanged: see payouts.
Availability may vary; the catalog (GET /v1/payins/methods) is always the source of truth. For a USD-principal account, the net credit is written directly in USD cents. Configured non-USD targets use the documented post-credit conversion. The quoted payin_rate and fixed fee still apply. Payin lots are consumed FIFO when the credit is applied. If enabled inventory does not cover the credit, the existing quote remains authoritative and the uncovered portion is recorded as spot fallback with an operator alert. If you’d rather keep your collections in another balance (USDC, BTC or GOLD), configure default_payin_asset — see the money model.

2. Pick the mode and create the charge

Each country has its own collection mode. The real request and response of each one:
Hosted payment page (fintoc) — recommended: you get a payment_url; the payer opens it and transfers from any Chilean bank or wallet (Banco Estado, Santander, Mach, Tenpo, Mercado Pago…). The payment is detected and validated automatically — no manual references.
Response 201:
Share the payment_url with the payer (link, redirect or WebView). Once the payment is confirmed your account is credited in USD and you receive the payin_credited webhook. The CLP amount must be an integer (the Chilean peso has no decimals) and the payment session expires after 24 hours by default. A retry with the same idempotency_key returns the same payin and the same URL — it never opens a second payment session.For method: "card", idempotency_key is required. Send it either in the JSON body or as the Idempotency-Key header. The platform records the operation before contacting the core: if the response is ambiguous, retry with the same key so the original charge can be reconciled. The platform never creates a second core charge for that retry; do not generate a new key to recover the same payment.Announced bank transfer (manual alternative): announce the incoming deposit and share the reference with the sender.
Response 201:
When the transfer arrives it is matched by the reference in the transfer description and your account is credited automatically. If the reference does not travel, the payer’s document backs it up — see matching an announced transfer.

Manual Ecuador USD operations

The EC/USD/bank_transfer corridor supports announced payins and provider-agnostic manual payouts. Follow the dedicated guide for the bank confirmation endpoints, evidence requirements, states, and idempotent retries:

Manual Ecuador USD operations

Announce or confirm a USD bank transfer, or confirm a payout paid manually by an authorized operator.

Active collection (pull)

Some corridors expose a pull charge where the payer authorizes a debit. The account must be identity-approved before the request.

Venezuela C2P

For VE/VES/c2p, the payer first generates a claveDinamica in their bank app. Your integration sends that value as otp; it is not an OTP returned by CBPay and there is no request-OTP step:
The request uses the existing collect contract. payer_document, payer_phone, payer_bank and otp are the payer data needed by the C2P rail; otp carries the bank-generated dynamic key. The response keeps the existing payin shape and, on synchronous success, includes paid: true, provider_reference and the credited payin identifiers.

OTP-based pull methods

The platform screens the payer before calling the core. A hold returns 202 in_review without charging; retry the same request after approval. If the account is not approved, the collect request returns 403 verification_required before the corridor is executed. A timeout or 5xx does not authorize a new operation: keep the same idempotency context and follow the resource/reconciliation state. Always check GET /v1/payins/methods because corridor availability can change. The universal payment link now lives in its own guide, covering the quote engine, every rail and the public endpoints:

Checkout

One link where the payer chooses how to pay - fiat in every live country, crypto, card or the CBPay app - settled in the balance you choose.

Saved cards and recurring charges (card)

Stored credentials (COF) and scheduled subscriptions moved to their own guide:

Stored cards & subscriptions

Save cards with the payer’s consent, charge them one-click or without the payer present, and schedule recurring subscriptions.

Refunds (card)

A credited card payin can be refunded in full or in part from your balance, with its own ledger entry, receipt and webhook:

Payin refunds

Refund a card payin fully or partially, void a same-day charge, and understand how a chargeback is applied.

Matching an announced transfer

An announced transfer (method: "bank_transfer") has no payment session: the payer moves the money from their own bank, so the deposit is recognized when it lands. Matching runs in this order and stops at the first hit:
  1. reference — the 12-character code in the transfer description.
  2. Payer document — the announcement’s payer_document against the payer the bank reports (dots, dashes and check digit are ignored).
  3. Single candidate — exactly one pending announcement for that amount and currency.
If none of the three resolves to one announcement — two pending announcements for the same amount, no reference, no payer document — the deposit is not credited on a guess: it lands as unassigned and your CBPay operator routes it. No money is lost; it is already in the collection account.
method: "bank_transfer" accepts the payer’s data. Every field is optional and additive — existing integrations keep working unchanged:
payer_source always comes back so your checkout knows what to ask the payer for:
A document shorter than 5 characters or with no digits is dropped as a signal (it cannot be told apart from an amount or a bank code). The announcement is still created and payer_source reports the real coverage.

Retries and idempotency

The announcement accepts idempotency_key (body) or the Idempotency-Key header. A retry with the same key returns the original announcement — same reference — with idempotency_hit: true and HTTP 200 instead of creating a second one.
Two live announcements that look identical (same account, currency, amount and payer) are exactly the case matching refuses to resolve: the real deposit matches both and lands unassigned. That is why a POST without a key reuses a live identical announcement instead of duplicating it (also 200 with idempotency_hit: true).To collect two real payments of the same amount from the same payer, send a different idempotency_key for each one — each key creates its own announcement with its own reference.
Keys are unique per account and per logical operation: if you reuse an idempotency_key that was already used with ANOTHER payin method (QR, checkout, card), the API replies 409 idempotency_conflict instead of returning an object that does not match your request.

Deposit instructions: where to send the money

On corridors where your organization registered a destination account for announced transfers (today Chile, Paraguay and the United States), the announcement response includes a deposit_instructions block — the exact bank account the payer must transfer to, with the amount and reference already baked into a copy-paste QR:
Response 201 (with deposit instructions)
You can also preview the destination account before creating a payin — useful to show the payer where they will need to send money once they confirm:
Response 200
On the US/USD corridor the preview returns two blocks: the domestic rail under deposit_instructions and, when your organization has the international variant configured, the SWIFT rail under deposit_instructions_swift (same shape, its own QR). The rail fields are absent (not empty) on corridors that do not use them:
Response 200 (US)
The preview endpoint’s qr_payload has no Amount/Reference lines (there is no payin yet); the one embedded in an actual announcement always has both, so the payer can pay without typing anything by hand. Both blocks on the announcement are a frozen snapshot: if your CBPay operator later updates a registered account, live announcements keep pointing at the account they were created with — only new ones pick up the change.
The exact same deposit_instructions (and deposit_instructions_swift, when present) blocks are echoed back on GET /v1/payins/{id} and on the list (GET /v1/payins), so your front end does not need to cache it from the creation response. On corridors without a registered destination account, the field is simply absent — fall back to showing the reference and asking the payer to use their usual bank details for your organization.

Dedicated-account metadata and payer identity

Pushes credited to a dedicated deposit account expose instrument_id, an instrument block and the bank-reported payer block — full shape in the Funding guide. Filter the history with ?instrument_id=<uuid>.

3. Receiving the credit

When the payment arrives (through any of the modes), your account is credited automatically and the payin_credited webhook fires:
fx_rate is your payin_rate at credit time — the conversion happens at exactly that rate: usdt_gross = 700.00 / 6.91. When present, rate_source is the quote provenance (lot or spot); historical payins without a loaded quote omit it. The payin object keeps the full detail:

Statuses

A deposit that cannot be resolved to a single announcement stays unassigned until the CBPay team routes it to an account (see matching an announced transfer). Once assigned, it is credited with the destination account’s rate and fees, and the announcement it belonged to is closed.
When an active charge (QR or checkout) dies unpaid, the payin moves from pending to expired (or failed) automatically and you receive the payin_expired webhook. No funds move: to retry the collection, create a new payin.

Reads and history

from/to use YYYY-MM-DD (organization timezone); an invalid date responds 400 invalid_range. method and kind narrow the rail and payin shape, refund_status accepts none, partial or full, and search performs a case-insensitive partial match on the payin ID, reference or idempotency key.

Common errors

GET /v1/payins/deposit-instructions responds 404 not_found when the corridor has no active destination account configured — treat it the same way as the 422 above: the payer cannot be shown an account yet.

FAQ

Subscribe to payin_credited: it carries the FX rate applied, the fee and the exact usdt_credited. You can also poll GET /v1/payins/{id}.
The quoted payin_rate returned by GET /v1/rates and carried by the payin at credit time. Check payin_rate_source: lot means enabled USDT payin inventory backed the quote, while spot means the spot path was used. Your agreed spread is already inside the rate — it is never itemized.
Yes — set default_payin_asset with PUT /v1/settlement. Accounts credit directly in USD cents; another configured target uses the post-credit conversion flow and conversion_status reports done or pending_retry.
You receive payin_expired and the payin closes without moving money. Create a new charge — nothing was debited or credited.
The reference still matches the announcement, but the amount that arrived is what gets credited. A transfer that resolves to no announcement stays unassigned for reconciliation; your CBPay team can assign it to the right payin manually.
Nobody by chance. If the payer document does not tell them apart, both announcements stay pending and the deposit lands as unassigned for the operator to route. Sending payer_document in the announcement is what turns this case into an automatic credit.
No — it is optional and nothing breaks without it. When you omit it, the verified tax ID of the account is used (payer_source: account_identity), which covers self-deposits. Send it whenever a third party pays for your client, and always show the reference to the payer.
No. With idempotency_key (body or Idempotency-Key header) the retry returns the original announcement with idempotency_hit: true. Even without a key, a POST that is identical to a live announcement (same account, currency, amount and payer) reuses it — duplicating it would leave the real deposit unassigned for ambiguity. Send different keys only when you really want to collect twice.
The corridor publishes two destination accounts on purpose, and your payer picks the rail their bank supports: senders banking inside the US use the domestic rail (deposit_instructions) with the routing_number (ABA) — a Fedwire or ACH; senders outside the US use the international rail (deposit_instructions_swift) with the swift (BIC), the correspondent bank (intermediary_bank_name / intermediary_bank_swift) and the notes hints for the wire form. Whatever the rail, the reference (CB…) in the memo / remittance field is what credits the deposit automatically. A wire is usually reported the same business day; an ACH can take one to three business days depending on the sending bank — the credit and the payin_credited webhook happen the moment the bank reports the deposit.
Banks don’t share a common QR standard for arbitrary destination accounts (unlike a merchant QR at checkout) — every bank encodes account transfers differently, and most banking apps can’t auto-fill a transfer from a third-party QR at all. qr_png_base64 renders the account details as a QR purely as a copy shortcut on mobile: the payer scans it, gets the multi-line text (bank, account, holder, amount, reference), and pastes it into their own bank’s transfer form — they still confirm the transfer themselves. Don’t build a scan-and-pay flow around it; show it next to the plain-text fields so the payer can always type them manually.

Card payin settlement timing

For card payins, the settlement_hours setting controls when the balance becomes available after the payment is confirmed. It accepts 0 or a multiple of 24: 0 makes the balance available immediately, while 24 is one US business day and 48 is two US business days. Business days are Monday through Friday excluding observed US federal holidays, evaluated in your organization’s timezone. For example, Friday at 15:00 plus 48 hours settles Tuesday at 15:00 when no holiday intervenes; Saturday plus 48 hours also settles Tuesday. A value such as 27 is rejected with HTTP 400 invalid_settlement_hours. The payment is confirmed as credited immediately; only balance availability waits for settle_at. Existing settle_at timestamps and earlier non-multiple configurations retain calendar-hour semantics.

Local-fiat payin hold

Set fiat_hold_local: true with PATCH /v1/accounts/{accountID} to retain future BOB, MXN or ARS credits in their local balance instead of converting them to USDT. Existing credited payins are never rewritten.
credit_asset is always present; fiat_credited is present only for local fiat. Cards always settle in USD. Auto-conversions, checkout swaps and OTC offers skip locally held payins.
refunded_amount uses the units of credit_asset (micro-USDT for USDT, minor fiat units for BOB/MXN/ARS). usdt_credited is always micro-USDT. Branch on credit_asset; never calculate usdt_credited - refunded_amount when the credit is fiat.

USD as the principal ledger asset

Accounts use USD as the default for settlement_asset and payin_settlement_asset; change it any time with PUT /v1/settlement. In-flight operations are never re-quoted. USD ledger amounts use cents (two decimal places). Direct credits and debits, fees, and supported payout, checkout, POS, and card paths use USD only where their asset field accepts it. Swaps between USD and USDT use 1:1 pricing and do not use an FX oracle. BOB, MXN, and ARS remain separate fiat-ledger assets; same-currency payouts debit those balances directly (see payouts). A payin credited directly to a USD-principal account exposes credit_asset: USD and the credited fiat amount in cents; usdt_credited remains the USD-equivalent reporting field. A controversy hold follows the credited asset: USD credits use hold_asset: USD. disputed and held use hold-asset units; disputed_usdt and held_usdt are normalized equivalents.
Last modified on October 17, 2026