The error shape
Every non-2xx response on the API carries the same body:POST /v1/invoices/{id}/send,
which adds code and retryAfterSeconds on every failure the mailer produces.
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.
Validation failures are always the same 400
Validation failures are always the same 400
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.Another store's resources are missing, not forbidden
Another store's resources are missing, not forbidden
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 and 403 are not interchangeable
401 and 403 are not interchangeable
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
OnPOST /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:
/voidanswers400 { "error": "Invoice not found" }./sendanswers404with the full{ error, code, retryAfterSeconds }envelope andcode: "not_found".
/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
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 a429 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.
400 { "error": "Idempotency-Key must be between 1 and 255 characters" }.
Current limitations
Stated plainly. These are the things people are most often surprised by. 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
errormessage you received, verbatim.