Skip to content
Newstest modesandboxAPIwebhooksintegrationdevelopers

Test crypto payments without spending a coin

CoinsFlow · · Updated

Add "test": true to an invoice and it behaves like a real one, with the same fields, the same payment page and the same signed webhooks, but no money. How to simulate paid, underpaid, overpaid and expired, how to keep test events out of your live code, and the limits.

The hard part of a payment integration is not the first request. It is everything after it: the webhook that arrives twice, the buyer who sends too little, the invoice that expires overnight. On most crypto rails you can only test those by paying real coins to yourself, waiting for blocks, and paying network fees for every try. CoinsFlow's test mode lets you walk the whole path with no money and no waiting.

What a test invoice is

Make an invoice the ordinary way, with one more field:

curl -X POST https://api.coinsflow.app/v1/invoices \
  -H "Authorization: Bearer $COINSFLOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "chain": "litecoin", "asset": "LTC", "priceAmount": "20.00", "test": true }'

The answer has the same fields as a real invoice: id, paymentUrl, amountDue, status, expiresAt, confirmationsRequired and the rest. The price is worked out at a real exchange rate, so the amount looks like a real one. Three things are different:

  • The address is a placeholder. It is not one of ours, and nothing can be sent to it.
  • No money moves, ever. A test invoice has no transaction, no ledger entry and no balance. Nothing the dashboard or the admin console adds up includes it.
  • Nothing happens on its own. A real invoice is paid when the chain says so. A test invoice is paid when you say so.

Test invoices live in their own table and are listed separately: GET /v1/invoices?test=true. They are kept for 30 days.

Simulating what the buyer does

Call POST /v1/invoices/{id}/simulate with an outcome, or press the buttons on the invoice's payment page, which does the same thing. The outcomes:

  • paid (the default): the invoice walks the real path, detected, confirming, then paid and settled. You receive invoice.detected, invoice.confirming, invoice.payment, invoice.paid and invoice.settled, in that order.
  • underpaid: half the amount arrives. You receive invoice.detected, invoice.confirming, invoice.payment and invoice.underpaid with amountRemaining. Simulate paid afterwards to see the top-up: a second payment arrives and the invoice is paid.
  • overpaid: a tenth more than the amount arrives. The same first three events, then invoice.paid, invoice.overpaid with amountOverpaid, and invoice.settled.
  • detected: the payment is seen, not yet in a block. From here, confirming or paid.
  • confirming: the payment is in a block, not yet at its confirmations. From here, paid.
  • expired: nobody paid in time. invoice.expired.

The order matters as it would in life. An invoice that is confirming cannot become underpaid; one that is expired cannot become anything else, and the API says so with 409 not_possible. A pay-what-you-want test invoice cannot be under- or overpaid, because it has no price to miss. And a test invoice expires like a real one: after its ttlSeconds, 3 days by default, it is marked expired the next time anything looks at it, and simulating further is refused. Make a new one.

Invoices in any coin can be tested too: leave out chain and asset, open the payment page, pick a coin, and simulate from there.

The webhooks are real

This is the point of the exercise. A simulated outcome queues the same events a real invoice would, into the same queue the notifier sends from, signed with the same endpoint secret, in the same JSON shape. The only difference is one field: the event carries "test": true, and so does the invoice inside data.

Your handler must look at that field. Verify the signature exactly as for a live event, then decide what to do with a test event. The pattern we recommend:

const event = JSON.parse(rawBody);
if (event.test && !process.env.TESTING) return res.sendStatus(200); // acknowledged, ignored

In development, set TESTING and let the handler run its full path: grant the role, send the download, mark the order paid in a staging database. In production, leave it unset and test events are acknowledged and dropped. Never let a test event fulfil a real order. Never let one fail your endpoint either: answer 2xx, or it is retried like any other.

The signature check is the same for test and live events, so test mode also proves your verification code, your raw-body handling and your idempotency. Simulate paid twice on two invoices and you will see invoice.paid twice; your code should fulfil each once. Simulate underpaid and then paid, and you will see the statuses arrive in the order a real top-up produces.

Keeping test and live apart

  • Test events from the dashboard's "Send test" button are a ping, not an invoice event. They prove the endpoint answers and the signature checks; they do not exercise your order logic. Use simulate for that.
  • Test invoices never appear in your balance, your volume or the admin's figures. If you want them out of your own reports, filter on test.
  • Test events are delivered after a merchant's real events. A busy test run never delays a real invoice.paid.
  • The payment page of a test invoice looks like the real one, with the simulate buttons added. Do not send it to a buyer.

The limits

Test mode calls your endpoints, so it has limits that stop a loop from hammering them, or ours:

  • 500 test invoices a day per merchant. Past that, 429 too_many_test_invoices. Reuse the ones you have; a test invoice can be simulated along any allowed path.
  • 30 simulations a minute per merchant: 429 rate_limited with a Retry-After header.
  • When an endpoint already has 200 test events waiting to be delivered, no more are queued for it. The simulate answer then carries webhooksNotQueued with how many were dropped and why. Fix or wake the endpoint and simulate again.
  • 10 "Send test" pings or resends a minute per endpoint.

The real-invoice limits apply too: 120 invoices a minute per merchant and 240 per account, test or not.

A test plan that catches the usual bugs

  1. Create a test invoice. Check that your code stores id and paymentUrl as strings and shows the page to the buyer.
  2. Simulate paid. Check the order is fulfilled once, that invoice.detected only showed a message, and that invoice.settled did nothing your paid handler had already done.
  3. Simulate underpaid on a new invoice. Check you show amountRemaining and do not fulfil. Then paid: check the top-up fulfils.
  4. Simulate overpaid. Check you fulfil once and do not tell the buyer they overpaid in a way that alarms them: all of it is credited to you.
  5. Simulate expired. Check the order is closed and the buyer is offered a fresh invoice.
  6. Send the same event body again by hand with a fresh valid signature. Check nothing happens twice.
  7. Turn TESTING off. Simulate paid. Check nothing is fulfilled and your endpoint still answered 200.

When all seven pass, remove "test": true and the same code is live. There is no second set of keys, no separate base URL and nothing to switch over. The full field list and every event are in the API docs, and the webhook signature is explained in the Webhooks section.