Testing with simulate_payment
Pay a test-mode invoice through the real pipeline, without a wallet or a faucet - exact, short, over, late or in another asset.
In test mode, POST /v1/test_helpers/invoices/{id}/simulate_payment sends a synthetic transfer to the
invoice's real deposit address. It goes through the same pipeline as a real payment: attribution by the
address lease, the confirmation policy, the invoice's status, the checkout's live status and your
webhooks. It never touches a chain, and it does not exist in live mode.
Every command on this page is run, as written, by the docs' test suite against a real server.
Set up
You need a test secret key (early access) and curl:
export API_BASE="https://api.tillsafe.com"
export SECRET_KEY="cp_test_sk_…"A short payment
Create a 30 USD invoice that accepts USDT on TRON Nile:
curl "$API_BASE/v1/invoices" \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Idempotency-Key: test-short-1" \
-H "Content-Type: application/json" \
-d '{"amount": "30.00", "currency": "USD", "assets": ["USDT@tron:nile"]}'export INVOICE_ID="inv_…"Send 25 of the 30 USDT, as a payer whose exchange took its fee out of the amount would:
curl "$API_BASE/v1/test_helpers/invoices/$INVOICE_ID/simulate_payment" \
-H "Authorization: Bearer $SECRET_KEY" \
-H "Idempotency-Key: test-short-1-pay-1" \
-H "Content-Type: application/json" \
-d '{"asset": "USDT@tron:nile", "amount": "25"}'The response is the invoice. Once the payment is final, the invoice is partially_paid and says what is
still missing:
curl "$API_BASE/v1/invoices/$INVOICE_ID" \
-H "Authorization: Bearer $SECRET_KEY"{
"id": "inv_…",
"status": "partially_paid",
"amount_remaining": { "amount": "5", "asset": "USDT@tron:nile" }
}Your webhook endpoint received invoice.detected and then invoice.partially_paid. Simulate a second
payment of the remaining 5 USDT (with a new Idempotency-Key) and the invoice is paid: payments to an
invoice add up.
The request body
Every field is optional, so {} pays the invoice's first payment option exactly.
| Field | Default | What it does |
|---|---|---|
asset | The invoice's first payment option | Which option to pay: an asset code (USDT@eip155:97), or a symbol (USDT) with network. |
network | From asset | A CAIP-2 network (tron:nile). When asset is a full code, it must agree with it. |
amount | The option's amount_due | Whole tokens, as a decimal string. Less is a short payment, more an overpayment. |
finality_steps | ["included", "final"] | The finality levels the payment passes through, in order: any of pending, included, safe, final. |
Unknown fields are refused. Each call is one payment: give each its own Idempotency-Key, and a retry
with the same key and body returns the first result instead of paying twice.
Scenarios
On a 30 USD invoice that accepts USDT@tron:nile and USDT@eip155:97:
| To see | Body | What happens |
|---|---|---|
| A normal payment | {} | invoice.detected, then invoice.paid. |
| A short payment | {"amount": "25"} | partially_paid, with amount_remaining of 5 USDT. |
| A top-up | The short payment, then {"amount": "5"} | paid. |
| A shortfall inside your tolerance | Set underpay_tolerance_bps first (new invoices only), then {"amount": "29.90"} | paid with a 50 (0.5%) tolerance. |
| An overpayment | {"amount": "31"} | overpaid. A refund created with {} returns exactly the 1 USDT excess. |
| The other network | {"asset": "USDT@eip155:97"} | paid, through BSC testnet. |
| Slow finality | {"finality_steps": ["pending", "included", "safe", "final"]} | The payment walks through every level, as on a slow chain: invoice.detected first, invoice.paid only at final. |
| A late payment | Cancel the invoice, then {} | It stays cancelled; you get invoice.late_payment, and the payment is refundable. |
A wrong token cannot be simulated with the standard test assets: the token must be one the network knows, on the same chain as the invoice's address.
Errors
Status and code | When |
|---|---|
403 | A live key (test_mode_only), or a publishable key. |
404 | No such invoice in test mode, or a live-mode server. |
400 parameter_invalid | param says which field: an asset the invoice cannot take, a mainnet network, a zero or malformed amount, or finality_steps out of order. |
Watching your webhooks
GET /v1/webhook_deliverieslists every delivery attempt with the status code and the start of your endpoint's response.POST /v1/webhook_deliveries/{id}/replaysends a delivery again with the same event id, to test your deduplication.- The quickstart sets up a receiver that verifies signatures, and webhooks and signatures has it in Python, PHP and Go.