CleonPay

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.

Enable it first. Crypto is off until an operator switches it on for your account, network by network; calls return 403 crypto_not_enabled until then.

Amounts are the token amount, not fiat

The 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

POST /v1/crypto/payments
FieldTypeNotes
chainstringRequired. ethereum, base or tron.
assetstringRequired. USDC (ethereum, base) or USDT (tron).
amountstringRequired. Token amount, e.g. "25.00".
order_referencestringYour order id. Searchable.
hold_minutesintegerHow long the address is held (1–1440; default 30).
metadataobjectYour 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

POST /v1/crypto/payment_sessions

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

GET /v1/crypto/payments/{id}

Adds received_amount, confirmations, confirmations_required and tx_hash (the on-chain deposit) as they appear.

GET /v1/crypto/payments

Lists your crypto payments newest first. Accepts status, reference, limit and created_before for keyset pagination — the same shape as GET /v1/payments.

POST /v1/crypto/payments/{id}/cancel

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:

POST /v1/crypto/payments/{id}/simulate

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

StatusMeaning
pendingWaiting for a deposit
detected / confirmingDeposit seen, gaining confirmations
paidConfirmed in full. Fulfil here.
underpaidConfirmed, but less than due
heldHeld for review — the sender failed sanctions screening
swept / settledPaid and collected by CleonPay. Nothing further happens on the payment; treat exactly like paid
expiredNo 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.