Crypto payments
Accept stablecoins with the same API as everything else.
Accept USDC on Ethereum and Base and USDT on TRON. Same API keys, same webhook engine, same idempotency as the rest of CleonPay. CleonPay collects every deposit — you never hold custody or touch a wallet in your integration.
403 crypto_not_enabled until then.Amounts are the token amount, not fiat
amount is the number of tokens to
collect: "25.00" means 25 USDC/USDT. Stablecoins are USD-pegged
and CleonPay does no FX, so read one token as one US dollar. There is no
minimum or maximum.Create a payment
| Field | Type | Notes |
|---|---|---|
chain | string | Required. ethereum, base or tron. |
asset | string | Required. USDC (ethereum, base) or USDT (tron). |
amount | string | Required. Token amount, e.g. "25.00". |
order_reference | string | Your order id. Searchable. |
hold_minutes | integer | How long the address is held (1–1440; default 30). |
metadata | object | Your own key/values, returned untouched on every read. |
Send an Idempotency-Key header so a retried create returns the
same payment and deposit address rather than reserving a new one. See
Idempotency.
curl https://api.cleonpay.com/v1/crypto/payments \
-H "Authorization: Bearer $CLEONPAY_KEY" \
-H "Idempotency-Key: order-8891" \
-H "Content-Type: application/json" \
-d '{ "chain": "base", "asset": "USDC", "amount": "25.00", "order_reference": "order-8891" }'
{
"id": "9b1c0f2e-…",
"object": "crypto_payment",
"status": "pending",
"chain": "base",
"asset": "USDC",
"network": "Base network",
"deposit_address": "0xea28…8836",
"amount": "25.00",
"amount_base": "25000000",
"order_reference": "order-8891",
"expires_at": "2026-09-21T12: 30: 00.000Z",
"created_at": "2026-09-21T12: 00: 00.000Z",
"qr": "data:image/png;base64,…",
"checkout_url": "https://pay.cleonpay.com/crypto/checkout/9b1c0f2e-…"
}
Show the customer the deposit_address and amount,
or send them to checkout_url — a hosted page with the QR, the
exact amount and live confirmation status.
Let the payer choose the network
Same body as a payment but without chain/asset.
Returns deposit_options — one payment (its own id,
checkout_url and webhooks) per supported network, sharing your
order_reference. Show them all, or a picker; whichever address is
paid fires that payment's crypto.payment.received.
Reading a payment
Adds received_amount, confirmations,
confirmations_required and tx_hash (the on-chain
deposit) as they appear.
Lists your crypto payments newest first. Accepts status,
reference, limit and created_before
for keyset pagination — the same shape as GET /v1/payments.
Cancels a still-pending payment the customer never funded.
Once a deposit is seen it can no longer be cancelled.
Testing in the sandbox
With a test key, drive a payment through to
paid without sending real testnet coin:
Runs the exact ingest path a real deposit would — the status transition,
ledger credit and crypto.payment.received webhook are all real.
Pass { "amount": "10.00" } (less than due) to test
underpaid instead. Live keys are rejected.
curl -X POST https://api.cleonpay.com/v1/crypto/payments/9b1c…/simulate \
-H "Authorization: Bearer $CLEONPAY_TEST_KEY"
Statuses
| Status | Meaning |
|---|---|
pending | Waiting for a deposit |
detected / confirming | Deposit seen, gaining confirmations |
paid | Confirmed in full. Fulfil here. |
underpaid | Confirmed, but less than due |
held | Held for review — the sender failed sanctions screening |
swept / settled | Paid and collected by CleonPay. Nothing further happens on the payment; treat exactly like paid |
expired | No deposit before the window closed |
Sanctions screening
Every deposit's source address is screened against sanctions lists before
it's credited. A hit puts the payment in held — never credited
or collected — and fires crypto.payment.held. Treat
held like a decline: do not fulfil. This runs on
every deposit, however the payment was created.
Webhooks
Listen for crypto.payment.received (fulfil here),
crypto.payment.underpaid, crypto.payment.held and
crypto.payment.expired. Their data carries
crypto_payment_id, not payment_id. See
Webhooks.