Skip to main content
This page is written for LLMs and coding agents writing Taberna integrations, and for the humans reviewing what they produce. It is self-contained: everything an agent needs to avoid the expensive mistakes is stated here, without following links.
Machine-readable copies of this site are published automatically at /llms.txt and /llms-full.txt, and every page is available as Markdown by appending .md to its URL.

Non-negotiable rules

1. There is no test mode. Every payment method is a live blockchain and every payment moves real funds. Never generate code that “tests the payment flow end to end” expecting a sandbox. Never suggest a tbrn_test_ key — no such thing exists. The only key prefix is tbrn_live_.
2. Verify webhook signatures over the RAW request body, before parsing. The signature is HMAC_SHA256(secret, timestamp + "." + rawBody) in hex. If the framework parses JSON and you re-serialize it, key order or whitespace will differ and every verification will fail.
  • Express: express.raw({ type: 'application/json' }) on that route.
  • FastAPI / Starlette: await request.body().
  • Next.js App Router: await req.text().
  • Laravel: $request->getContent().
Compare in constant time — crypto.timingSafeEqual, hmac.compare_digest, hash_equals. Never ==.
3. Never log, echo or commit a raw key or secret. tbrn_live_… API keys and tbrn_whsec_… webhook secrets must come from environment variables or a secret manager. Do not print them in debug output, do not include them in error messages, do not write them into example files that get committed. An API key is shown exactly once at creation and cannot be recovered; a webhook signing secret, by contrast, stays readable in the dashboard, so a lost one is re-read rather than rotated.
4. Never ship an API key to a browser. The /v1/checkouts endpoints exist precisely so the buyer’s page needs no credential. If you find yourself putting an Authorization header in client-side code, the design is wrong.
5. Never grant value from a browser redirect. successUrl is an unauthenticated redirect the buyer controls. Fulfil only on a signature-verified webhook, or on a server-side read of GET /v1/orders/{id} or GET /v1/invoices/{id}.

Discover capabilities before assuming them

Call GET /v1/store at startup. It requires no scope, works with any valid key, and returns the calling key’s own scopes plus the store’s enabled payment methods:
Assert the scopes you need at boot and fail loudly, rather than discovering a missing scope during a customer’s checkout. Do not probe by calling an endpoint and catching the 403 — whoami exists so you never have to. The eight scopes: orders:read, orders:write, invoices:read, invoices:write, products:read, products:write, webhooks:read, webhooks:write.

Error handling rules

The error body is { "error": "…" } everywhere except POST /v1/invoices/{id}/send, which adds code and retryAfterSeconds. error is a human message, not a stable machine code — branch on status and, where documented, on code.

Money formatting rules

Fiat amounts are decimal strings, never numbers. "9.99", not 9.99. Up to 3 decimal places. Do not parse them into floats for arithmetic — use a decimal library or integer minor units. "50" means fifty, not fifty cents.Crypto amounts are integer strings in the asset’s smallest unit. "50010000" is 50.01 USDC, because USDC has 6 decimals. Do not compare them to fiat amounts, and do not assume 18 decimals.

Idempotency and retries

Always send an Idempotency-Key header on POST /v1/orders and POST /v1/invoices, derived from your own purchase id. 1 to 255 characters. A replayed key returns the original resource, whatever the new body says — Taberna does not compare bodies. Keys never expire. Never generate a random key per HTTP attempt: that defeats the entire purpose, because the retry of a timed-out create would mint a second charge.

Webhook handler requirements

Delivery is at-least-once. A correct handler:

Reads the raw body

Before any JSON parsing, for signature verification.

Verifies the HMAC in constant time

Reject non-matching requests with a non-2xx and stop.

Rejects stale timestamps

Taberna does not enforce a tolerance for you. Reject when abs(now_seconds - X-Taberna-Timestamp) > 300 — a 5-minute window. Without this, a captured request is replayable forever.

Applies the change idempotently against business state

Never dedup on X-Taberna-Delivery-Id alone. A manual resend re-queues the same delivery row, so it arrives with the same delivery id as the original — a handler that skips ids it has already seen silently drops every resend, which is precisely the traffic a merchant is replaying to fix a problem.Guard on your own state instead: has this data.id (or your extraContext.purchaseId / metadata.purchaseId) already been fulfilled? If yes, no-op. Record the delivery id for correlation if you like, but do not let it decide whether to act.

Responds 2xx fast

Success means a 2xx within about 10 seconds. Queue the real work; a handler that fulfils inline will time out under load and be replayed.
Reference handler:

Polling as a fallback, not a primary

Webhooks are the completion signal. When a webhook did not arrive, reconcile from your server against the authenticated endpoints — GET /v1/orders/{id} or GET /v1/invoices/{id} — and act on the status. Do not poll GET /v1/checkouts/{id} from your backend: it is the buyer-facing, unauthenticated view and it omits extraContext and metadata, which is what you need to correlate. Stop polling as soon as the status leaves pending (orders) or open (invoices). Every other state is terminal:
  • Orders: pendingcompleted | partial | expired
  • Invoices: openpaid | partial | expired | voided
Fulfil on completed / paid only. Never fulfil on partial unless a human has explicitly decided to honour underpayments — there is no automatic refund, and completion uses a strict >= comparison with no tolerance.

Things that do not exist — do not invent them

An agent’s most common failure here is fabricating a plausible endpoint. None of the following exist:
  • A test or sandbox mode, or a tbrn_test_ key prefix.
  • A refund endpoint, or any way to return funds through the API.
  • API routes to create or edit products, stores, invoice templates or API keys. All four are dashboard-only.
  • A second webhook URL, or per-event webhook subscriptions.
  • Currencies beyond usd and eur.
  • Product types beyond single-purchase.
  • Subscriptions, recurring billing or metered usage. Model these as repeated charges from your own scheduler.
  • A storeId parameter on any /v1 endpoint — the key carries the store.
The complete /v1 surface is: /v1/orders, /v1/invoices, /v1/invoice-templates, /v1/products, /v1/store, /v1/webhook-deliveries (all API-key authenticated) and /v1/checkouts (unauthenticated, buyer-facing). If a route is not in that list, it does not exist — check the API Reference tab rather than guessing.

Choosing orders versus invoices

When in doubt for a dynamic amount, use invoices — they need no product setup and carry metadata for correlation.

Minimal correct integration