> ## 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.
> A merchant can run an entire shop with no code: the dashboard creates products and invoices, and every store can host a storefront at {slug}.taberna.io with its own theme, sections, branding and optional custom domain. Never assume the reader has an API integration.
> Taberna issues no refunds and performs no KYC. A refund is something the merchant sends from their own payout wallet, off-platform.

# Quickstart

> Take your first crypto payment — from an API key to a fulfilled order.

This walks the **order** flow end to end. If your amounts are decided per request rather
than drawn from a catalogue, read [Invoices](/invoices) instead: the setup is identical
and step 2 becomes a single `POST /v1/invoices` call with no product setup.

<Warning>
  **There is no test mode yet.** Every payment method is a live chain and every
  payment moves real funds. Develop against small amounts, and expect a real
  transfer to be needed before an order reaches `completed`.
</Warning>

## Before you start

Three things happen in the dashboard, not through the API. Create a store, enable at
least one payment method and set a payout wallet for its chain — walked through in
[Create your store](/guides/create-your-store). Then create at least one **listed**
product and note its `id`, a UUID: that is what you pass as `productId`. See
[Products](/guides/products).

A store with no payment methods enabled cannot create orders, and an unlisted product is
rejected at order creation with `Product is not available`.

<Steps>
  <Step title="Mint an API key with the scopes you need" icon="key">
    In the dashboard's developer settings, create a key. You **must** choose scopes;
    there is no implicit full-access key. For this quickstart:

    * `orders:write` — to create and expire orders
    * `orders:read` — to read them back

    The raw key is shown **once**, at creation. Taberna stores only a SHA-256 hash,
    so if you lose it, revoke and mint a new one.

    Verify it works with the whoami call, which needs no scope at all:

    <CodeGroup>
      ```bash cURL theme={"dark"}
      curl https://api.taberna.io/v1/store \
        -H 'Authorization: Bearer tbrn_live_3f1c2b9a7d4e4f889a102c6e5b4d1a90aabbccdd001122'
      ```

      ```javascript Node.js theme={"dark"}
      const res = await fetch('https://api.taberna.io/v1/store', {
      	headers: { Authorization: `Bearer ${process.env.TABERNA_API_KEY}` },
      });
      const store = await res.json();
      console.log(store.name, store.scopes, store.enabledPaymentMethods);
      ```

      ```python Python theme={"dark"}
      import os, requests

      res = requests.get(
      	"https://api.taberna.io/v1/store",
      	headers={"Authorization": f"Bearer {os.environ['TABERNA_API_KEY']}"},
      )
      store = res.json()
      print(store["name"], store["scopes"], store["enabledPaymentMethods"])
      ```
    </CodeGroup>

    The response carries the **calling key's own** `scopes`, and
    `enabledPaymentMethods` confirms your setup landed. See
    [the whoami endpoint](/authentication#the-one-endpoint-that-needs-no-scope) for
    the full body.
  </Step>

  <Step title="Create an order" icon="cart-shopping">
    <CodeGroup>
      ```bash cURL theme={"dark"}
      curl -X POST https://api.taberna.io/v1/orders \
        -H 'Authorization: Bearer tbrn_live_3f1c2b9a7d4e4f889a102c6e5b4d1a90aabbccdd001122' \
        -H 'Content-Type: application/json' \
        -H 'Idempotency-Key: purchase_8213' \
        -d '{
          "items": [{ "productId": "0197f8a0-1111-7aaa-8ccc-3e5d7c9b1a24", "quantity": 1 }],
          "customerEmail": "buyer@example.com",
          "extraContext": { "userId": "u_123" },
          "successUrl": "https://yourapp.com/thanks",
          "cancelUrl": "https://yourapp.com/pricing"
        }'
      ```

      ```javascript Node.js theme={"dark"}
      const res = await fetch('https://api.taberna.io/v1/orders', {
      	method: 'POST',
      	headers: {
      		Authorization: `Bearer ${process.env.TABERNA_API_KEY}`,
      		'Content-Type': 'application/json',
      		'Idempotency-Key': purchaseId,
      	},
      	body: JSON.stringify({
      		items: [{ productId: PRODUCT_ID, quantity: 1 }],
      		customerEmail: user.email,
      		extraContext: { userId: user.id },
      		successUrl: 'https://yourapp.com/thanks',
      		cancelUrl: 'https://yourapp.com/pricing',
      	}),
      });
      if (!res.ok) throw new Error((await res.json()).error);
      const order = await res.json();
      ```

      ```python Python theme={"dark"}
      import os, requests

      res = requests.post(
      	"https://api.taberna.io/v1/orders",
      	headers={
      		"Authorization": f"Bearer {os.environ['TABERNA_API_KEY']}",
      		"Content-Type": "application/json",
      		"Idempotency-Key": purchase_id,
      	},
      	json={
      		"items": [{"productId": PRODUCT_ID, "quantity": 1}],
      		"customerEmail": user.email,
      		"extraContext": {"userId": user.id},
      		"successUrl": "https://yourapp.com/thanks",
      		"cancelUrl": "https://yourapp.com/pricing",
      	},
      )
      if not res.ok:
      	raise RuntimeError(res.json()["error"])
      order = res.json()
      ```
    </CodeGroup>

    ```json 201 Created theme={"dark"}
    {
      "id": "CcvnKdAXzvW8h3JaS1VJe",
      "checkoutUrl": "https://taberna.io/checkout/CcvnKdAXzvW8h3JaS1VJe",
      "status": "pending",
      "expiresAt": "2026-07-09T12:30:00.000Z",
      "items": [
        {
          "productId": "0197f8a0-1111-7aaa-8ccc-3e5d7c9b1a24",
          "title": "Pro licence",
          "quantity": 1,
          "unitPrice": "50",
          "currency": "usd"
        }
      ]
    }
    ```

    Three things to do with that response:

    * **Persist `id`.** It is the order's public short id and the value every webhook
      for this order carries as `data.id`.
    * **Note `expiresAt`.** 30 minutes from creation by default; pass `expiresIn`
      (10 minutes to 90 days) to widen it.
    * **Use `extraContext`** to carry your own correlation data. It is echoed back
      verbatim in every webhook for this order, which is how you find the user again.

    <Info>
      The `Idempotency-Key` header makes the create safe to retry. A repeat with the
      same key returns the **original** order rather than minting a second one — no
      matter how the body changed.
    </Info>
  </Step>

  <Step title="Redirect the buyer to checkoutUrl" icon="arrow-right">
    Send the customer to `checkoutUrl`. Everything from there is Taberna's hosted
    page: the buyer picks a token from your enabled methods, gets a locked quote and
    a deposit address, pays, and watches confirmations tick up.

    When the payment completes, the page shows a **Continue** button pointing at your
    `successUrl`. The dead-end states — expired, partial — link to `cancelUrl`.
    Neither is required, but without `successUrl` the buyer simply stays on the
    hosted page.

    <Warning>
      Never grant anything based on the buyer arriving at your `successUrl`. It is a
      browser redirect with no authentication. Fulfil on the webhook, or on a
      server-side read of `GET /v1/orders/:id`.
    </Warning>
  </Step>

  <Step title="Learn that it was paid" icon="tower-broadcast">
    Two mechanisms, and you want the first.

    **Webhooks (recommended).** Set a webhook URL in the dashboard, save the returned
    `tbrn_whsec_…` secret, and handle `order.completed`:

    ```javascript Node.js (Express) theme={"dark"}
    import crypto from 'node:crypto';
    import express from 'express';

    const app = express();

    // The raw body is required — a re-serialized object will not match the signature.
    app.post(
    	'/webhooks/taberna',
    	express.raw({ type: 'application/json' }),
    	(req, res) => {
    		const timestamp = req.get('x-taberna-timestamp');
    		const received = req.get('x-taberna-signature');
    		if (!timestamp || !received) return res.sendStatus(400);

    		const expected = crypto
    			.createHmac('sha256', process.env.TABERNA_WEBHOOK_SECRET)
    			.update(`${timestamp}.${req.body}`)
    			.digest('hex');

    		const a = Buffer.from(expected);
    		const b = Buffer.from(received);
    		if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    			return res.sendStatus(400);
    		}

    		// Reject stale timestamps to blunt replays (5-minute tolerance).
    		if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    			return res.sendStatus(400);
    		}

    		const { event, data } = JSON.parse(req.body.toString('utf8'));
    		if (event === 'order.completed') {
    			// Idempotent: this may arrive more than once.
    			fulfil(data.extraContext.userId, data.id);
    		}

    		res.sendStatus(200); // Respond fast; do heavy work asynchronously.
    	},
    );
    ```

    **Polling (fallback).** Read the order back from your server whenever a webhook
    did not arrive:

    ```bash theme={"dark"}
    curl https://api.taberna.io/v1/orders/CcvnKdAXzvW8h3JaS1VJe \
      -H 'Authorization: Bearer tbrn_live_…'
    ```

    Act on `status === "completed"`. See [Webhooks](/webhooks) for the full event
    list and signature scheme.
  </Step>
</Steps>

## What to read next

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/authentication">
    The eight scopes, what each unlocks, and 401 versus 403.
  </Card>

  <Card title="Conventions" icon="ruler" href="/build/conventions">
    Money formats, timestamps, ids and the error shape.
  </Card>

  <Card title="Webhooks" icon="tower-broadcast" href="/webhooks">
    Payload shapes and signature verification in three languages.
  </Card>

  <Card title="Payment lifecycle" icon="diagram-project" href="/payment-lifecycle">
    State machines, partial payments, confirmation depths.
  </Card>
</CardGroup>
