Skip to main content
An order is a purchase of one or more listed products, each with a quantity. Taberna computes the total from the products’ own prices, reserves any finite delivery stock for the order’s lifetime, and hands you a hosted checkout link. Use orders when your prices live in a catalogue. When the amount is composed per request, use Invoices instead. Amounts, timestamps and ids follow the conventions that hold across the API.

Create an order

POST /v1/orders — requires the orders:write scope.

Request body

Exact types, bounds and formats for every field are in the API Reference; the bounds that bite are collected on Request limits. About items:
  • Every product must belong to the key’s store, be listed, and share a single currency with the rest of the cart.
  • A productId that appears more than once has its quantities summed into one line — a cart is deduplicated, not rejected.
  • The order total is Σ (product price × quantity).
A longer expiresIn holds any reserved delivery stock out of circulation for exactly as long. Widen the window deliberately, not by default.

The Idempotency-Key header

Optional, 1 to 255 characters, unique per store. Retrying a create with a key this store has already used returns the original order — same id, same everything — instead of minting a duplicate, no matter how the request body changed. Keys never expire, so use one per logical purchase; your own purchase id is the natural choice. Full semantics in Idempotency.

Example

201 Created
unitPrice is a fiat decimal string with no trailing-zero padding — "50" is $50.00. The create response deliberately returns only what you need to proceed; GET /v1/orders/{id} returns the full order.

Errors

All of these are 400 with a { "error": … } body. Anything the request schema rejects never reaches the order logic — the validator answers first, always with the generic { "error": "Invalid request data" }. The messages below are the ones the order logic itself produces, once the body’s shape is already known to be valid.
Quantity bounds come from the product’s delivery configuration and default to a minimum of 1 and no maximum. See Quantity bounds.
Product does not belong to this store is the API’s one deliberate exception to reporting another store’s resources as simply missing. Treat it as a signal you have crossed keys and stores, not as a lookup to depend on — see Errors.

Retrieve an order

GET /v1/orders/{id} — requires orders:read. {id} is the order’s public short id.
200 OK
paymentMethod, cryptoAmount, receivedAmount and transactions all describe a single payment attempt, chosen in this order: the attempt that completed the order; otherwise the best-funded one, holding the largest receivedAmount; otherwise the most recent. They are null (and transactions is []) while no attempt has been started.
The best-funded tier is not a detail. Switching token abandons an attempt but leaves it watched and payable, so the attempt that actually took the money is often older than the last quote the buyer requested. Picking it here is what keeps these fields agreeing with the order.partial webhook, which reports the attempt the indexer credited. A well-formed id that does not resolve — undecodable, unknown, or an order belonging to another store — answers 404 with { "error": "Order not found" }. An order you cannot see is reported as missing, never as forbidden. A malformed id is a 400; see Errors.

List orders

GET /v1/orders — requires orders:read. Newest first.
200 OK

Expire an order

POST /v1/orders/{id}/expire — requires orders:write. Ends a pending order now rather than waiting out its window. This fires the order.expired webhook and returns any reserved delivery stock to the pool, which is the main reason to call it: an abandoned cart holding the last five licence keys is worth reclaiming. Returns 200 with the full order, now expired. Like voiding an invoice, the call retires every still-watchable attempt first, so no lapsed-but-unreaped deposit address is left collecting funds for an order that can no longer be paid. If one of those attempts was holding funds, retiring it turns the order partial — terminal — and the call then fails with Only pending orders can be expired. Re-read the order to see the partial status. An order that was already expired when the call arrived is refused with Only pending orders can be expired. The one case that returns a body instead is the narrow race where a concurrent request expires it after this one read it — then you get the expired order back, because your intent already holds.

Statuses

Note that picking a coin does not change the order’s status — it creates a payment attempt under the order, which stays pending. See Payment lifecycle for the attempt-level state machine and what each transition means.

Multi-item and currency rules

One currency per order

An order snapshots exactly one currency, taken from its products. Mixing a USD product with a EUR product is All items must use the same currency. If you sell in both, keep separate carts.

Duplicate products are merged

The response’s items array may therefore be shorter than the array you sent.

Prices are snapshotted

items[].title, unitPrice and image are captured at creation. Editing the product later never rewrites a past order — which is what makes webhooks and receipts trustworthy after the fact.

Stock reservation is all-or-nothing

Every line item that needs finite stock reserves it inside one transaction. If any line cannot, the whole create fails with Product is out of stock and nothing is held. Reservations are released when the order expires.