easyway
API DOCS
Get an API key

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.

Base URL: https://api.easyway.cash
Auth: Bearer sk_live_… / sk_test_…
Format: JSON · snake_case · amounts as strings

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.

StepWhat happens
1. CreatePOST /v1/invoices with the amount. You get back an id and a payment_url.
2. PayRedirect the customer to payment_url (or open it in a new tab / iframe). The page updates itself while the payment confirms.
3. ConfirmYou receive invoice.paid on your webhook URL. Verify the signature, then fulfil the order. (invoice.underpaid if they sent less.)
4. SettleFunds 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)
ModeThe key decides: sk_test_… keys only ever see sandbox data, sk_live_… keys only real money. There is no mode parameter.
ScopesEach 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 allowlistOptional per key. With one set, requests from any other IP are refused (403 ip_not_allowed).
Rate limit300 requests per minute per key. Over it you get 429 rate_limited; the RateLimit-* headers tell you when to retry.
Keep it secretNever 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.
Test the whole flow with a 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

POST
/v1/invoices
scope invoices:write · supports Idempotency-Key
FieldTypeDescription
amountstring, requiredDecimal string. In currency units: "149.00" USD, or "0.005" when currency is BTC.
currencystring"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_idstring ≤128Your reference. Comes back on the invoice and in every webhook; searchable in the list endpoint.
descriptionstring ≤500Shown to the customer on the payment page.
customer_emailemailStored on the invoice for your records and support.
success_urlhttps URLWhere the customer can return after paying.
cancel_urlhttps URLWhere the customer can return if they give up.
allowed_assetsstring[]Restrict which coins are offered, e.g. ["USDT_TRC20", "USDT_BEP20"]. Default: everything enabled on your account.
metadataobjectUp to 20 string values (≤500 chars each). Returned untouched, e.g. your cart or user id.
lifetimeinteger 5–1440Minutes 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

FieldTypeDescription
idstringinv_ + 22 characters. Also the last part of payment_url.
statusstringpendingconfirmingpaid; or underpaid, expired, cancelled. See lifecycle below.
modestringlive or test.
amount / currencystringWhat you asked for, exactly as you sent it.
pay_assetstring | nullThe coin the customer chose (e.g. USDT_TRC20); null until they choose.
pay_amountstring | nullHow much of that coin they must send — quoted at selection and locked for the invoice's lifetime.
pay_addressstring | nullThe deposit address. Unique to this invoice on every network except TON and XRP.
pay_memostring | nullWhen non-null, the customer must attach it (TON comment / XRP destination tag). The payment page shows it and encodes it in the QR.
ratestring | nullUSD price of one unit of pay_asset at quote time.
received_amountstringConfirmed on-chain amount so far, in pay_asset.
remaining_amountstringWhat is still owed (0 once paid). Non-zero with status underpaid.
fee_amount / net_amountstringeasyway's processing fee and what was credited to your balance, in pay_asset.
payment_urlstringThe hosted payment page for this invoice.
expires_at / paid_at / created_atISO-8601UTC timestamps.

Lifecycle

StatusMeaningWhat you should do
pendingCreated; nothing seen on-chain yet.Nothing. Expires at expires_at if unpaid.
confirmingA payment was seen but is not final yet (waiting for confirmations).Nothing — you get invoice.confirming so you can show \"payment detected\".
paidFinal. 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.
underpaidFinal, 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.
expiredNobody 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.
cancelledYou cancelled it (dashboard or API) while it was pending.Nothing.
Overpayments are credited in full. Underpayments: the customer's wallet may deduct a network fee from the amount — tell them to send the exact pay_amount, and pick a tolerance in Dashboard → Settings that suits your margins.

Retrieve an invoice

GET
/v1/invoices/:id
scope invoices:read
curl https://api.easyway.cash/v1/invoices/inv_7Q2mN4kL8pR1sT9uV3wX0y \
  -H "Authorization: Bearer $EASYWAY_API_KEY"

List invoices

GET
/v1/invoices
scope invoices:read
FieldTypeDescription
pageinteger1-based, default 1.
pageSizeinteger1–100, default 20.
order_idstringOnly 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

POST
/v1/invoices/:id/cancel
scope invoices:write · only while pending
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.

idCoinNetworkConfirmationsMin payout
USDT_TRC20USDTTRON · TRC-20190.5 USDT
USDT_BEP20USDTBNB Smart Chain · BEP-20150.5 USDT
USDT_ERC20USDTEthereum · ERC-20120.5 USDT
USDT_TONUSDTTON (memo)10.5 USDT
USDT_SOLUSDTSolana · SPL10.5 USDT
USDC_ERC20USDCEthereum · ERC-20120.5 USDC
USDC_BEP20USDCBNB Smart Chain · BEP-20150.5 USDC
USDC_SOLUSDCSolana · SPL10.5 USDC
BTCBTCBitcoin20.00001 BTC
ETHETHEthereum120.0002 ETH
BNBBNBBNB Smart Chain150.0007 BNB
TRXTRXTRON191.5 TRX
SOLSOLSolana10.005 SOL
XRPXRPXRP Ledger (destination tag)10.35 XRP
LTCLTCLitecoin40.008 LTC
DOGEDOGEDogecoin65 DOGE
GRAMGRAMTON (memo)10.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… }
  }
}
EventWhen
invoice.confirmingA payment was seen on-chain and is waiting for confirmations.
invoice.paidThe payment is final and credited. Fulfil the order.
invoice.underpaidA final payment arrived, but less than the amount. data.invoice.remaining_amount says how much is missing.
invoice.expiredThe invoice expired unpaid.
invoice.cancelledThe invoice was cancelled.
payout.sentA withdrawal was broadcast. data.payout has the tx_hash.
payout.failedA withdrawal could not be sent; the funds are back on your balance. data.payout.failure_reason says why.
webhook.testSent 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 fastAnswer 2xx within 10 seconds, then do the work. Anything else (timeouts, 4xx, 5xx) counts as a failure.
RetriesFailed 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 handlersThe 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).
OrderEvents are not guaranteed to arrive in order. Decide from the invoice's status in the payload, not from which event came last.
Source of truthIf in doubt, GET /v1/invoices/:id. The payload is a snapshot from the moment the event was created.

Balances & rates

GET
/v1/balances
scope balances:read
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.

GET
/v1/rates
any key
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.

POST
/v1/payouts
scope payouts:write · supports Idempotency-Key
FieldTypeDescription
assetstring, requiredA coin id from Coins & networks.
addressstring, requiredDestination. Validated for that network (checksums included); in live mode it must be whitelisted.
memostring ≤120TON comment / XRP destination tag, when the destination (an exchange) needs one. In live mode the whitelist entry's memo is used.
amountstring, requiredIn the asset's units, at most its on-chain precision, and at least the minimum payout. The network fee is charged on top.
notestring ≤255For 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: pendingprocessing (being signed) → sent with a tx_hash, or failed with the funds back on your balance. Subscribe to payout.sent / payout.failed, or poll:

GET
/v1/payouts/:id
scope payouts:read
curl https://api.easyway.cash/v1/payouts/po_3Hd8Kq1Lm5Np7Rs9Tv2Wx4 \
  -H "Authorization: Bearer $EASYWAY_API_KEY"
In test mode payouts settle instantly with a fake tx_hash and need no whitelist, so you can automate settlement end to end before going live.

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
SituationResponse
First requestProcessed normally; the response is stored under the key for 24 hours.
Retry, same bodyThe stored response (same status code, same id) — nothing is created twice.
Retry, different body422 idempotency_key_reused — the key is bound to the first body.
Retry while the first is still running409 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" } ]
}
StatuserrorMeaning
400validation_errorA field is missing or malformed; `details` lists which.
400invalid_idempotency_keyIdempotency-Key must be 1–255 printable characters.
400unknown_currency / unknown_assetNot one of the ids in Coins & networks.
400asset_not_allowedThe invoice's allowed_assets doesn't include it.
400amount_too_small / amount_too_largeBelow the coin's precision or above 1,000,000 USD.
400invalid_addressThe payout address is not valid for that network (checksum included).
400below_minimum_payoutSee the min payout column.
401invalid_api_keyMissing, malformed, revoked, or a test key used against live data.
403missing_scopeThe key was created without the scope this endpoint needs.
403ip_not_allowedThe key has an IP allowlist and this IP isn't on it.
403address_not_whitelistedLive payouts only go to addresses approved in the dashboard with 2FA.
403payouts_temporarily_lockedPayouts are frozen for this account (e.g. 2FA was just disabled).
404invoice_not_found / payout_not_foundWrong id, or it belongs to another merchant or mode.
409invoice_not_pending / invoice_expiredThe invoice can no longer be changed.
409request_in_progressThe same Idempotency-Key is still being processed — retry in a moment.
422insufficient_fundsAvailable balance is below amount + network fee.
422idempotency_key_reusedSame Idempotency-Key, different request body.
429rate_limitedOver 300 requests per minute on this key. Back off and retry.
503rate_unavailableNo 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
InvoicesCreated 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.
PayoutsSettle instantly with a fake tx_hash; no whitelist needed.
RatesLive rates when available, otherwise a static fallback, so tests never block.
WebhooksA separate list of endpoints. Use the Send test button to check your signature code.

Go-live checklist

1Create a live API key with only the scopes your server needs, and an IP allowlist if your servers have fixed IPs.
2Add a live webhook endpoint over HTTPS, verify the signature, respond 2xx fast, and make the handler idempotent.
3Fulfil only on invoice.paid (and decide your policy for invoice.underpaid). Never trust the success_url visit.
4Enable 2FA on your account and whitelist the wallet addresses you will withdraw to (the cooldown starts then, not when you first need the money).
5Send an Idempotency-Key with every POST and retry failed requests with the same key.
6Make a real payment of a few dollars end to end — the first live invoice is the best test there is.

Questions? support@easyway.cash

API documentation | easyway