Skip to main content

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:

StatusMeaning
NewInvoice created, waiting for payment
ProcessingPayment detected. On-chain it means waiting for block confirmation; Lightning passes through it on the way to Settled
SettledPayment confirmed and funds are being forwarded to your wallet
ExpiredInvoice expired before full payment was received
InvalidInvoice 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:

statusadditionalStatusMeaning
SettledNoneFull amount received, or short by less than the store's underpayment tolerance
SettledOverpaidCustomer paid more than the invoice amount
SettledPaidAfterExpirationPayment received after the invoice expired
ExpiredNoneNo payment at all
ExpiredUnderpaidShort 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
}
FieldWhat to do with it
idStore in your database to correlate with the webhook
checkoutLinkRedirect the customer here (easiest path)
lightningInvoiceRender as QR if building a custom UI
onchainAddressOn-chain fallback QR in custom UI
bip21BIP-21 URI combining address + amount — use for a unified QR
orderIdYour reference — echoed back in the webhook
createdAtUnix 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 redirectUrl after 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.