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

# Emailing an invoice

> The send endpoint, its three-level rate limit, and the one error envelope that is not the standard shape.

`POST /v1/invoices/{id}/send` — requires `invoices:write`. **Takes no request body.**

The recipient is always the invoice's own stored `customerEmail` and cannot be
overridden. That is what stops the endpoint from being an open relay. Creating an
invoice never emails anyone; this call is the only thing that does.

```json 200 OK theme={"dark"}
{
  "invoiceId": "CcvnBwRQaKY1rXSMLrbYe",
  "sentTo": "buyer@example.com",
  "sentAt": "2026-07-09T12:05:00.000Z",
  "sendCount": 1,
  "sendsRemaining": 2,
  "maxSends": 3
}
```

<CodeGroup>
  ```bash cURL theme={"dark"}
  curl -X POST https://api.taberna.io/v1/invoices/CcvnBwRQaKY1rXSMLrbYe/send \
    -H 'Authorization: Bearer tbrn_live_…'
  ```

  ```javascript Node.js theme={"dark"}
  const res = await fetch(
  	`https://api.taberna.io/v1/invoices/${invoiceId}/send`,
  	{
  		method: 'POST',
  		headers: { Authorization: `Bearer ${process.env.TABERNA_API_KEY}` },
  	},
  );
  if (!res.ok) {
  	const { error, code, retryAfterSeconds } = await res.json();
  	// `code` is the branchable signal here, not the status.
  	throw new Error(`${code}: ${error} (retry in ${retryAfterSeconds ?? '—'}s)`);
  }
  ```

  ```python Python theme={"dark"}
  res = requests.post(
  	f"https://api.taberna.io/v1/invoices/{invoice_id}/send",
  	headers={"Authorization": f"Bearer {api_key}"},
  )
  if not res.ok:
  	body = res.json()
  	raise RuntimeError(f"{body.get('code')}: {body['error']}")
  ```
</CodeGroup>

## The three limits

Sending is capped at three independent levels. Any one of them can refuse a call.

| Level         | Limit                                                         |
| ------------- | ------------------------------------------------------------- |
| Per invoice   | **3** sends in total                                          |
| Per invoice   | **5 minutes** minimum between sends                           |
| Per store     | **100** invoice emails per rolling 24 hours                   |
| Platform-wide | **5,000** per rolling 24 hours, reported as a temporary pause |

These are server-configurable, so treat the numbers as current defaults rather than a
contract. The shapes — a small per-invoice allowance, a cooldown, and a store-level daily
budget — are the part to design against.

<Info>
  The `email` object on an invoice reflects **per-invoice state only**. It cannot tell
  you that the store's daily budget or the platform ceiling is what will refuse the
  next send, so `canSend: true` is a necessary condition, not a guarantee.
</Info>

## The rate-limit envelope

This is the **one endpoint whose error body differs** from the rest of the API. Every
failure the mailer produces carries three fields:

```json theme={"dark"}
{
  "error": "This invoice was emailed less than 5 minutes ago. Try again in 4 minutes.",
  "code": "rate_limited",
  "retryAfterSeconds": 214
}
```

* `code` is one of `not_found`, `not_sendable`, `rate_limited`, `send_failed`.
* `retryAfterSeconds` is **always present**, and `null` when the block is not a
  cooldown you can wait out (an exhausted per-invoice allowance, for example).
* When `retryAfterSeconds` is set, the same value is mirrored in a **`Retry-After`
  header**.

| Status | `code`         | When                                                                                                        |
| ------ | -------------- | ----------------------------------------------------------------------------------------------------------- |
| `400`  | `not_sendable` | The invoice is not `open`, or has no stored `customerEmail`                                                 |
| `404`  | `not_found`    | No such invoice for this key's store                                                                        |
| `429`  | `rate_limited` | A per-invoice cap, a per-invoice cooldown, a per-store 24-hour cap, or a platform-wide pause                |
| `502`  | `send_failed`  | The email provider rejected the send. **The attempt does not count against the allowance — safe to retry.** |

<Info>
  A request rejected **before** it reaches the mailer answers with the plain
  `{ "error": … }` shape instead, with no `code` and no `retryAfterSeconds`: a
  malformed `{id}` is `400 { "error": "Invalid request data" }`, and a well-formed id
  that does not decode is `404 { "error": "Invoice not found" }`. Read `code`
  defensively.
</Info>

## Read the allowance instead of probing

Every invoice payload — from create, retrieve and list — carries an `email` object, so
you never need a `429` to discover you are blocked.

| Field            | Meaning                                                             |
| ---------------- | ------------------------------------------------------------------- |
| `sendCount`      | Sends made so far for this invoice                                  |
| `sendsRemaining` | How many are left before the per-invoice cap                        |
| `maxSends`       | The per-invoice cap                                                 |
| `lastSentAt`     | Timestamp of the last send, or `null`                               |
| `cooldownUntil`  | When the minimum-interval cooldown lifts, or `null`                 |
| `canSend`        | Whether a send would be accepted right now                          |
| `blockedReason`  | A human-readable reason when `canSend` is `false`, otherwise `null` |

Check `email.canSend` before calling the send endpoint rather than treating a `429` as
your signal. It costs nothing — the field is on the invoice you already have.

```javascript theme={"dark"}
const invoice = await api(`/v1/invoices/${invoiceId}`);

if (!invoice.email.canSend) {
	// blockedReason is already phrased for a human.
	return { sent: false, reason: invoice.email.blockedReason };
}

await api(`/v1/invoices/${invoiceId}/send`, { method: 'POST' });
```

<Info>
  A merchant can send the same email from the dashboard, against the same allowance.
  See [Invoices from the dashboard](/guides/invoices-from-the-dashboard) for what the
  customer receives.
</Info>
