easyway
Sign inCreate free account

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

Machine-readable spec: /openapi.json (OpenAPI 3.1) — import it into Postman, Insomnia or a client generator.

Get an API key

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.

Store plugins (no code)

Run a shop or a billing system? Download the plugin for your platform, upload it, paste your API key and webhook secret from the dashboard, and "Pay with crypto" appears at checkout. Orders are marked paid by the signed webhook. Install steps for each one are on the plugins page.

WooCommerce

v0.1.0 · beta

WordPress 6.0+, WooCommerce 7.0+, PHP 7.4+, store currency USD

DownloadHow to install

PrestaShop

v0.1.0 · beta

PrestaShop 8.0+, PHP with curl and json, HTTPS

OpenCart

v0.1.0 · beta

OpenCart 4.0.0.0 – 4.0.2.x, PHP 8.0+ with curl and json, HTTPS

Magento 2

v0.1.0 · beta

Magento 2.4.x, PHP 7.4 or 8.1+, base currency USD

WHMCS

v0.1.0 · beta

WHMCS 8.x, PHP with curl, client currency USD

Blesta

v0.1.0 · beta

Blesta 5.x, PHP with curl, currency USD

Odoo

v17.0.0.1.0 · beta

Odoo 17 (Community or Enterprise) with the payment module, prices in USD

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).
Project detailsOptional per key: a business name, a support email and a webhook. Running several shops on one account? Give each its own key — the checkout of every invoice created with that key shows that name and support address (and underpayment alerts go there) instead of the ones in Settings → Business, and its invoice events go to that key's webhook.
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. A full walk-through for one coin, with sandbox testing and a go-live checklist: accept USDT TRC-20 payments.

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",            // trailing zeros dropped
  "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.
statusstringpending → confirming → paid; or underpaid, expired, cancelled. See lifecycle below.
modestringlive or test.
amount / currencystringWhat you asked for, as a canonical decimal string: trailing zeros are dropped ("149.00" comes back as "149"), USD keeps at most 2 decimals.
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 the full amount arrived). Non-zero with status underpaid — and also on a paid invoice that came in short within your underpayment tolerance, so decide on status, not on this field.
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 send the rest to the same address from the same payment page; a top-up that arrives up to 7 days after expires_at is still credited and turns the invoice paid. An underpaid invoice never changes to expired — it stays underpaid until topped up, so you decide whether to fulfil partially, wait, or refund.
expiredNobody paid before expires_at.The address is watched for 7 more days: a payment that arrives in that window is still credited and the invoice flips to paid (or underpaid) — so keep handling invoice.paid for old ids.
cancelledYou cancelled it (dashboard or API) while it was pending.Nothing — but if a coin was already chosen, its address stays watched until 7 days after expires_at: a payment the customer still sends is credited and the invoice flips to paid (or underpaid). Handle that like a late payment.
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. Every case with a handler you can copy: underpaid and late payments.

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.
from / toYYYY-MM-DDOnly invoices created in this range of UTC days, both inclusive. Either side may be left out.
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
USDT_POLYGONUSDTPolygon PoS640.5 USDT
USDT_AVAXUSDTAvalanche · C-Chain100.5 USDT
USDC_ERC20USDCEthereum · ERC-20120.5 USDC
USDC_BEP20USDCBNB Smart Chain · BEP-20150.5 USDC
USDC_SOLUSDCSolana · SPL10.5 USDC
USDC_POLYGONUSDCPolygon PoS (native USDC)640.5 USDC
USDC_AVAXUSDCAvalanche · C-Chain (native USDC)100.5 USDC
DAI_ERC20DAIEthereum · ERC-20120.5 DAI
FDUSD_BEP20FDUSDBNB Smart Chain · BEP-20150.5 FDUSD
PYUSD_ERC20PYUSDEthereum · ERC-20120.5 PYUSD
USDS_ERC20USDSEthereum · ERC-20120.5 USDS
USDE_ERC20USDeEthereum · ERC-20120.5 USDe
LINK_ERC20LINKEthereum · ERC-20120.05 LINK
UNI_ERC20UNIEthereum · ERC-20120.08 UNI
AAVE_ERC20AAVEEthereum · ERC-20120.004 AAVE
ONDO_ERC20ONDOEthereum · ERC-20121.3 ONDO
ENA_ERC20ENAEthereum · ERC-20122.6 ENA
CRV_ERC20CRVEthereum · ERC-20121.6 CRV
GRT_ERC20GRTEthereum · ERC-201227 GRT
SKY_ERC20SKYEthereum · ERC-20128 SKY
MNT_ERC20MNTEthereum · ERC-20121.1 MNT
ARB_ERC20ARBEthereum · ERC-20123.5 ARB
WLD_ERC20WLDEthereum · ERC-20121.4 WLD
MANA_ERC20MANAEthereum · ERC-20127 MANA
SAND_ERC20SANDEthereum · ERC-201211 SAND
IMX_ERC20IMXEthereum · ERC-20124 IMX
GALA_ERC20GALAEthereum · ERC-2012310 GALA
FET_ERC20FETEthereum · ERC-20123.5 FET
JASMY_ERC20JASMYEthereum · ERC-2012140 JASMY
SPX_ERC20SPXEthereum · ERC-20121.4 SPX
XCN_ERC20XCNEthereum · ERC-2012150 XCN
APE_ERC20APEEthereum · ERC-20125 APE
CAKE_BEP20CAKEBNB Smart Chain · BEP-20150.25 CAKE
TWT_BEP20TWTBNB Smart Chain · BEP-20151.2 TWT
SHIB_ERC20SHIBEthereum · ERC-2012100000 SHIB
PEPE_ERC20PEPEEthereum · ERC-2012120000 PEPE
BONK_SOLBONKSolana · SPL1150000 BONK
WIF_SOLWIFSolana · SPL13 WIF
RENDER_SOLRENDERSolana · SPL10.3 RENDER
JUP_SOLJUPSolana · SPL12 JUP
PYTH_SOLPYTHSolana · SPL18 PYTH
JTO_SOLJTOSolana · SPL11.4 JTO
RAY_SOLRAYSolana · SPL10.35 RAY
W_SOLWSolana · SPL153 W
FARTCOIN_SOLFARTCOINSolana · SPL15.2 FARTCOIN
NOT_TONNOTTON (memo) · Notcoin jetton11000 NOT
DOGS_TONDOGSTON (memo) · DOGS jetton110000 DOGS
CATI_TONCATITON (memo) · Catizen jetton18 CATI
HMSTR_TONHMSTRTON (memo) · Hamster Kombat jetton13500 HMSTR
BTCBTCBitcoin20.00001 BTC
ETHETHEthereum120.0002 ETH
BNBBNBBNB Smart Chain150.0007 BNB
POLPOLPolygon PoS644 POL
AVAXAVAXAvalanche C-Chain100.05 AVAX
TRXTRXTRON191.5 TRX
SOLSOLSolana10.005 SOL
XRPXRPXRP Ledger (destination tag)10.35 XRP
LTCLTCLitecoin40.008 LTC
DOGEDOGEDogecoin65 DOGE
DASHDASHDash40.01 DASH
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, XRP and Avalanche; about a minute on TRON and BSC; a few minutes on Ethereum, Polygon, Litecoin, Dogecoin and Dash; ~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

Running several projects? Give each API key its own webhook (Dashboard → Developers → API keys → Create / Edit): events of invoices created with that key go to that URL only, signed with that endpoint's own secret. Everything else — payouts, invoices made in the dashboard or with payment links, and keys without a webhook of their own — goes to the account webhooks (Developers → Account webhooks). Billing a SaaS? The crypto payments for SaaS guide shows a renewal flow built on these events. Either way, pick the events and copy the 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. Step-by-step guide with a Next.js handler, a local test and common mistakes: verify webhook signatures.

# 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.
RetriesA failed delivery is tried up to 8 times in about 45 hours, retrying 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 & assets

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.

GET
/v1/assets
any key
curl https://api.easyway.cash/v1/assets \
  -H "Authorization: Bearer $EASYWAY_API_KEY"
{ "fee_bps": 50,
  "data": [
    { "id": "USDT_TRC20", "symbol": "USDT", "name": "Tether", "network": "TRON", "network_label": "TRON · TRC-20",
      "memo_required": false, "decimals": 6, "confirmations": 19, "payout_fee": "1", "min_payout": "0.5" },
    …
  ] }

The coins you can use right now for new invoices and payouts — the same list as Coins & networks, straight from the server, so your integration never offers a coin that isn't enabled. fee_bps is your processing fee in basis points (50 = 0.5%); payout_fee is charged in the asset itself on top of each payout.

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",
  "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:

GET
/v1/payouts/:id
scope payouts:read
curl https://api.easyway.cash/v1/payouts/po_3Hd8Kq1Lm5Np7Rs9Tv2Wx4 \
  -H "Authorization: Bearer $EASYWAY_API_KEY"
GET
/v1/payouts
scope payouts:read
FieldTypeDescription
pageinteger1-based, default 1. Newest first.
pageSizeinteger1–100, default 20.
statusstringOnly payouts in this state: pending, processing, sent, failed or cancelled.
assetstringOnly payouts of this coin id (coins that are no longer enabled included).
from / toYYYY-MM-DDOnly payouts created in this range of UTC days, both inclusive. Either side may be left out.
curl https://api.easyway.cash/v1/payouts?page=1&pageSize=50&status=sent&from=2026-09-01&to=2026-09-30 \
  -H "Authorization: Bearer $EASYWAY_API_KEY"

Returns { data: [payout…], total, page, page_size } for the key's mode — handy for reconciling your settlement records: page through one month with from / to until you have total rows. A day is a UTC day.

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.

A walk-through with idempotent retries, the payout webhooks and a go-live checklist: send crypto payouts with the API.

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        # 8–128 chars (A–Z a–z 0–9 _ - : .), 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",
  "fields": ["amount"]
}

fields (validation errors only) names the inputs that were rejected; other errors carry a human-readable message instead.

StatuserrorMeaning
400validation_errorA field is missing or malformed; `fields` lists which.
400invalid_idempotency_keyIdempotency-Key must be 8–128 characters: letters, digits, _ - : .
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. A test plan for paid, underpaid, expired and late payments: test crypto payments in sandbox mode.

In test mode
InvoicesCreated the same way, with realistic addresses that nobody controls. Open the invoice in the dashboard and press Simulate payment — empty pays what is due, a smaller amount makes it underpaid. 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 — what to include is on the contact page, and live API and watcher health on system status.

Crypto payment API documentation | easyway