What is an Invoice?
If you've used Stripe, you're familiar with a PaymentIntent. A Coinsnap invoice is the equivalent — a payment request for a specific amount.
The lifecycle
New → Processing → Settled
→ Expired
→ Invalid
The main statuses are:
| Status | Meaning |
|---|---|
New | Invoice created, waiting for payment |
Processing | Payment detected. On-chain it means waiting for block confirmation; Lightning passes through it on the way to Settled |
Settled | Payment confirmed and funds are being forwarded to your wallet |
Expired | Invoice expired before full payment was received |
Invalid | Invoice was invalidated (e.g. tip amount exceeded total) |
Invalid is readable on the invoice but is never delivered as a webhook. See the Webhook Status Reference.
The additionalStatus field refines Settled and Expired:
status | additionalStatus | Meaning |
|---|---|---|
Settled | None | Full amount received, or short by less than the store's underpayment tolerance |
Settled | Overpaid | Customer paid more than the invoice amount |
Settled | PaidAfterExpiration | Payment received after the invoice expired |
Expired | None | No payment at all |
Expired | Underpaid | Short by more than the tolerance |
Underpayment tolerance
Payers routinely send a little less than the invoice asks for, without meaning to. An on-chain wallet may take the mining fee out of the amount rather than adding it on top, a Lightning payment can lose a few satoshis to routing, and wallets round. Treating every such invoice as underpaid would block fulfillment over a shortfall of a few satoshis, so each store has a tolerance.
It is a percentage of the invoice amount, so it scales with the invoice rather than being a fixed number of satoshis. An invoice counts as underpaid only when:
amount - totalPaid > amount × (underpaymentTolerance / 100)
A shortfall exactly equal to the tolerance is still within it. Anything larger expires the invoice with additionalStatus: Underpaid; anything smaller settles as Settled with additionalStatus: None.
Reading the value
underpaymentTolerance is a percentage, not a fraction. The scale runs 0 to 100, not 0 to 1, which is where the division by 100 in the formula comes from. So a stored 2 means 2 percent, and a stored 0.25 means a quarter of one percent, not 25 percent. Values below 1 are ordinary here, and misreading one as a fraction overstates the tolerance a hundredfold.
Worked example, on an invoice for 100,000 sats with a tolerance of 2:
- tolerated shortfall is
100,000 × (2 / 100)= 2,000 sats - 98,000 sats received settles as
Settled+None, since the shortfall equals the tolerance - 97,999 sats received expires as
Expired+Underpaid
Up to two decimal places are accepted. 0 disables the tolerance and requires the exact amount. Read your store's value as underpaymentTolerance on GET /api/v1/stores/{storeId}.
The invoice always carries the exact figures, amount expected and totalPaid received, both in satoshis, so you can apply a stricter rule of your own.
What's inside an invoice
When you create an invoice, Coinsnap returns everything you need to present payment options to your customer:
{
"id": "inv_4Kz9mXpQ2rNvBtYwLs8cDf",
"status": "New",
"amount": 10.00,
"currency": "EUR",
"orderId": "order-123",
"checkoutLink": "https://app.coinsnap.io/i/4Kz9mXpQ2rNvBtYwLs8cDf",
"lightningInvoice": "lnbc100u1p...",
"onchainAddress": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh",
"bip21": "bitcoin:bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh?amount=0.00010000",
"createdAt": 1705312800
}
| Field | What to do with it |
|---|---|
id | Store in your database to correlate with the webhook |
checkoutLink | Redirect the customer here (easiest path) |
lightningInvoice | Render as QR if building a custom UI |
onchainAddress | On-chain fallback QR in custom UI |
bip21 | BIP-21 URI combining address + amount — use for a unified QR |
orderId | Your reference — echoed back in the webhook |
createdAt | Unix timestamp (seconds) of invoice creation |
The simplest integration path
You don't have to render QR codes yourself. The checkoutLink points to a Coinsnap-hosted page that:
- Shows both Lightning and Bitcoin QR codes
- Updates the status in real time
- Redirects to your
redirectUrlafter payment
// After creating the invoice, just redirect:
window.location.href = invoice.checkoutLink;
One invoice = one payment
Invoices are single-use. Once paid, the same invoice cannot receive another payment. If a customer needs to pay again (e.g. after a session timeout), create a new invoice.
For a reusable payment link (e.g. a donation page), use Pay Links instead.
What happens if a customer underpays?
If a customer pays less than the invoice amount, the invoice expires with additionalStatus: Underpaid and you receive an Expired webhook.
Confirming payment
Never trust the redirect. The redirect to your redirectUrl is a UX convenience — it can be spoofed or bypassed. Always confirm payment via webhook.
See Webhooks → Verification for the signature check.