easyway API
Accept crypto payments in a few lines of code. One REST API, JSON in and out, and signed webhooks that tell your server when the money has arrived.
Overview
A payment on easyway is an invoice. You create one from your server with the amount your customer owes, redirect them to its payment_url, and easyway does the rest: the customer picks a coin and network, gets a one-off address (or a shared address plus a memo on TON and XRP), and the on-chain watchers credit the payment the moment it is final. Your server learns about it from a webhook — never by trusting the browser.
| Step | What happens |
|---|---|
| 1. Create | POST /v1/invoices with the amount. You get back an id and a payment_url. |
| 2. Pay | Redirect the customer to payment_url (or open it in a new tab / iframe). The page updates itself while the payment confirms. |
| 3. Confirm | You receive invoice.paid on your webhook URL. Verify the signature, then fulfil the order. (invoice.underpaid if they sent less.) |
| 4. Settle | Funds land in your easyway balance, minus the processing fee. Withdraw with POST /v1/payouts or from the dashboard — to wallets you own. |
Amounts are always decimal strings ("149.00", never 149) so nothing is lost to floating point. Ids are prefixed and unguessable: inv_… invoices, po_… payouts, evt_… webhook events. Timestamps are ISO-8601 in UTC.
Authentication
Create keys in Dashboard → Developers. A key is shown once. Send it as a bearer token:
Authorization: Bearer sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX # (X-Api-Key: sk_live_… is accepted too)
| Mode | The key decides: sk_test_… keys only ever see sandbox data, sk_live_… keys only real money. There is no mode parameter. |
| Scopes | Each key has scopes: invoices:read, invoices:write, balances:read, payouts:read, payouts:write. Give a server only what it needs — a key that can create invoices does not need to move money. |
| IP allowlist | Optional per key. With one set, requests from any other IP are refused (403 ip_not_allowed). |
| Rate limit | 300 requests per minute per key. Over it you get 429 rate_limited; the RateLimit-* headers tell you when to retry. |
| Keep it secret | Never ship a key in a browser, mobile app or public repo. Rotate it from the dashboard if it leaks — revoking is instant. |
Quickstart
Three steps: create an invoice, send the customer to it, act on the webhook.
1. Create an invoice
curl -X POST https://api.easyway.cash/v1/invoices \
-H "Authorization: Bearer $EASYWAY_API_KEY" \
-H "Idempotency-Key: order-2048" \
-H "Content-Type: application/json" \
-d '{"amount":"149.00","currency":"USD","order_id":"2048","description":"Order #2048","customer_email":"jane@example.com","success_url":"https://shop.example.com/thanks","cancel_url":"https://shop.example.com/cart","metadata":{"cart":"c_81f2"}}'Response 201:
{
"id": "inv_7Q2mN4kL8pR1sT9uV3wX0y",
"status": "pending",
"mode": "live",
"order_id": "2048",
"description": "Order #2048",
"amount": "149.00",
"currency": "USD",
"pay_asset": null, // filled once the customer picks a coin
"pay_amount": null,
"pay_address": null,
"pay_memo": null,
"pay_network": null,
"rate": null,
"received_amount": "0",
"remaining_amount": "0",
"fee_amount": "0",
"net_amount": "0",
"customer_email": "jane@example.com",
"metadata": { "cart": "c_81f2" },
"payment_url": "https://easyway.cash/pay/inv_7Q2mN4kL8pR1sT9uV3wX0y",
"expires_at": "2026-09-23T13:15:00.000Z",
"paid_at": null,
"created_at": "2026-09-23T12:15:00.000Z"
}2. Send the customer to the payment page
// Server-side redirect (Express)
res.redirect(303, invoice.payment_url);
// …or a button in your own page
<a href={invoice.payment_url}>Pay with crypto</a>After paying, the customer is offered a button back to your success_url. Don't treat that visit as proof of payment — the webhook is.
3. Fulfil on the webhook
# Webhooks are pushed to YOUR server — there is nothing to curl. # The header looks like this: # X-Webhook-Signature: t=1758470400,v1=5f1c…e2a9 # expected = HMAC_SHA256(secret, "<t>.<raw request body>") # Compare v1 with expected in constant time, and reject if |now − t| > 300 s.
sk_test_ key first: sandbox invoices are identical, and the dashboard has a Simulate payment button that fires the real webhook. See Test mode.Invoices
Create an invoice
| Field | Type | Description |
|---|---|---|
| amount | string, required | Decimal string. In currency units: "149.00" USD, or "0.005" when currency is BTC. |
| currency | string | "USD" (default) — the customer chooses the coin and the crypto amount is quoted at payment time — or a coin id such as "USDT_TRC20" to price in that coin (then only that coin is accepted). |
| order_id | string ≤128 | Your reference. Comes back on the invoice and in every webhook; searchable in the list endpoint. |
| description | string ≤500 | Shown to the customer on the payment page. |
| customer_email | Stored on the invoice for your records and support. | |
| success_url | https URL | Where the customer can return after paying. |
| cancel_url | https URL | Where the customer can return if they give up. |
| allowed_assets | string[] | Restrict which coins are offered, e.g. ["USDT_TRC20", "USDT_BEP20"]. Default: everything enabled on your account. |
| metadata | object | Up to 20 string values (≤500 chars each). Returned untouched, e.g. your cart or user id. |
| lifetime | integer 5–1440 | Minutes until the invoice expires unpaid. Default: your account setting (60). |
Pricing in a coin instead of USD:
curl -X POST https://api.easyway.cash/v1/invoices \
-H "Authorization: Bearer $EASYWAY_API_KEY" \
-H "Idempotency-Key: topup-7731" \
-H "Content-Type: application/json" \
-d '{"amount":"25","currency":"USDT_TRC20","order_id":"topup-7731","lifetime":30}'The invoice object
| Field | Type | Description |
|---|---|---|
| id | string | inv_ + 22 characters. Also the last part of payment_url. |
| status | string | pending → confirming → paid; or underpaid, expired, cancelled. See lifecycle below. |
| mode | string | live or test. |
| amount / currency | string | What you asked for, exactly as you sent it. |
| pay_asset | string | null | The coin the customer chose (e.g. USDT_TRC20); null until they choose. |
| pay_amount | string | null | How much of that coin they must send — quoted at selection and locked for the invoice's lifetime. |
| pay_address | string | null | The deposit address. Unique to this invoice on every network except TON and XRP. |
| pay_memo | string | null | When non-null, the customer must attach it (TON comment / XRP destination tag). The payment page shows it and encodes it in the QR. |
| rate | string | null | USD price of one unit of pay_asset at quote time. |
| received_amount | string | Confirmed on-chain amount so far, in pay_asset. |
| remaining_amount | string | What is still owed (0 once paid). Non-zero with status underpaid. |
| fee_amount / net_amount | string | easyway's processing fee and what was credited to your balance, in pay_asset. |
| payment_url | string | The hosted payment page for this invoice. |
| expires_at / paid_at / created_at | ISO-8601 | UTC timestamps. |
Lifecycle
| Status | Meaning | What you should do |
|---|---|---|
| pending | Created; nothing seen on-chain yet. | Nothing. Expires at expires_at if unpaid. |
| confirming | A payment was seen but is not final yet (waiting for confirmations). | Nothing — you get invoice.confirming so you can show \"payment detected\". |
| paid | Final. The amount (within your underpayment tolerance, default 1%) is on your balance. | Fulfil the order on invoice.paid. This is the only status that means money. |
| underpaid | Final, but less than the amount arrived. remaining_amount says how much is missing. | The credited part is on your balance. The customer can top up on the same page (the invoice stays open until it expires); you decide whether to fulfil partially, wait, or refund. |
| expired | Nobody paid before expires_at. | A payment that arrives after expiry is still credited and the invoice flips to paid — so keep handling invoice.paid for old ids. |
| cancelled | You cancelled it (dashboard or API) while it was pending. | Nothing. |
pay_amount, and pick a tolerance in Dashboard → Settings that suits your margins.Retrieve an invoice
curl https://api.easyway.cash/v1/invoices/inv_7Q2mN4kL8pR1sT9uV3wX0y \ -H "Authorization: Bearer $EASYWAY_API_KEY"
List invoices
| Field | Type | Description |
|---|---|---|
| page | integer | 1-based, default 1. |
| pageSize | integer | 1–100, default 20. |
| order_id | string | Only invoices with exactly this order_id. |
curl https://api.easyway.cash/v1/invoices?page=1&pageSize=50&order_id=2048 \ -H "Authorization: Bearer $EASYWAY_API_KEY"
{ "data": [ { …invoice… } ], "total": 1, "page": 1, "page_size": 50 }Cancel an invoice
curl -X POST https://api.easyway.cash/v1/invoices/inv_7Q2mN4kL8pR1sT9uV3wX0y/cancel \
-H "Authorization: Bearer $EASYWAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'Once a payment has been detected the invoice can't be cancelled (409 invoice_not_pending).
Coins & networks
Every (coin, network) pair has its own id — USDT on TRON and USDT on Ethereum are different balances, because they are different things to withdraw. Use these ids in currency, allowed_assets, pay_asset and payouts.
| id | Coin | Network | Confirmations | Min payout |
|---|---|---|---|---|
| USDT_TRC20 | USDT | TRON · TRC-20 | 19 | 0.5 USDT |
| USDT_BEP20 | USDT | BNB Smart Chain · BEP-20 | 15 | 0.5 USDT |
| USDT_ERC20 | USDT | Ethereum · ERC-20 | 12 | 0.5 USDT |
| USDT_TON | USDT | TON (memo) | 1 | 0.5 USDT |
| USDT_SOL | USDT | Solana · SPL | 1 | 0.5 USDT |
| USDC_ERC20 | USDC | Ethereum · ERC-20 | 12 | 0.5 USDC |
| USDC_BEP20 | USDC | BNB Smart Chain · BEP-20 | 15 | 0.5 USDC |
| USDC_SOL | USDC | Solana · SPL | 1 | 0.5 USDC |
| BTC | BTC | Bitcoin | 2 | 0.00001 BTC |
| ETH | ETH | Ethereum | 12 | 0.0002 ETH |
| BNB | BNB | BNB Smart Chain | 15 | 0.0007 BNB |
| TRX | TRX | TRON | 19 | 1.5 TRX |
| SOL | SOL | Solana | 1 | 0.005 SOL |
| XRP | XRP | XRP Ledger (destination tag) | 1 | 0.35 XRP |
| LTC | LTC | Litecoin | 4 | 0.008 LTC |
| DOGE | DOGE | Dogecoin | 6 | 5 DOGE |
| GRAM | GRAM | TON (memo) | 1 | 0.35 GRAM |
Minimum payouts are about half a dollar on every network. “Confirmations” is how many blocks a payment must be buried under before it is credited: seconds on Solana, TON and XRP; about a minute on TRON and BSC; a few minutes on Ethereum, Litecoin and Dogecoin; ~20 minutes on Bitcoin. On TON and XRP all payments arrive at one address and are told apart by the memo / destination tag in pay_memo.
Webhooks
Add an endpoint in Dashboard → Developers → Webhooks (one per mode), pick the events, and copy its signing secret. easyway POSTs a JSON body to it:
{
"id": "evt_5Gz2Kq8Lm1Np4Rs7Tv0Wx3",
"type": "invoice.paid",
"mode": "live",
"created": 1758630900,
"data": {
"invoice": { …the invoice object, exactly as GET /v1/invoices/:id returns it… }
}
}| Event | When |
|---|---|
| invoice.confirming | A payment was seen on-chain and is waiting for confirmations. |
| invoice.paid | The payment is final and credited. Fulfil the order. |
| invoice.underpaid | A final payment arrived, but less than the amount. data.invoice.remaining_amount says how much is missing. |
| invoice.expired | The invoice expired unpaid. |
| invoice.cancelled | The invoice was cancelled. |
| payout.sent | A withdrawal was broadcast. data.payout has the tx_hash. |
| payout.failed | A withdrawal could not be sent; the funds are back on your balance. data.payout.failure_reason says why. |
| webhook.test | Sent by the “Send test” button in the dashboard. |
Verify the signature
Every delivery carries X-Webhook-Signature: t=<unix seconds>,v1=<hex> where v1 = HMAC-SHA256(secret, t + "." + raw_body). Recompute it over the raw request body, compare in constant time, and reject anything older than five minutes. Without this check anyone who finds your URL can mark their own orders as paid.
# Webhooks are pushed to YOUR server — there is nothing to curl. # The header looks like this: # X-Webhook-Signature: t=1758470400,v1=5f1c…e2a9 # expected = HMAC_SHA256(secret, "<t>.<raw request body>") # Compare v1 with expected in constant time, and reject if |now − t| > 300 s.
Delivery rules
| Respond fast | Answer 2xx within 10 seconds, then do the work. Anything else (timeouts, 4xx, 5xx) counts as a failure. |
| Retries | Failed deliveries are retried 8 times over about 45 hours: after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h. You can also retry any delivery by hand from the dashboard, which shows every attempt, status code and error. |
| Idempotent handlers | The same event can reach you twice (a retry after a slow 200). Store event.id and skip repeats — or make your handler naturally idempotent (“set order 2048 to paid” is safe to run twice). |
| Order | Events are not guaranteed to arrive in order. Decide from the invoice's status in the payload, not from which event came last. |
| Source of truth | If in doubt, GET /v1/invoices/:id. The payload is a snapshot from the moment the event was created. |
Balances & rates
curl https://api.easyway.cash/v1/balances \ -H "Authorization: Bearer $EASYWAY_API_KEY"
{ "data": [
{ "asset": "USDT_TRC20", "available": "1240.55", "locked": "500" },
{ "asset": "BTC", "available": "0.0312", "locked": "0" }
] }available is what you can withdraw now; locked is reserved by payouts that are still in flight.
curl https://api.easyway.cash/v1/rates \ -H "Authorization: Bearer $EASYWAY_API_KEY"
{ "data": { "USDT_TRC20": "1", "BTC": "67412.18000000", "ETH": "3188.40000000", "TRX": "0.34319100", … } }USD per unit, the median of several exchanges, refreshed every minute. Stablecoins are pinned to 1. These are indicative — an invoice locks its own rate at the moment the customer picks a coin.
Payouts
A payout moves coins from your easyway balance to a wallet of yours (or anyone's). In live mode a payout can only go to an address you have whitelisted in the dashboard with 2FA — so a leaked API key can never drain your balance to a stranger. Whitelisting takes effect after a short cooldown.
| Field | Type | Description |
|---|---|---|
| asset | string, required | A coin id from Coins & networks. |
| address | string, required | Destination. Validated for that network (checksums included); in live mode it must be whitelisted. |
| memo | string ≤120 | TON comment / XRP destination tag, when the destination (an exchange) needs one. In live mode the whitelist entry's memo is used. |
| amount | string, required | In the asset's units, at most its on-chain precision, and at least the minimum payout. The network fee is charged on top. |
| note | string ≤255 | For your records; visible in the dashboard. |
curl -X POST https://api.easyway.cash/v1/payouts \
-H "Authorization: Bearer $EASYWAY_API_KEY" \
-H "Idempotency-Key: settle-2026-09-23" \
-H "Content-Type: application/json" \
-d '{"asset":"USDT_TRC20","address":"TJRabPrwbZy45sbavfcjinPJC18kjpRTv8","amount":"500","note":"Daily settlement"}'{
"id": "po_3Hd8Kq1Lm5Np7Rs9Tv2Wx4",
"status": "pending",
"mode": "live",
"asset": "USDT_TRC20",
"address": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"memo": null,
"amount": "500",
"network_fee": "1.5",
"tx_hash": null,
"failure_reason": null,
"created_at": "2026-09-23T12:20:00.000Z"
}Amount + network fee are reserved (locked) immediately. Then: pending → processing (being signed) → sent with a tx_hash, or failed with the funds back on your balance. Subscribe to payout.sent / payout.failed, or poll:
curl https://api.easyway.cash/v1/payouts/po_3Hd8Kq1Lm5Np7Rs9Tv2Wx4 \ -H "Authorization: Bearer $EASYWAY_API_KEY"
tx_hash and need no whitelist, so you can automate settlement end to end before going live.Payment links & hosted checkout
No code at all: create a payment link in Dashboard → Payment links (fixed price, or let the customer type an amount within limits) and share https://easyway.cash/pay/link/pl_…. Each visit mints a normal invoice, so your webhooks fire exactly as for API-created ones.
The hosted checkout page (payment_url) works on phones and desktops, shows a QR code that pre-fills address, amount and memo in wallet apps, polls for the payment itself, and handles under-payments by asking for the remainder. It is safe to open in an <iframe> on your domain.
Idempotency
Networks fail. If your request to create an invoice or payout times out, you can't know whether it went through — unless you sent an Idempotency-Key. Then a retry with the same key and body returns the original result instead of creating a second one. Use something that identifies the action on your side: an order id, a settlement batch id.
Idempotency-Key: order-2048 # 1–255 printable characters, unique per action
| Situation | Response |
|---|---|
| First request | Processed normally; the response is stored under the key for 24 hours. |
| Retry, same body | The stored response (same status code, same id) — nothing is created twice. |
| Retry, different body | 422 idempotency_key_reused — the key is bound to the first body. |
| Retry while the first is still running | 409 request_in_progress — wait a moment and retry. |
Errors
Errors use conventional HTTP status codes and a small JSON body. error is a stable machine-readable code — match on it, not on the message.
HTTP/1.1 400 Bad Request
{
"error": "validation_error",
"message": "validation_error",
"details": [ { "path": ["amount"], "message": "invalid_amount" } ]
}| Status | error | Meaning |
|---|---|---|
| 400 | validation_error | A field is missing or malformed; `details` lists which. |
| 400 | invalid_idempotency_key | Idempotency-Key must be 1–255 printable characters. |
| 400 | unknown_currency / unknown_asset | Not one of the ids in Coins & networks. |
| 400 | asset_not_allowed | The invoice's allowed_assets doesn't include it. |
| 400 | amount_too_small / amount_too_large | Below the coin's precision or above 1,000,000 USD. |
| 400 | invalid_address | The payout address is not valid for that network (checksum included). |
| 400 | below_minimum_payout | See the min payout column. |
| 401 | invalid_api_key | Missing, malformed, revoked, or a test key used against live data. |
| 403 | missing_scope | The key was created without the scope this endpoint needs. |
| 403 | ip_not_allowed | The key has an IP allowlist and this IP isn't on it. |
| 403 | address_not_whitelisted | Live payouts only go to addresses approved in the dashboard with 2FA. |
| 403 | payouts_temporarily_locked | Payouts are frozen for this account (e.g. 2FA was just disabled). |
| 404 | invoice_not_found / payout_not_found | Wrong id, or it belongs to another merchant or mode. |
| 409 | invoice_not_pending / invoice_expired | The invoice can no longer be changed. |
| 409 | request_in_progress | The same Idempotency-Key is still being processed — retry in a moment. |
| 422 | insufficient_funds | Available balance is below amount + network fee. |
| 422 | idempotency_key_reused | Same Idempotency-Key, different request body. |
| 429 | rate_limited | Over 300 requests per minute on this key. Back off and retry. |
| 503 | rate_unavailable | No fresh exchange rate for a non-stablecoin right now — retry shortly. |
5xx and network errors: retry with exponential back-off (1 s, 2 s, 4 s…) and the same Idempotency-Key.
Test mode & go-live
Everything exists twice: test and live. Test keys, test invoices, test webhooks and test balances never touch a blockchain. Switch the dashboard to Test mode (top bar) to see and manage sandbox data.
| In test mode | |
|---|---|
| Invoices | Created the same way, with realistic addresses that nobody controls. Open the invoice in the dashboard and press Simulate payment (full or partial) — it runs the real crediting code and fires your webhooks. |
| Payouts | Settle instantly with a fake tx_hash; no whitelist needed. |
| Rates | Live rates when available, otherwise a static fallback, so tests never block. |
| Webhooks | A separate list of endpoints. Use the Send test button to check your signature code. |
Go-live checklist
| 1 | Create a live API key with only the scopes your server needs, and an IP allowlist if your servers have fixed IPs. |
| 2 | Add a live webhook endpoint over HTTPS, verify the signature, respond 2xx fast, and make the handler idempotent. |
| 3 | Fulfil only on invoice.paid (and decide your policy for invoice.underpaid). Never trust the success_url visit. |
| 4 | Enable 2FA on your account and whitelist the wallet addresses you will withdraw to (the cooldown starts then, not when you first need the money). |
| 5 | Send an Idempotency-Key with every POST and retry failed requests with the same key. |
| 6 | Make a real payment of a few dollars end to end — the first live invoice is the best test there is. |
Questions? support@easyway.cash