Accept stablecoins
A stablecoin donation is a checkout link paid a different way. You mint the link exactly as you would for a card, then ask for a deposit address instead of a payment form.
There is no wallet connection, no signature request and no SDK. The donor sends a transfer from whatever wallet or exchange they already use, and Vaki watches for it.
The one thing that is genuinely different
Section titled “The one thing that is genuinely different”A transfer cannot be reversed. There is no chargeback, no void and no refund rail. Everything below that looks like extra care — the amount never coming from your request, the address and network always shown together, the insistence that you poll the link rather than trust the response — is that single fact working its way through the design.
The flow
Section titled “The flow”POST /v1/checkout_links → chl_… (as for any other method)GET /v1/checkout_links/{id}/crypto_assets → which coins, and how much of eachPOST /v1/checkout_links/{id}/crypto → deposit address + amount to sendGET /v1/checkout_links/{id} → poll this for the outcome1. Ask what this link can take
Section titled “1. Ask what this link can take”curl https://api.vaki.cohttps://public-api-staging.vaki.co/v1/checkout_links/chl_8fK2xQ/crypto_assets{ "assets": [ { "asset": "USDT", "network": "polygon", "amount": 12.690356, "minimum": 1, "maximum": 1000000 }, { "asset": "USDC", "network": "polygon", "amount": 12.690356, "minimum": 1, "maximum": 1000000 }, { "asset": "COPM", "network": "polygon", "amount": 50000, "minimum": 4000, "maximum": 4000000000 } ]}Call this before you render anything. Stablecoins are opt-in per cause, each coin lives on its own set of chains, and each has its own floor and ceiling in its own units. This endpoint applies all three rules at once, so an option it returns is one the next call will accept.
An empty array is a normal answer. It means the cause is not enabled for stablecoins, or the link’s amount cannot be represented in any accepted coin — COP 3.000 is below every floor. Render no crypto option and do not retry.
2. Open the donation
Section titled “2. Open the donation”curl -X POST https://api.vaki.cohttps://public-api-staging.vaki.co/v1/checkout_links/chl_8fK2xQ/crypto \ -H 'Content-Type: application/json' \ -d '{"asset":"USDT","network":"polygon","email":"donante@example.com"}'{ "object": "crypto_intent", "id": "9xKp2mQr7sT", "deposit_address": "0x3f8a…", "asset": "USDT", "network": "polygon", "amount": 12.690356, "quote": { "currency": "COP", "official": 4000, "spread_bps": 150, "applied": 3940, "target_value": 50000, "quoted_at": "2026-09-21T14:03:11.000Z" }, "expires_at": "2026-09-21T15:03:11.000Z", "created_at": "2026-09-21T14:03:11.000Z"}There is no amount field in the request, and sending one is a 400 rather
than a value we quietly ignore. The figure is solved from the amount on the
stored link and the live rate. This endpoint is unauthenticated — the chl_…
id is the capability, so that your checkout page can call it from the browser —
which is exactly why nothing in the body is allowed to decide money.
3. Show the donor the address
Section titled “3. Show the donor the address”Two rules, and the second is the one integrations get wrong:
- Show
deposit_addressverbatim, and let the donor copy it. Never re-type it, never truncate it inside a copyable field, and if you render a QR, build it from this string. - Show
networknext to it, always. A deposit address is chain-specific. A donor who sends USD₮ on a chain other thannetworkhas sent it to an address nobody is watching.
The address is a contract derived from id, controlled by Vaki. It has no
private key and no outbound path except Vaki’s treasury — which is why it is
safe to publish it in a page.
4. Poll the link, not this
Section titled “4. Poll the link, not this”curl https://api.vaki.cohttps://public-api-staging.vaki.co/v1/checkout_links/chl_8fK2xQ \ -H 'Authorization: Bearer sk_live_…'GET /v1/checkout_links/{id} is the only endpoint that answers “was this
paid” — its status is derived from the payment document itself. The response
in step 2 says what the donor was asked to do, and nothing about what they
did.
Expect to poll for longer than you would for a card. A transfer is confirmed on-chain and then converted: Polygon needs 128 confirmations, and the cause’s balance moves when the conversion settles, minutes later. A donor staring at your page has already seen their wallet say “sent”; tell them Vaki is waiting for the network rather than leaving a spinner running.
What the cause actually receives
Section titled “What the cause actually receives”amount is an instruction to the donor, not a promise to the cause.
The cause is credited from what actually arrives, at the rate in force when
the transfer confirms — not from the figure you were quoted. A donor who sends a
different amount, or who sends twenty minutes later while the market moves, has
still donated; the credited total simply differs a little from
quote.target_value.
The quote object is there so that difference can be explained rather than
argued about. official is the mid-market rate, spread_bps is Vaki’s spread
on the conversion, and applied is the rate the figure was solved at.
spread_bps is 0 for a coin already pegged to the cause’s currency — COPM
into a Colombian cause is not converted at all, so there is nothing to price.
Repeat calls are safe
Section titled “Repeat calls are safe”Calling POST .../crypto twice for the same link, coin and chain returns the
same address and the same figure. A reload or a double-tap cannot show a
donor two different amounts, and it cannot hand out a second address while money
may already be on its way to the first.
Asking for a different coin or chain opens a genuinely new donation — a donor who picked the wrong coin is never locked out.
After an hour the quote goes stale and the next call issues a fresh figure. The address itself does not expire. A donor who sends tomorrow has still donated and is still credited to the same cause.
If your page was reloaded and you need the address back without opening
anything, GET /v1/checkout_links/{id}/crypto returns the open donation.
When it will not work
Section titled “When it will not work”| Error | What it means |
|---|---|
crypto_not_available | The cause is not enabled for stablecoins, or the coin is not accepted on that chain. Offer card or PSE. |
crypto_amount_out_of_range | The link’s amount converts outside the coin’s bounds. minimum, maximum and asset are on the error, in units of the coin. |
checkout_link_not_payable | The link is already completed, expired or cancelled. Mint a new one. |
Chains and coins available today
Section titled “Chains and coins available today”| Coin | Chain | Pegged to |
|---|---|---|
| USD₮ | Polygon | USD |
| USDC | Polygon | USD |
| COPM | Polygon | COP |
Polygon only, deliberately: a deposit address is only usable on a chain where
Vaki’s deposit factory is deployed. Read the live list from
GET /v1/checkout_links/{id}/crypto_assets rather than hardcoding this table —
it is the one that will be right when this page is not.