PayLioDocs

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. 1

    Create the payment

    Your server calls POST /api/v1/crypto/payments with 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. 2

    The customer pays

    Redirect to the hosted checkout_url, or show deposit_address and crypto_amount in your own UI. The customer has 60 minutes: the price is fixed until expires_at, and the hosted checkout shows a countdown.

  3. 3

    PayLio detects the deposit

    Once the address holds the quoted amount, the payment becomes paid and you receive the payment.paid webhook. Detection runs every minute and counts confirmed deposits.

  4. 4

    Funds are forwarded

    The amount minus the crypto fee is sent to your address in the same coin on the same network. You receive payment.forwarded with the transaction hash.

All requests need an API key, see Authentication. Find valid coins with Coins & Quotes.

Create a crypto payment#

POSThttps://paylio.org/api/v1/crypto/payments

Send a JSON body (form-encoded bodies are accepted too). A successful call returns 201 Created.

Body parameters#

coinstringrequired
Coin as chain/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 | numberrequired
Order amount in currency, 0.01 to 10000 with at most two decimals. It must also be at least the coin's minimum.
currencystring
USD (default), EUR, CAD, INR or GBP.
addressstringrequired
Your payout wallet on the selected network. The forward is sent here in the same coin. It must be an approved payout wallet. See address formats.
callbackstringrequired
HTTPS URL that receives the webhooks. Must be publicly reachable; localhost, private IPs and URLs with credentials are rejected. Max 2048 characters.
pass_fee_to_customerboolean
If true, the amount is raised so you receive the full amount after the crypto fee. Also accepted as passFeeToCustomer.
emailstring
Customer email. Pre-fills the hosted checkout.
notestring
Up to 500 characters, e.g. your order number. Shown in your dashboard and included in webhooks. Do not put private data here. Must not start with [[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

Response · JSON
{
  "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_idstring
Unique payment ID. Store it on your order.
ipn_tokenstring
Create only. Secret that signs the webhooks for this payment. Store it on your order and never show it to the customer.
statusstring
unpaid, expired, paid or canceled. See lifecycle.
forward_statusstring
pending, ready, processing, completed, failed or manual_required.
amount / currencystring
Fiat amount of the order. Includes the fee when pass_fee_to_customer is true; original_amount and customer_fee_amount then show the split.
fee_percentnumber
Crypto fee of your account, in percent.
coin / chain / tokenstring
The coin (trc20/usdt) and its parts. chain_name and token_name are display names.
contract / decimals / evm_chain_idstring | number | null
Token contract (null for native coins), token decimals and the EVM chain ID (null outside EVM).
deposit_addressstring
The address the customer pays to. Unique to this payment.
crypto_amountstring
The exact coin amount the customer must send. Fixed at creation.
received_amountstring | null
Coins seen on the deposit address so far.
remaining_amountstring | null
What is still missing while the payment is unpaid, otherwise null.
payment_uristring | null
Wallet URI with address and amount (BIP-21, EIP-681 or Solana Pay) for a QR code. null for coins without a common format (Tron, Bitcoin Cash); encode deposit_address instead.
payout_address / forwarded_amount / payout_txidstring | null
Your wallet, the amount sent to it and the transaction hash. payout_tx_url links to the explorer.
checkout_url / short_urlstring
Hosted checkout for the customer.
created_at / expires_at / paid_atstring | null
ISO 8601 timestamps. expires_at is 60 minutes after creation.

Collect the payment#

OptionWhen to use it
Hosted checkoutRedirect to checkout_url. PayLio shows the address, amount, QR code and status. Nothing else to build.
Your own UIShow deposit_address, crypto_amount, chain_name and a QR code from payment_uri. Poll Retrieve or wait for the webhook.
Own UI · JavaScript
// 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 show chain_name next to the amount.

Retrieve a crypto payment#

GEThttps://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#

statusforward_statusMeaning
unpaidpendingWaiting for the deposit, at most 60 minutes. Part of the amount may already be there (received_amount).
expiredpendingThe 60 minutes passed without a deposit. Create a new payment for the customer.
paidreadyThe deposit covers the quote. Forwarding is queued. Fulfil the order now.
paidprocessingThe payout transaction is being sent.
paidcompletedThe payout reached your wallet. payout_txid is set.
paidfailedThe payout will be retried automatically, e.g. after high network fees.
unpaidmanual_requiredOnly part of the amount arrived, or a volatile coin arrived very late. PayLio reviews it; contact support.
canceledpendingClosed 25 hours after creation without a deposit. The address is no longer watched.
  • A payment counts as paid when at least 99% of crypto_amount has arrived, so a small exchange withdrawal fee does not block it. Several smaller transfers add up.
  • paid means 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_at the address is watched for another 24 hours, so a transfer sent just before the deadline, or a Bitcoin transfer that confirms slowly, still turns an expired payment into paid and 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_at go 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.

payment.paid
{
  "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
}
payment.forwarded
{
  "event":           "payment.forwarded",
  "paymentId":       "8d1f0c52-6a3e-4f1b-9b7d-2c4e5a6f7081",
  "forwardTxid":     "5c1e9f0b7a…",
  "amountForwarded": "51.81",
  "toWallet":        "TYourTronPayoutWallet",
  "timestamp":       1793351954
}
  1. Verify the signature with the stored ipn_token.
  2. Confirm with Retrieve that status is paid.
  3. Fulfil the order once, even if an event arrives twice. Respond 200 within 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 is paid or expired.

Payout address formats#

NetworkExpected format
EVM networks0x… address. Mixed-case addresses must have a valid checksum.
Bitcoin, Litecoin, Bitcoin Cash, DogecoinA standard address for that chain.
SolanaA regular wallet address (base58). Token accounts and program addresses are rejected.
TronStarts 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. With pass_fee_to_customer the 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#

StatuscodeMeaning
400validation_errorA parameter is missing or invalid. error says which.
400invalid_coinThe coin is unknown. Check Coins & Quotes.
400invalid_addressThe payout address does not fit the network.
400below_minimumThe amount is below the coin's minimum. error names it in your currency.
401invalid_api_keyMissing, invalid or revoked API key, a key used from an IP it is not allowed for, or a blocked account.
403crypto_disabledDirect crypto is not enabled for your account.
403payout_address_not_approvedThe payout address is not an approved payout wallet. Approve it in the dashboard.
404not_foundNo crypto payment with that ID on your account.
429rate_limitedToo many requests. Wait for Retry-After seconds.
429too_many_open_payments100 payments of your account are already waiting inside their 60 minutes. Retry when some are paid or expired.
503quote_unavailableNo 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.