SatLane
Documentation

Invoice statuses

Invoice statuses

StatusMeaningTerminal?
pendingNo payment seen yetno
seenPayment 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
paidPayment confirmed (≥ conf_threshold) and amount within amount_sats ± amount_tolerance_satsno (could be reverted via reorg)
expiredpending past expires_at with no detected paymentno — address still watched through the grace window for a possible late_paid
late_paidConfirmed payment arrived after expires_at but inside the grace windowno (could be reverted)
underpaidConfirmed 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
overpaidConfirmed cumulative amount exceeds amount_sats + amount_tolerance_satsno — you may want to refund the difference
requires_reviewRouting landed on "no-match" (payment to a recycled address with no matching invoice), or cross-check disagreed. Manual admin action.no
revertedPreviously paid, then a chain reorg removed the txyes
cancelledVendor cancelled before paymentyes

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:

  1. Sums all non-reverted payments rows for that invoice + the new tx.
  2. If the total now lands within [amount_sats − tolerance, amount_sats + tolerance], the invoice flips to paid (or late_paid if past expiry).
  3. If still short, it stays underpaid and amount_paid_sats reflects the new total.
  4. If the total now exceeds amount_sats + tolerance, it transitions to overpaid.

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.