Skip to main content
If you are trying to work out why an order is still 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, with paid 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 be processing 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 attempt abandoned, 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 parent pending / 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 quoted cryptoAmount. 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

partial is terminal on the parent and is not resolved automatically — there is no refund. Do not fulfil on partial unless you have decided to honour underpayments.
What you get:
  • An order.partial or invoice.partial webhook.
  • receivedAmount and cryptoAmount on the order, or under payment on the invoice, so the shortfall is cryptoAmount − receivedAmount in the token’s smallest unit.
  • transactions[], so you can trace what actually arrived and from where.
Deciding what to do — refund off-platform, honour the purchase anyway, ask the buyer to send the difference through a new charge — is a merchant policy call.

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.
Expiry is swept by a background pass rather than evaluated on read, so a resource can sit a few seconds past its 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 on GET /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.