Invoice statuses
Invoice statuses
| Status | Meaning | Terminal? |
|---|---|---|
pending | No payment seen yet | no |
seen | Payment detected in mempool (0 conf). Once a payment is in seen, the chain owns the lifecycle and we do not auto-expire it even if expires_at elapses — the next block decides. | no |
paid | Payment confirmed (≥ conf_threshold) and amount within amount_sats ± amount_tolerance_sats | no (could be reverted via reorg) |
expired | pending past expires_at with no detected payment | no — address still watched through the grace window for a possible late_paid |
late_paid | Confirmed payment arrived after expires_at but inside the grace window | no (could be reverted) |
underpaid | Confirmed amount is less than amount_sats − amount_tolerance_sats. Buyer can send a top-up and we'll auto-merge (see top-up flow below). | no |
overpaid | Confirmed cumulative amount exceeds amount_sats + amount_tolerance_sats | no — you may want to refund the difference |
requires_review | Routing landed on "no-match" (payment to a recycled address with no matching invoice), or cross-check disagreed. Manual admin action. | no |
reverted | Previously paid, then a chain reorg removed the tx | yes |
cancelled | Vendor cancelled before payment | yes |
Top-up payments (short-pay recovery)
If a buyer sends less than the invoice amount (outside the tolerance), the invoice transitions to underpaid and the watcher keeps listening. When a second on-chain transaction lands on the same address, the watcher:
- Sums all non-reverted
paymentsrows for that invoice + the new tx. - If the total now lands within
[amount_sats − tolerance, amount_sats + tolerance], the invoice flips topaid(orlate_paidif past expiry). - If still short, it stays
underpaidandamount_paid_satsreflects the new total. - If the total now exceeds
amount_sats + tolerance, it transitions tooverpaid.
What this means for your webhook handler: you may receive invoice.underpaid more than once for the same invoice (each top-up fires the matching event), followed by invoice.paid / invoice.late_paid / invoice.overpaid when the cumulative total settles. Deduplicate by event_id only — never by invoice.id. The invoice can leave underpaid and arrive at paid after a follow-up payment.
The hosted checkout page surfaces this automatically: an underpaid invoice shows a "Send remaining X sats" CTA with a fresh bitcoin: URI encoding only the remaining amount, so the buyer can't double-pay by rescanning the original QR.