Skip to main content
A payin is a fiat collection: your customer pays in local currency and your account gets credited in USDT automatically, converted at your payin rate (payin_rate in GET /v1/rates) minus the fixed payin fee when configured for your account. 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: Availability may vary; the catalog (GET /v1/payins/methods) is always the source of truth. In every case the credit works the same way: converted to USDT at your current payin_rate and credited net of the fixed payin fee. 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 USDT 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.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.
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.

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. 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.

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 payin_rate in force at credit time (see GET /v1/rates). Your agreed spread is already inside the rate — it is never itemized.
Yes — set default_payin_asset with PUT /v1/settlement. The credit still enters in USDT and is converted right after at the real price; conversion_status reports done or pending_retry (auto-retried).
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.
The response and GET /v1/payins/{id} persist a failure block with the rail’s code and message (for example, a document that does not match the payer’s bank registration). Fix the input and retry with a new key.
Last modified on August 10, 2026