TillsafeDocstillsafe.comRequest early access

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:

sh
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:

sh
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"]}'
sh
export INVOICE_ID="inv_…"

Send 25 of the 30 USDT, as a payer whose exchange took its fee out of the amount would:

sh
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:

sh
curl "$API_BASE/v1/invoices/$INVOICE_ID" \
  -H "Authorization: Bearer $SECRET_KEY"
json
{
  "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.

FieldDefaultWhat it does
assetThe invoice's first payment optionWhich option to pay: an asset code (USDT@eip155:97), or a symbol (USDT) with network.
networkFrom assetA CAIP-2 network (tron:nile). When asset is a full code, it must agree with it.
amountThe option's amount_dueWhole 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 seeBodyWhat happens
A normal payment{}invoice.detected, then invoice.paid.
A short payment{"amount": "25"}partially_paid, with amount_remaining of 5 USDT.
A top-upThe short payment, then {"amount": "5"}paid.
A shortfall inside your toleranceSet 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 paymentCancel 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 codeWhen
403A live key (test_mode_only), or a publishable key.
404No such invoice in test mode, or a live-mode server.
400 parameter_invalidparam 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