TillsafeDocstillsafe.comRequest early access

Invoices

A request for an exact amount, its statuses, how long it stays open, and what each status means for you.

An invoice is a fiat price ("30.00 USD") turned into one or more payment options: an exact amount of an asset on a network, and the address to send it to. You create it on your server with POST /v1/invoices, and read it back with GET /v1/invoices/{id} or from your webhooks.

Statuses

The status is derived from what has actually arrived on the chain, every time something changes. It is never set by hand, and never from anything the payer tells the checkout.

StatusMeaningWhat you do
awaiting_paymentNothing seen yet.Wait.
detectedA payment was seen on the chain, nothing is final yet.Show "payment received, confirming". Do not ship.
confirmingThe same, with confirmations arriving. The webhook stays invoice.detected: confirmations alone send nothing.Wait.
partially_paidFinal funds arrived, but less than the amount due (minus your tolerance). The payer can top up.Wait, or refund if it expires.
paidThe amount due arrived and is final.Fulfil the order.
overpaidPaid, with an excess of at least 0.01 of the stablecoin (any excess, on a BTC-only invoice).Fulfil; refund the excess if you want to.
paid_latePaid, but the payment that completed it arrived after the detection window, or (for a volatile asset) after the price lock.Fulfil.
expiredThe window closed without full payment. Anything that did arrive is refundable.Cancel the order; refund partial payments.
under_reviewA payment is held for a person to look at (see reviews). Nothing more is credited and the clock stops.Accept or reject it.
cancelledYou cancelled it before anything arrived, or rejected a held payment.Nothing, or refund what arrived.

paid, overpaid and paid_late all mean "paid": treat the three alike when you fulfil. Each status change sends invoice.<status> (for example invoice.paid) to your webhook, except confirming. A paid invoice never expires afterwards, but a chain reorganisation can undo a credit: you then get invoice.payment_reversed and the status steps back. That is why finality matters (finality per chain).

How long an invoice stays open

expires_at is the end of the detection window. It is set when the invoice is created and cannot be changed per invoice:

Invoice offersWindowPrice lock
Stablecoins only24 hoursnone: a stablecoin is priced 1:1
Any volatile asset (BTC), even alongside stablecoins60 minutes15 minutes (price_locked_until)

A payment that arrives in the 10 minutes after expires_at still counts, as paid_late. A payment that was already seen inside the window but is still confirming gets up to 6 more hours to become final. After that the invoice is expired, and later arrivals are late payments.

Creating one

json
{
  "amount": "30.00",
  "currency": "USD",
  "assets": ["USDT@tron:nile", "USDT@eip155:97"],
  "description": "Order #1001",
  "customer_ref": "cus_8812",
  "metadata": { "order_id": "1001" }
}
  • amount and currency: the price, as a decimal string in the currency's major unit.
  • assets: which payment options to offer. Leave it out to offer your account's default_assets.
  • description: shown to the payer, at most 500 characters.
  • customer_ref (at most 200 characters) and metadata (at most 20 pairs, keys up to 40 characters, values up to 500): yours only, never shown to the payer.

Unknown fields are refused, so a typo is an error rather than a silently ignored option.

Idempotency

Every POST takes an Idempotency-Key header (1 to 255 visible ASCII characters). Use one key per logical operation, such as your order id:

  • Same key, same request (method, path and body, byte for byte): you get the stored response again, marked Idempotent-Replayed: true. Nothing is created twice.
  • Same key, different request: 409 with idempotency_key_reused.
  • Same key while the first request is still running: 409 with idempotency_key_in_use. Retry shortly.
  • A 5xx response is not stored, so retrying it runs the request again.

Keys are remembered for 24 hours.

Cancelling

POST /v1/invoices/{id}/cancel works only while nothing at all has been seen for the invoice: after that it is 409 invoice_not_cancellable, because money may already be on its way. Cancelling twice is harmless. The address goes into its cooldown rather than straight back into use, so a payment that still arrives is attributed to this invoice and becomes a refundable late payment.