pending, what partial commits you
to, or whether confirmed means paid, this is the page.
Three state machines are in play, and keeping them apart is most of what there is to
understand:
- The order or invoice is the thing you created. It is payable, then terminal.
- A payment attempt is one quote-locked session under it. Several may come and go during a single order’s life.
- A confirmation is a presentation-layer read of how deeply a transfer is buried in the chain. Nothing gates on it.
Orders
Two things that are easy to get wrong. Picking a coin does not change the order’s
status — it creates a payment attempt under the order, which stays
pending. And
an attempt that lapses with zero funds does not affect the order; the buyer can keep
retrying with fresh quotes until the order itself expires.
Invoices
Same machine as orders, withpaid in place of completed and one extra terminal
state, voided, for a merchant cancelling before payment. Voiding is refused while an
attempt is live — see the void guard.
Payment attempts
An attempt is created when the buyer picks a token. It locks a crypto quote, mints a fresh deposit wallet used for that attempt alone, and is watched until it resolves.
Three consequences worth internalising.
At most one live attempt per order or invoice
Only one attempt can beprocessing at a time. Re-selecting the token that is already
live returns the same attempt — same address, same quote — so reloading the checkout
page is safe and does not mint a second address.
Abandoned addresses keep working
Switching tokens marks the old attemptabandoned, but it stays watched until its own
window ends, and it can still complete. Funds a buyer broadcast just before switching are
not lost. Abandonment emits no webhook — it is a UI affordance, not a lifecycle event.
A lapsed empty attempt costs nothing
An attempt that expires holding zero leaves the parentpending / open. The buyer picks
a token again, gets a fresh quote on a fresh address, and can keep doing that until the
parent itself expires.
Partial payments
There is no underpayment tolerance. Completion is a strict comparison: the balance must be greater than or equal to the quotedcryptoAmount. A payment one base unit
short does not complete.
While an attempt is live, an insufficient balance simply sits there — Taberna keeps
re-reading the deposit address, so the buyer can top it up and the attempt completes
the moment the total clears the quote. Nothing is decided until the window closes.
At the attempt’s expiresAt, exactly one rule applies:
Overpayment completes normally. The comparison is
>=, so a buyer who sends more
than quoted gets a completed order; the surplus is part of the swept amount.
Surfacing a partial
What you get:- An
order.partialorinvoice.partialwebhook. receivedAmountandcryptoAmounton the order, or underpaymenton the invoice, so the shortfall iscryptoAmount − receivedAmountin the token’s smallest unit.transactions[], so you can trace what actually arrived and from where.
Confirmations
activeAttempt.confirmation on the public checkout
payload reports how deeply the inbound
transfers are buried.
confirmations is clamped to at most required, so it never overshoots. Both are
null when unknown.
stale: true means the counts cannot be trusted — the chain-head reading behind
them is missing or older than 60 seconds. Do not render progress while it is set.And confirmed is not “paid”. It says the transfers on file are buried deeply
enough; it says nothing about whether they add up. An underpayment can sit at
confirmed indefinitely. The authoritative signal is always the order or invoice
status.Per-chain confirmation depths
A slot is Solana’s block interval, and
finalized is its strongest commitment level:
the point past which the network will not reorganise a block away.
These values reach the API through the chain indexers rather than being hardcoded in
it, which is why required can legitimately be null: no depth has been published for
that chain yet. Treat a null as “unknown”, never as “zero”.
Expiry windows
The attempt window is precisely
min(now + 30 minutes, the parent's expiresAt). A
locked quote must never outlive its parent, so an attempt started five minutes before an
order expires gets five minutes, not thirty.
A wider order window holds reserved delivery stock out of circulation for exactly as
long. If you widen
expiresIn for finite-stock products, expire abandoned orders
through the expire endpoint rather than waiting them out.expiresAt before its status changes. Poll for the status
change; do not compute expiry client-side.
After completion
Once an attempt settles, its deposit wallet is swept on-chain to your store’s payout wallet, net of the processing fee. Sweep state appears nowhere on the public API — not on the buyer-facing checkout payload and not onGET /v1/orders/{id} or
GET /v1/invoices/{id}. Your completion signal is the order or invoice status, never
the sweep.
How you get paid
Deposit wallet, sweep, and what lands in your address.
Fees
What is deducted, and who pays the network fee.
Payment methods
The 13 methods, the five chains, and which to enable.
Webhooks
The events each of these transitions emits.