> ## Documentation Index
> Fetch the complete documentation index at: https://docs.taberna.io/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Taberna is a crypto payments API. Amounts are priced in fiat (USD or EUR) and paid in crypto.
> Fiat amounts are ALWAYS decimal strings ("9.99"), never numbers. Crypto amounts are integer strings in the asset's smallest unit.
> Authenticate merchant endpoints with `Authorization: Bearer tbrn_live_...`. Every key is bound to one store and carries an explicit scope set, so never pass a store id to a /v1 endpoint.
> A 401 means the credential is bad; a 403 means the key lacks a scope. Never retry or re-authenticate on a 403 — surface it as a configuration error.
> The /v1/checkouts endpoints are deliberately unauthenticated and run in the buyer's browser. Never send an API key to a browser.
> Verify webhook signatures over the RAW request body before parsing JSON, and treat delivery as at-least-once: handlers must be idempotent.
> Never grant value based on a browser redirect to successUrl. Fulfil on a signature-verified webhook, or on a server-side read of GET /v1/orders/{id} or GET /v1/invoices/{id}.
> There is no test mode: every payment method is a live chain moving real funds.

# Payment lifecycle

> State machines for orders, invoices and payment attempts — and what partial, confirmed and expired actually mean.

Three state machines are in play, and keeping them apart is most of what there is to
understand about Taberna.

* 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.
  Nothing gates on it.

## Orders

```mermaid theme={null}
stateDiagram-v2
    [*] --> pending: order created
    pending --> completed: an attempt receives the full amount
    pending --> partial: an attempt lapses holding partial funds
    pending --> expired: window elapses with nothing received
    pending --> expired: merchant expires it early
    completed --> [*]
    partial --> [*]
    expired --> [*]
```

| Status      | Payable | Meaning                                                                   |
| ----------- | ------- | ------------------------------------------------------------------------- |
| `pending`   | yes     | Initial state. Buyers may start and retry attempts.                       |
| `completed` | no      | Full amount received on-chain. **Terminal — fulfil here.**                |
| `partial`   | no      | An attempt lapsed holding insufficient funds. Terminal; resolve manually. |
| `expired`   | no      | The window elapsed with nothing received, or the merchant ended it early. |

## Invoices

```mermaid theme={null}
stateDiagram-v2
    [*] --> open: invoice created
    open --> paid: an attempt receives the full amount
    open --> partial: an attempt lapses holding partial funds
    open --> expired: window elapses with nothing received
    open --> voided: merchant voids it
    paid --> [*]
    partial --> [*]
    expired --> [*]
    voided --> [*]
```

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](/invoices#void-an-invoice).

## 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.

```mermaid theme={null}
stateDiagram-v2
    [*] --> processing: buyer picks a token
    processing --> abandoned: buyer picks a DIFFERENT token
    processing --> completed: balance reaches the quoted amount
    abandoned --> completed: balance reaches the quoted amount
    processing --> partial: window lapses holding partial funds
    abandoned --> partial: window lapses holding partial funds
    processing --> expired: window lapses holding nothing
    abandoned --> expired: window lapses holding nothing
    completed --> [*]
    partial --> [*]
    expired --> [*]
```

| Status       | Meaning                                                                                   |
| ------------ | ----------------------------------------------------------------------------------------- |
| `processing` | Live. The quote is locked and the deposit address is being watched. Initial state.        |
| `abandoned`  | The buyer switched to another token. **Still watched, still payable.**                    |
| `completed`  | Balance reached the quoted amount. Flips the parent to `completed` / `paid`.              |
| `partial`    | Window lapsed holding a non-zero but insufficient balance. Flips the parent to `partial`. |
| `expired`    | Window lapsed holding nothing. **The parent is untouched and stays payable.**             |

Three consequences worth internalising:

<AccordionGroup>
  <Accordion title="At most one live attempt per order or invoice" icon="lock">
    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.
  </Accordion>

  <Accordion title="Abandoned addresses keep working" icon="eye">
    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.
  </Accordion>

  <Accordion title="A lapsed empty attempt costs nothing" icon="arrows-rotate">
    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.
  </Accordion>
</AccordionGroup>

## Partial payments

<Warning>
  **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.
</Warning>

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:

| Balance at expiry          | Attempt becomes | Parent becomes            |
| -------------------------- | --------------- | ------------------------- |
| `>= cryptoAmount`          | `completed`     | `completed` / `paid`      |
| `> 0` and `< cryptoAmount` | `partial`       | `partial`                 |
| `0` or nothing received    | `expired`       | unchanged — still payable |

**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
automatic refund. 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. **Do not fulfil on
`partial` unless you have decided to honour underpayments.**

## Confirmations

`activeAttempt.confirmation` on the [public checkout payload](/checkout#confirmation-progress)
reports how deeply the inbound transfers are buried.

| `state`      | Means                                                                |
| ------------ | -------------------------------------------------------------------- |
| `waiting`    | No transfers on file yet. `confirmations` and `required` are `null`. |
| `detected`   | A transfer is known but its depth cannot be computed yet.            |
| `confirming` | Depth is known and below `required`.                                 |
| `confirmed`  | Depth has reached `required`.                                        |

`confirmations` is clamped to at most `required`, so it never overshoots. Both are
`null` when unknown.

<Warning>
  **`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`.
</Warning>

### Per-chain confirmation depths

| Chain     | Depth     | Nature                                                                   |
| --------- | --------- | ------------------------------------------------------------------------ |
| Ethereum  | 3 blocks  | Functional — balances are read at that depth                             |
| Base      | 10 blocks | Functional                                                               |
| BNB Chain | 15 blocks | Functional                                                               |
| Polygon   | 30 blocks | Functional                                                               |
| Solana    | 32 slots  | **Display only** — Solana settlement uses `finalized` commitment instead |

<Note>
  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".
</Note>

## Expiry windows

| Window                  | Default    | Range                | Set by                                            |
| ----------------------- | ---------- | -------------------- | ------------------------------------------------- |
| Order                   | 30 minutes | 10 minutes – 90 days | `expiresIn` on `POST /v1/orders`                  |
| Invoice                 | 24 hours   | 10 minutes – 90 days | `expiresIn` on `POST /v1/invoices`, or a template |
| Payment attempt (quote) | 30 minutes | —                    | Not settable                                      |

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.

<Warning>
  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](/orders#expire-an-order) rather than waiting them out.
</Warning>

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.

## Supported chains and currencies

Fiat pricing currencies: **`usd`** and **`eur`**.

| Chain (`blockchain`)  | Native coin | Stablecoins                      |
| --------------------- | ----------- | -------------------------------- |
| Solana (`solana`)     | `sol`       | `usdc_solana`                    |
| Ethereum (`ethereum`) | `eth`       | `usdc_ethereum`, `usdt_ethereum` |
| Base (`base`)         | `eth_base`  | `usdc_base`, `usdt_base`         |
| Polygon (`polygon`)   | `matic`     | `usdc_polygon`, `usdt_polygon`   |
| BNB Chain (`bnb`)     | `bnb`       | `usdt_bnb` — **no USDC**         |

Thirteen payment methods in total. Which of them a buyer can actually pick is your
store's `enabledPaymentMethods`, readable at any time from `GET /v1/store`.

<Tip>
  Stablecoins on a low-fee chain — `usdc_base`, `usdc_polygon` — are the smoothest
  default: the buyer's gas cost is negligible and there is no exchange-rate movement
  inside the quote window to explain away.
</Tip>

## Settlement

Each payment lands in the deposit wallet Taberna generated for that attempt, and is then
swept on-chain to **your store's payout wallet** for that chain — the address you
registered in the dashboard. There is no Taberna balance to withdraw from and no payout
schedule.

### The processing fee

The sweep splits the swept balance: **the processing fee goes to Taberna, the remainder
to your payout wallet**. The standard rate is 3%; it is set per store, so check the
dashboard for the rate that applies to yours. The deduction happens on-chain, in the
sweep transaction itself, so what arrives at your address is already net of it.

That is the only deduction Taberna makes:

| Cost                            | Paid by                                               |
| ------------------------------- | ----------------------------------------------------- |
| Network fee to send the payment | The buyer, from their own wallet                      |
| Processing fee                  | You — 3% as standard, taken on-chain during the sweep |
| Gas for the sweep transaction   | Taberna                                               |

Because sweep gas is covered by Taberna, the processing fee is the whole cost of
settlement.
Processors that charge a percentage and then bill network or withdrawal fees separately
deduct on more than one line.

### What gets swept, and when

A funded deposit wallet is swept once its attempt has **settled** — the sweepers pick up
attempts in `completed`, `expired` or `partial`. `expired` and `partial` are included so
that funds which arrived too late, or fell short of the quote, still reach your payout
wallet rather than being stranded.

An `abandoned` attempt is **not** swept while it is abandoned. It stays watched to the
end of its own window and then settles into `completed`, `partial` or `expired` like any
other attempt; the sweep follows from that settled status, not from the abandonment.

<Note>
  Sweep state is internal plumbing. It 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}`. Settlement progress is visible only in the dashboard. Your
  completion signal is the order or invoice `status`, never the sweep.
</Note>
