Skip to main content

The error shape

Every non-2xx response on the API carries the same body:
There is one documented exception: POST /v1/invoices/{id}/send, which adds code and retryAfterSeconds on every failure the mailer produces.
error is a human-readable message, not a stable machine code. Log it, surface it to an operator, branch on the HTTP status — but do not build control flow on exact string equality unless a page here documents that string as the signal (as the /void and /expire endpoints do, where the status alone is ambiguous).

Status conventions

A request to a path the API does not serve answers 404 with { "error": "Not found" } — distinct from the per-resource 404 messages below, and a useful signal that a URL is wrong rather than a resource missing. Three conventions worth stating explicitly.
A body, query string or path parameter that fails schema validation answers:
The specific field is deliberately not disclosed. Check your request against the API Reference schema rather than trying to parse the message.This includes malformed short ids, on every route that takes one. Order, invoice and checkout ids are validated against the short-id pattern — 21 or 22 base58 characters — before any lookup happens, so /v1/orders/nope is a 400 here, not a 404. 404 is reserved for an id that passes the pattern and then fails to decode, or decodes to a resource that does not exist. There is no per-route exception to this.
A resource that exists but belongs to a different store answers exactly like one that does not exist — 404, same message. This is deliberate: the alternative would let anyone with a key confirm the existence of another store’s ids.The same principle drives the checkout 404: an unknown order id and an unknown invoice id both return { "error": "Checkout not found" }, so the endpoint never reveals which resource was probed.One place does not follow it. Creating an order with a productId that exists but belongs to another store answers 400 { "error": "Product does not belong to this store" }, which does confirm the id exists. A product id that exists nowhere answers Product not found instead, so on POST /v1/orders the two cases are distinguishable. Everywhere else, another store’s resource is indistinguishable from a missing one.
401 is a bad credential — retrying with the same key will not help, and the body is always exactly { "error": "Unauthorized" } with the reason withheld. The WWW-Authenticate header carries the RFC 6750 challenge.403 is a good credential without the right scope, and names the scope: { "error": "Missing required scope: orders:write" }. Never retry it, and never re-authenticate or sign a user out on it. See Authentication.

Two endpoints where the status is ambiguous

On POST /v1/invoices/{id}/void and POST /v1/invoices/{id}/send, an id that is not a well-formed short id is a 400 { "error": "Invalid request data" } like everywhere else. Past that, both reserve 404 for an id that passes the pattern but fails to decode, and then report a decodable id with no invoice behind it differently:
  • /void answers 400 { "error": "Invoice not found" }.
  • /send answers 404 with the full { error, code, retryAfterSeconds } envelope and code: "not_found".
So on /void, “no such invoice” can arrive as either status depending on where the id failed. Branch on the message or the code for these two, not on the status alone.

Request limits

The 8 MiB cap is on encoded bytes, not characters. A stock-append text well under its 5,000,000-character limit can still 413 once non-ASCII content is UTF-8 encoded. See Appending stock.

Invoice email limits

Emailing an invoice is rate-limited at three levels — per invoice, per store over a rolling 24 hours, and platform-wide. A refusal is a 429 carrying retryAfterSeconds and a matching Retry-After header when the block is a cooldown you can wait out. Check email.canSend on the invoice rather than probing. Details in Email an invoice.

Idempotency

POST /v1/orders and POST /v1/invoices accept an optional Idempotency-Key header.

One key, one resource

A retried create with a key this store has already used returns the original resource — same id, same status code (201) — instead of minting a second one.

The body is not part of the key

The replay returns the original resource no matter how the request body changed. Taberna does not compare bodies and does not error on a mismatch. Reuse a key only for a retry of the same logical purchase.

Keys never expire

They are scoped to your store and kept indefinitely. A create months later that reuses an old key returns that old order or invoice. Derive keys from something naturally unique — your own purchase id — rather than a counter that could wrap.
A key outside the 1–255 character range is rejected with 400 { "error": "Idempotency-Key must be between 1 and 255 characters" }.
Sending the header is cheap and the failure mode it prevents — a network timeout on a create, retried, producing two charges — is expensive. Send it on every create.

Current limitations

Stated plainly. These are the things people are most often surprised by.

No test mode

There is no sandbox and no test key prefix. Every payment method is a live chain and every payment moves real funds. Develop against small amounts on a low-fee chain, and expect that reaching completed requires an actual transfer.

No refunds

Taberna does not issue refunds. Funds settle to your own payout wallet, so returning money is something you do from that wallet, off-platform, on your own terms.This includes underpayments: an attempt that lapses holding insufficient funds settles as partial, terminal, with no automatic return. See Partial payments.

One webhook URL per store, no event filtering

Every event for a store goes to a single endpoint. You cannot register a second URL, and you cannot subscribe to a subset of the ten event types — filter on event in your handler. See Webhooks.
Also true today:

Getting help

When something is wrong and you need to report it, include:
  • The resource short id (data.id, the value in the checkout link), or the product UUID.
  • For a webhook problem, the X-Taberna-Delivery-Id — it is stable across retries and matches the dashboard’s deliveries view exactly.
  • The HTTP status and the error message you received, verbatim.