Test crypto payments in sandbox mode
Before a single real coin moves, you can run your whole integration against the sandbox: create invoices, receive paid, underpaid and expired webhooks, and make payouts. Nothing touches a blockchain. This guide shows what the sandbox does, what it does not do, and a test plan that covers the cases real customers will hit.
How the sandbox works
Every easyway account exists twice, test and live, with separate API keys, webhook endpoints, invoices and balances. The API key decides the mode: a sk_test_ key creates test data, a sk_live_ key real data. In the dashboard, the Test mode switch in the top bar shows the sandbox side.
| In test mode | What happens |
|---|---|
| Invoices | Same request, same response, same hosted payment page with a "Test mode, do not send real funds" banner. Addresses look real, but nobody controls them. |
| Payments | Simulated from the dashboard with Simulate payment. The deposit is recorded as fully confirmed and goes through the real crediting code, fee and webhooks. |
| Rates | Live prices when they are available, otherwise a fixed fallback price, so a test never fails because a price source is down. Live invoices never use the fallback. |
| Payouts | No whitelist needed. A payout is settled at once with a fake tx_hash starting with test_, and payout.sent is sent. |
| Webhooks | Their own list of endpoints with their own signing secrets. Send test delivers a webhook.test event. |
1. Set up the test side
Switch the dashboard to test mode, then in Developers create a test API key (scope invoices:write, plus invoices:read and payouts:write if you want to try those) and add a test webhook endpoint. Press Send test on the endpoint first: if your handler answers 2xx to the webhook.test event, the URL is reachable and your signature check works. The webhook signatures guide has handlers and a local curl test.
The webhook URL must be a public https:// address, in test mode too: localhost, private networks and plain HTTP are refused. During development, expose your local server with an HTTPS tunnel and use its public URL.
2. Create a test invoice from your code
The call is the same as in production. Only the key changes, so keep it in configuration, not in code.
Node.js
// Same call as in production, only the key is a sk_test_ key
const res = await fetch("https://api.easyway.cash/v1/invoices", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EASYWAY_API_KEY}`,
"Idempotency-Key": "test-order-1",
"Content-Type": "application/json",
},
body: JSON.stringify({
"amount": "25.00",
"currency": "USD",
"allowed_assets": [
"USDT_TRC20"
],
"order_id": "test-order-1"
}),
});
if (!res.ok) throw new Error(`easyway ${res.status}: ${(await res.json()).error}`);
const data = await res.json();PHP
// Same call as in production, only the key is a sk_test_ key
$ch = curl_init("https://api.easyway.cash/v1/invoices");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
"amount" => "25.00",
"currency" => "USD",
"allowed_assets" => ["USDT_TRC20"],
"order_id" => "test-order-1",
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("EASYWAY_API_KEY"),
"Idempotency-Key: test-order-1",
"Content-Type: application/json",
],
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
throw new Exception("easyway: " . $data["error"]);
}Python
# Same call as in production, only the key is a sk_test_ key
import os, requests
res = requests.post(
"https://api.easyway.cash/v1/invoices",
headers={
"Authorization": f"Bearer {os.environ['EASYWAY_API_KEY']}",
"Idempotency-Key": "test-order-1",
},
json={
"amount": "25.00",
"currency": "USD",
"allowed_assets": ["USDT_TRC20"],
"order_id": "test-order-1",
},
)
res.raise_for_status()
data = res.json()Open the returned payment_url to see the payment page your customers will get. With allowed_assets limited to one coin the address is assigned straight away; otherwise pick a coin on the page.
3. Simulate payments
In the dashboard (test mode), open the invoice under Payments. Simulate payment pays the amount that is still due. If no coin was chosen yet, it uses the first coin in allowed_assets, or USDT on TRON. Once a coin is chosen, an amount field next to the button lets you send less. Simulated payments are always fully confirmed, so you get invoice.paid or invoice.underpaid directly, not invoice.confirming.
| Case to test | How | What your code should do |
|---|---|---|
| Full payment | Press Simulate payment with the field empty. | invoice.paid: fulfil the order exactly once. |
| Underpayment | Enter less than the amount due, then press. | invoice.underpaid with remaining_amount: do not fulfil yet. |
| Top-up after an underpayment | Press again with the field empty. | invoice.paid for the same invoice: now fulfil. |
| Expiry | Create an invoice with lifetime: 5 (minutes, the minimum) and wait. | invoice.expired: release the stock, keep the order. |
| Late payment | Simulate a payment on that expired invoice (its coin must already be chosen). | invoice.paid after invoice.expired: your handler must still accept it. |
| Duplicate delivery | Make your endpoint answer 500 once; the retry comes about a minute later. | The retry carries the same event id: ignore it the second time. |
The webhook log in the dashboard lists every delivery with its HTTP status, so you can see what your handler answered. Your test balance grows by each simulated payment minus the fee, the same way a live balance does.
4. Try a payout
With payouts:write on the test key, a payout from your test balance needs no whitelisted address and is settled immediately. The address must still be valid for the coin, and the amount must be at least the coin's minimum payout.
// Sandbox: no whitelist needed, settled at once with a test_ tx hash
const res = await fetch("https://api.easyway.cash/v1/payouts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.EASYWAY_API_KEY}`,
"Idempotency-Key": "test-payout-1",
"Content-Type": "application/json",
},
body: JSON.stringify({
"asset": "USDT_TRC20",
"address": "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8",
"amount": "10",
"note": "Sandbox payout"
}),
});
if (!res.ok) throw new Error(`easyway ${res.status}: ${(await res.json()).error}`);
const data = await res.json();What the sandbox cannot show you
| Not covered | Why it matters in live mode |
|---|---|
| Real confirmation times | A real payment first sends invoice.confirming and becomes paid after the coin's confirmations, from seconds to tens of minutes. |
| Wallet behaviour | Some wallets deduct the network fee from the amount sent, which makes an invoice underpaid. Set an underpayment tolerance in Settings. |
| Whitelist and 2FA | Live payouts go only to whitelisted addresses, and live money actions, including a live key that can send payouts, need 2FA. |
The best last test is a real payment of a few dollars with a sk_live_ key. The go-live checklist lists the rest, and the USDT TRC-20 guide shows a complete integration from invoice to fulfilment.