Skip to content

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.

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.

POST /v1/checkout_links → chl_… (as for any other method)
GET /v1/checkout_links/{id}/crypto_assets → which coins, and how much of each
POST /v1/checkout_links/{id}/crypto → deposit address + amount to send
GET /v1/checkout_links/{id} → poll this for the outcome
Terminal window
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.

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

Two rules, and the second is the one integrations get wrong:

  • Show deposit_address verbatim, 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 network next to it, always. A deposit address is chain-specific. A donor who sends USD₮ on a chain other than network has 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.

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

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.

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.

ErrorWhat it means
crypto_not_availableThe cause is not enabled for stablecoins, or the coin is not accepted on that chain. Offer card or PSE.
crypto_amount_out_of_rangeThe 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_payableThe link is already completed, expired or cancelled. Mint a new one.
CoinChainPegged to
USD₮PolygonUSD
USDCPolygonUSD
COPMPolygonCOP

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.