A confirmation is not a unit of time. It is a unit of how hard it would be to take this money back.
The most common integration mistake we see is treating "1 confirmation" as a universal constant. It isn't. One confirmation on Bitcoin and one confirmation on Polygon describe completely different states of the world. If you wire your accounting to a confirmation count without knowing which chain produced it, you will eventually credit a deposit that later vanishes — and you'll spend a support cycle explaining a balance that went backwards.
This is the version of the explanation we give engineers during onboarding, before they ask why our invoice.paid event is sometimes seconds behind the block explorer and sometimes a couple of minutes.
What a confirmation actually buys you
When a transaction lands in a block, it isn't settled — it's proposed. Most chains can still reorganize: a competing block at the same height wins, the chain reorganizes around it, and any transaction that lived only in the orphaned block is gone. A confirmation is just another block stacked on top of the one holding your transaction. Each additional block makes rewriting history exponentially more expensive, because an attacker would have to out-produce the honest network across every block in between.
So the question a payment processor has to answer for every chain is: how many blocks deep is "deep enough" that we'll treat this as final and never expect to walk it back? That number is the confirmation threshold, and it's different on every chain for concrete reasons.
Why the thresholds differ
Three properties drive the threshold, and they pull in different directions.
Block time. Bitcoin produces a block roughly every ten minutes; Solana lands one in well under a second. A six-block wait on Bitcoin is about an hour of wall-clock time. Six blocks on a fast chain is nothing. So the count that buys equivalent safety scales inversely with how fast blocks arrive — fast chains need more blocks to represent the same economic finality.
Finality model. Some chains offer deterministic finality: once a block is finalized by the consensus protocol, it cannot be reorganized without the network catastrophically failing. Others — Bitcoin most famously — offer only probabilistic finality, where the chance of a reversal shrinks with depth but never mathematically hits zero. A chain with an explicit finality gadget lets us wait for the finalized checkpoint instead of counting raw blocks. A purely probabilistic chain forces us to pick a depth where the reversal odds are negligible for the amounts involved.
Reorg history. Some chains reorganize their tip routinely as a normal part of operation; short, shallow reorgs are background noise. A chain that frequently drops its most recent block needs a deeper buffer before we believe a transaction is staying put. We tune per-chain thresholds against the actual observed reorg behavior of each network, not a theoretical worst case pulled from a whitepaper.
Put those together and you get the spread we expose across the rails on the networks page: a small number of blocks on a deterministic-finality chain can be safer than a much larger number on a probabilistic one. The count alone tells you nothing. The count plus the chain tells you everything.
Reorg-aware crediting, concretely
Here is the part that actually protects merchant balances. We do not credit a deposit the instant it appears in a block. We watch it, and we are prepared for it to disappear.
A deposit moves through observable states. When we first see the transaction in a block, the invoice goes to invoice.confirming — seen, not trusted. We then wait for the chain's threshold. Only when the deposit is buried deep enough that a chain reorganization would have to be implausibly large do we fire invoice.paid and credit the balance.
Crucially, the path between those two states is not assumed to be one-way. If the chain reorganizes and the deposit's block is orphaned before it cleared the threshold, the deposit is no longer real — and we surface that. That is what the invoice.deposit_reversed event is for: a deposit we had observed is no longer part of the canonical chain. A invoice.late_deposit covers the inverse — funds that arrive after we'd already moved on. None of these are error states. They are the honest mechanics of chains that can rewrite their own recent history.
The block explorer shows you what the chain currently says. A reorg-aware processor shows you what the chain has committed to. Those are the same most of the time, and the gap between them is exactly where money gets lost.
Why this is the whole point of a paid event
If you take one thing from this: invoice.paid is a promise, not an observation. It does not mean "we saw a transaction." It means "we saw a transaction, we waited for this specific chain's threshold, the deposit survived, and we will not be walking this balance back." That distinction is the entire reason to integrate against our events instead of polling a block explorer yourself.
It also explains the timing you'll notice in practice. On a fast deterministic-finality chain, invoice.paid can land within seconds of the deposit — because finality genuinely arrives that fast. On Bitcoin, the same event takes longer, because the safety you're being sold takes longer to manufacture. The lag isn't our system being slow. It's our system refusing to lie to you about settlement.
A few operating rules that fall out of this model:
- Never act on
invoice.confirming. It's a UI signal at best — show the customer "we see your payment." Don't release goods, don't update a ledger. - Make
invoice.paidhandling idempotent and reversal-aware. Credit onpaid; have a defined response todeposit_reversed. (Webhook delivery guarantees at-least-once, so dedupe regardless — see designing webhooks that survive everything.) - Don't hardcode a single confirmation count across chains. The threshold is the network's property, not yours. We own it per rail so you don't have to.
The mental model we use internally
A confirmation count is a coordinate, and the chain is the map it's plotted on. Read either one alone and you're guessing. The job of a payment processor is to fold both into a single boolean — has this value held? — and only then tell you it's money. Every per-chain threshold, every reorg watcher, every confirming → paid transition exists to make that boolean trustworthy.
We do the per-chain bookkeeping so your code gets to be simple: wait for invoice.paid, handle the rare deposit_reversed, and never count blocks yourself.