Collections (pay-ins)
A collection asks a customer in a supported country for money. The customer pays
in the local currency. After the customer pays, we add the equal amount in
USD to your balance. To see the price first, use
POST /v1/collections/quote.
Initiate a collection
POST /v1/collections
curl -X POST {BASE_URL}/v1/collections \
-H "x-api-key: sk_test_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"country": "NG",
"method": "bank",
"amount": 50000,
"customer_id": "cus_123",
"customer_name": "Ada Obi",
"reference": "invoice_8842"
}'
| Field | Required | Description |
|---|---|---|
country |
yes | Corridor country (from corridors). |
amount |
yes | The local-currency amount the customer pays. Currency is derived from country. |
method |
no | bank or momo. Defaults to the corridor’s default (bank for NG, momo for KE/UG). |
customer_name |
bank, momo | The paying customer’s name. |
network / phone_number |
momo | Mobile-money network + the customer’s phone number. |
customer_id |
recommended | The customer this is for. |
reference |
recommended | Your own reference for reconciliation. |
Response — bank transfer
For a bank corridor, we give a temporary account in bank_info. The
customer sends the money to that account. The amount field shows the USD
that we will add to your balance.
{
"code": 201,
"message": "Collection initiated",
"data": {
"id": "txn_…",
"type": "deposit",
"status": "pending",
"amount": 32.5,
"currency": "USD",
"fee": 0,
"reference": "invoice_8842",
"description": "NGN Bank Deposit",
"created_at": "2026-06-25T08:44:25.000Z",
"bank_info": {
"bank_name": "Providus Bank",
"account_name": "Figo / Ada Obi",
"account_number": "9900112233"
},
"timeline": [
{
"stage": "requested",
"message": "We received your request",
"status": "completed",
"occurred_at": "2026-06-25T08:44:25.000Z"
}
]
}
}
Response — mobile money
For mobile-money corridors the customer approves a push prompt on their phone;
bank_info is null. Poll the transaction (or use webhooks) for progress.
Response — Latin America
Argentina, Brazil, Colombia and Mexico are bank corridors, and answer in the
same bank_info shape — but what fills it differs by country, because the rails
do:
| Country | What the customer is given |
|---|---|
Brazil (BR) |
A Pix code to paste into their banking app |
Mexico (MX) |
A CLABE to send to over SPEI |
Colombia (CO) |
Local bank details, or a link to pay through PSE |
Argentina (AR) |
A CVU to transfer to |
Show the customer whatever bank_info carries. Some of these are standing
instruments — Colombia's link opens a checkout the customer can pay any amount
through, Brazil's Pix code pastes into a banking app — so they may be reused for
that customer rather than fetched again per payment.
Crypto collection
Accept a digital asset and get credited USD. First list what customers can pay
with and pick a network’s asset_id:
GET /v1/collections/assets
{
"code": 200,
"message": "Collection assets fetched successfully",
"data": [
{
"symbol": "USDC",
"asset_logo": "https://cdn.jsdelivr.net/…/USDC.png",
"preferred": true,
"chains": [
{
"asset_id": "usdc-polygon",
"chain": "pol",
"chain_name": "Polygon",
"chain_logo": "https://…/POL.png",
"decimals": 6,
"price": 0.9999,
"contract_address": "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359",
"direct": true,
"can_deposit": true,
"can_payout": true,
"checked_at": "2026-10-05T09:30:00.000Z"
},
{
"asset_id": "usdc-solana",
"chain": "sol",
"chain_name": "Solana",
"chain_logo": "https://…/SOL.png",
"decimals": 6,
"price": 0.9999,
"contract_address": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"direct": false,
"can_deposit": true,
"can_payout": true,
"checked_at": "2026-10-05T09:30:00.000Z"
}
]
}
]
}
Same shape on all three assets listings. chain is a short code, not the
display name. preferred marks the assets we suggest first, and direct: true
means we send the asset as-is rather than bridging it.
can_deposit says whether that corridor actually priced the last time we
asked, which is the field that matters on a collection — a listed asset is not
the same as a priceable one. Absent is not false: it is missing when we
have not probed that corridor yet, so filter on === false rather than on
falsiness. See
payouts for the full field
notes.
Then create the collection. We give a deposit address for the customer to use. We also give a memo if the network needs one. The funds settle directly to your balance.
curl -X POST {BASE_URL}/v1/collections \
-H "x-api-key: sk_test_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"method": "crypto",
"amount": 100,
"asset_id": "usdt-polygon",
"refund_to": "0xCustomerSendingAddress",
"reference": "invoice_8842"
}'
| Field | Required | Description |
|---|---|---|
method |
yes | crypto. |
amount |
yes | Amount of the chosen asset the customer sends. |
asset_id |
yes | A network’s asset_id from GET /v1/collections/assets. |
refund_to |
yes | An address the customer can receive at, used if the collection can’t be completed. Not necessarily where they send from — a payment from an exchange cannot be refunded to its sending address. |
reference |
recommended | Your own reference for reconciliation. |
{
"code": 201,
"message": "Collection initiated",
"data": {
"method": "crypto",
"direct": false,
"pay_to": {
"address": "G… (or 0x…/T…)",
"memo": "12345",
"asset_id": "usdt-polygon",
"symbol": "USDT",
"network": "tron",
"amount": 100,
"expires_at": "2026-06-25T10:30:00.000Z"
},
"amount_usd": 99.5
}
}
The customer sends the asset to pay_to.address. If we give a memo, the
customer must send it with the asset. After the asset settles, we add USD to
your balance.
Is the address one-time or permanent? Read direct
This is the field to branch on, and getting it wrong loses money.
direct: false |
direct: true |
|
|---|---|---|
| The address | One-time. Issued for this collection only. | Permanent. Your own settlement address, shared by every payment. |
expires_at |
Set. Do not accept payment after it. | Absent — it never expires. |
| Reuse it later? | No. Never store it or show it twice. | Yes. |
| Attribution | This collection's status tracks the payment. | None. We cannot tell this payment from any other. |
refund_to |
Required. | Not used. |
| Conversion | The asset is converted; amount_usd is net of fees. |
1:1, no fee. |
You get direct: true only when the asset the customer is sending is already
the asset we settle in, so there is nothing to convert and no temporary address
to issue.
A one-time address is exactly that. Funds sent to it after expires_at, or
sent to it a second time, are not credited and are not automatically returned —
recovering them is a manual support case. If a customer needs to pay again,
create a new collection and use the new pay_to.address.
Don't cache the address against your order, don't put it in an email the
customer may open days later, and don't render a QR code that outlives
expires_at. Re-read it from the collection each time you show it.
A permanent address has no status to poll. Because every payment lands on
the same address, nothing distinguishes one from another, so a direct: true
collection is never marked paid and no collection.completed fires for it. If
you need to know that this invoice was paid, you need a one-time address —
which is why a USD-denominated collection (a payment link) refuses a permanent
one unless you pass allow_permanent_address: true to say you have no
attribution to lose.
Not in sandbox. A crypto collection cannot be completed with test keys —
sending an asset to pay_to.address does nothing, because the conversion behind
a crypto pay-in runs on mainnet only. The quote and the instructions work; the
money cannot arrive, and no webhook fires. Use the settle endpoint below
instead.
Testing a collection in sandbox
Nothing arrives on its own with test keys — there is no payer at a bank, and paying the address does not work. You decide when a collection lands:
POST /v1/sandbox/collections/{reference}/settle
{ "outcome": "success" }
{
"code": 200,
"message": "Collection settled",
"data": { "reference": "invoice_8842", "status": "success" }
}
A collection that has already finished cannot be settled as the other outcome.
That returns 400 rather than reporting success while leaving it failed.
Everything downstream is real: your balance moves, collection.completed fires
at your webhook, and a payment link records the payment. Send
{"outcome": "failed"} to rehearse a payment that never arrives — worth doing
at least once.
This is the only way to complete a collection in sandbox, whichever method you opened it with. See getting started for the rest of what sandbox does and does not do.
Lifecycle
A collection moves through four stages: wait for payment, payment received,
credit to your balance, and settled. The timeline field shows the current
stage. See transaction lifecycle.