API Reference
Crypto Payments API
Accept USDT, USDC, BTC, ETH, SOL and more on 15 networks. Each payment gets its own deposit address and is forwarded to your wallet in the same coin.
Enabled per account
Direct crypto is switched on per account.GET /api/v1/crypto/currencies returns "crypto_enabled": true when your key can use it. If it is false, ask support to enable it.How it works#
- 1
Create the payment
Your server calls
POST /api/v1/crypto/paymentswith the order amount in fiat, the coin and your payout wallet. PayLio fixes the coin amount at the current price and returns a unique deposit address. - 2
The customer pays
Redirect to the hosted
checkout_url, or showdeposit_addressandcrypto_amountin your own UI. The customer has 60 minutes: the price is fixed untilexpires_at, and the hosted checkout shows a countdown. - 3
PayLio detects the deposit
Once the address holds the quoted amount, the payment becomes
paidand you receive thepayment.paidwebhook. Detection runs every minute and counts confirmed deposits. - 4
Funds are forwarded
The amount minus the crypto fee is sent to your
addressin the same coin on the same network. You receivepayment.forwardedwith the transaction hash.
All requests need an API key, see Authentication. Find valid coins with Coins & Quotes.
Create a crypto payment#
https://paylio.org/api/v1/crypto/paymentsSend a JSON body (form-encoded bodies are accepted too). A successful call returns 201 Created.
Body parameters#
coinstringrequiredchain/token, e.g. trc20/usdt, polygon/usdc, btc/btc. Native coins may be shortened to the chain (btc, sol). Instead of coin you can send chain and token separately. Case does not matter. See the coin list.amountstring | numberrequiredcurrency, 0.01 to 10000 with at most two decimals. It must also be at least the coin's minimum.currencystringUSD (default), EUR, CAD, INR or GBP.addressstringrequiredcallbackstringrequiredpass_fee_to_customerbooleantrue, the amount is raised so you receive the full amount after the crypto fee. Also accepted as passFeeToCustomer.emailstringnotestring[[plio_meta:.Example#
curl -X POST https://paylio.org/api/v1/crypto/payments \
-H "Authorization: Bearer plio_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"coin": "trc20/usdt",
"amount": "49.99",
"currency": "EUR",
"address": "TYourTronPayoutWallet",
"callback": "https://example.com/paylio/webhook?order=1042",
"note": "Order #1042"
}'201 Created
{
"object": "crypto_payment",
"payment_id": "8d1f0c52-6a3e-4f1b-9b7d-2c4e5a6f7081",
"status": "unpaid",
"forward_status": "pending",
"amount": "49.99",
"original_amount": null,
"customer_fee_amount": null,
"pass_fee_to_customer": false,
"currency": "EUR",
"fee_percent": 1.5,
"coin": "trc20/usdt",
"chain": "trc20",
"chain_name": "Tron",
"evm_chain_id": null,
"token": "usdt",
"token_name": "USDT (TRC-20)",
"contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
"decimals": 6,
"deposit_address": "TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"deposit_address_url": "https://tronscan.org/#/address/TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE",
"crypto_amount": "54.12",
"received_amount": null,
"remaining_amount": "54.12",
"payment_uri": null,
"payout_address": "TYourTronPayoutWallet",
"forwarded_amount": null,
"payout_txid": null,
"payout_tx_url": null,
"checkout_url": "https://paylio.org/pay/8d1f0c52-6a3e-4f1b-9b7d-2c4e5a6f7081",
"short_url": "https://paylio.org/p/Xk2m9QpA",
"note": "Order #1042",
"created_at": "2026-10-24T09:12:44.000Z",
"expires_at": "2026-10-24T10:12:44.000Z",
"paid_at": null,
"ipn_token": "c0a8f1e2-7b3d-4e9a-8f6c-0d1e2f3a4b5c",
"callback_url": "https://example.com/paylio/webhook?order=1042"
}The payment object#
Create and Retrieve return the same object. Create adds ipn_token and callback_url.
payment_idstringipn_tokenstringstatusstringforward_statusstringpending, ready, processing, completed, failed or manual_required.amount / currencystringpass_fee_to_customer is true; original_amount and customer_fee_amount then show the split.fee_percentnumbercoin / chain / tokenstringtrc20/usdt) and its parts. chain_name and token_name are display names.contract / decimals / evm_chain_idstring | number | nullnull for native coins), token decimals and the EVM chain ID (null outside EVM).deposit_addressstringcrypto_amountstringreceived_amountstring | nullremaining_amountstring | nullunpaid, otherwise null.payment_uristring | nullnull for coins without a common format (Tron, Bitcoin Cash); encode deposit_address instead.payout_address / forwarded_amount / payout_txidstring | nullpayout_tx_url links to the explorer.checkout_url / short_urlstringcreated_at / expires_at / paid_atstring | nullexpires_at is 60 minutes after creation.Collect the payment#
| Option | When to use it |
|---|---|
| Hosted checkout | Redirect to checkout_url. PayLio shows the address, amount, QR code and status. Nothing else to build. |
| Your own UI | Show deposit_address, crypto_amount, chain_name and a QR code from payment_uri. Poll Retrieve or wait for the webhook. |
// Render your own payment screen from the create response
const qrValue = payment.payment_uri ?? payment.deposit_address;
showPaymentScreen({
amount: `${payment.crypto_amount} ${payment.token_name}`, // "54.12 USDT (TRC-20)"
network: payment.chain_name, // "Tron"
address: payment.deposit_address,
qr: qrValue, // encode with any QR library
expires: new Date(payment.expires_at),
});
// Then poll GET /crypto/payments/{id} every 15–30 s while the screen is open,
// or simply wait for the payment.paid webhook.Tell the customer the network
The customer must send exactly this token on exactly this network. USDT on Ethereum sent to a Tron address, for example, cannot be detected. Always showchain_name next to the amount.Retrieve a crypto payment#
https://paylio.org/api/v1/crypto/payments/{payment_id}Returns the payment object for a crypto payment of your account, or 404. Use it to confirm a webhook before you fulfil the order and to reconcile open orders.
curl https://paylio.org/api/v1/crypto/payments/8d1f0c52-6a3e-4f1b-9b7d-2c4e5a6f7081 \
-H "Authorization: Bearer plio_live_YOUR_KEY"Lifecycle#
| status | forward_status | Meaning |
|---|---|---|
unpaid | pending | Waiting for the deposit, at most 60 minutes. Part of the amount may already be there (received_amount). |
expired | pending | The 60 minutes passed without a deposit. Create a new payment for the customer. |
paid | ready | The deposit covers the quote. Forwarding is queued. Fulfil the order now. |
paid | processing | The payout transaction is being sent. |
paid | completed | The payout reached your wallet. payout_txid is set. |
paid | failed | The payout will be retried automatically, e.g. after high network fees. |
unpaid | manual_required | Only part of the amount arrived, or a volatile coin arrived very late. PayLio reviews it; contact support. |
canceled | pending | Closed 25 hours after creation without a deposit. The address is no longer watched. |
- A payment counts as
paidwhen at least 99% ofcrypto_amounthas arrived, so a small exchange withdrawal fee does not block it. Several smaller transfers add up. paidmeans the money is yours: you can ship as soon as you see it. Forwarding only moves it to your wallet.- Late transfers still count. After
expires_atthe address is watched for another 24 hours, so a transfer sent just before the deadline, or a Bitcoin transfer that confirms slowly, still turns anexpiredpayment intopaidand sends the webhook. If you cancel the order on expiry, be ready to refund such a late payment or fulfil it anyway. - Stablecoins settle at the quote for the whole 24 hours. Other coins (BTC, ETH, SOL…) that arrive more than 2 hours after
expires_atgo to manual review, because the price may have moved. - Deposits that arrive after a payment was canceled are not settled automatically. Contact support.
Webhooks#
Crypto payments use the same delivery and signature as all webhooks: a POST to your callback with X-PayLio-Signature, the hex HMAC-SHA256 of the raw body keyed with the payment's ipn_token.
{
"event": "payment.paid",
"id": "8d1f0c52-6a3e-4f1b-9b7d-2c4e5a6f7081",
"paymentId": "8d1f0c52-6a3e-4f1b-9b7d-2c4e5a6f7081",
"status": "paid",
"amount": "49.99",
"currency": "EUR",
"coin": "trc20/usdt",
"receivedAmount": "54.12",
"note": "Order #1042",
"timestamp": 1793351890
}{
"event": "payment.forwarded",
"paymentId": "8d1f0c52-6a3e-4f1b-9b7d-2c4e5a6f7081",
"forwardTxid": "5c1e9f0b7a…",
"amountForwarded": "51.81",
"toWallet": "TYourTronPayoutWallet",
"timestamp": 1793351954
}- Verify the signature with the stored
ipn_token. - Confirm with Retrieve that
statusispaid. - Fulfil the order once, even if an event arrives twice. Respond
200within 10 seconds.
Webhooks are sent once
If your endpoint is unreachable the event is not repeated. Poll Retrieve for open orders, for example every few minutes until the payment ispaid or expired.Payout address formats#
| Network | Expected format |
|---|---|
| EVM networks | 0x… address. Mixed-case addresses must have a valid checksum. |
| Bitcoin, Litecoin, Bitcoin Cash, Dogecoin | A standard address for that chain. |
| Solana | A regular wallet address (base58). Token accounts and program addresses are rejected. |
| Tron | Starts with T, 34 characters, valid checksum. |
An address that does not fit the network is rejected with invalid_address. Double-check it: crypto transfers cannot be reversed.
Fees and network costs#
- The crypto fee (
fee_percent, 1.5% by default) is deducted from the forward. Withpass_fee_to_customerthe customer pays it instead. - The network fee of your payout is deducted from it, in the same coin, on every network. It is the actual cost of the transfer to your wallet at that moment, for example a few cents on Polygon or Base and a few USDT on Tron.
- Ethereum and other EVM networks have a live minimum: a payment must be at least 10 times the current network fee, so the fee never eats a large part of it. When gas is expensive, creating a smaller payment fails with
below_minimum, and the message names the current minimum. Check it beforehand with the quote endpoint (min_amount_fiat,network_fee_estimate). - On Bitcoin, Litecoin, Bitcoin Cash and Dogecoin the forward waits while the network fee would exceed 10% of the amount, and is sent once fees drop.
Errors#
| Status | code | Meaning |
|---|---|---|
400 | validation_error | A parameter is missing or invalid. error says which. |
400 | invalid_coin | The coin is unknown. Check Coins & Quotes. |
400 | invalid_address | The payout address does not fit the network. |
400 | below_minimum | The amount is below the coin's minimum. error names it in your currency. |
401 | invalid_api_key | Missing, invalid or revoked API key, a key used from an IP it is not allowed for, or a blocked account. |
403 | crypto_disabled | Direct crypto is not enabled for your account. |
403 | payout_address_not_approved | The payout address is not an approved payout wallet. Approve it in the dashboard. |
404 | not_found | No crypto payment with that ID on your account. |
429 | rate_limited | Too many requests. Wait for Retry-After seconds. |
429 | too_many_open_payments | 100 payments of your account are already waiting inside their 60 minutes. Retry when some are paid or expired. |
503 | quote_unavailable | No current price for the coin. Retry shortly or offer another coin. |
Rate limits: 60 creates and 120 retrieves per minute per API key, and 60 creates per minute for the whole account across all keys. See Errors & Rate Limits.
Testing
There is no sandbox for crypto. Test with one real payment slightly above the coin's minimum, for example USDC on Polygon or Base, where network fees are a few cents.