Skip to content

The Storefront API

The Storefront API is your store for your own front end: a Next.js site, an app, or a page made by Claude Code, Lovable or v0. It serves your catalogue, search, the basket, the checkout and each order’s status at https://api.tuppence.ai/storefront/v1, with your publishable key (pk_test_… or pk_live_…). A basket priced through it costs exactly what the same basket costs on your Tuppence store, because Tuppence prices both the same way.

The publishable key is public by design. It reads only what your store shows anyone: never a customer, never a stock count beyond low stock, never an order without its status token.

It is on for every store. Under Store › Settings › Developers:

  • Storefront API on switches it off and on. Off, your publishable key gets nothing from it.
  • Allowed origins lists the sites whose browsers may call it, such as https://shop.example.com (https, no path, up to 20). Calls from a server always work.

Test and live are separate stores, each with its own list: set the origins in both modes. Your server can set them with your secret key:

Terminal window
curl -sS https://api.tuppence.ai/v1/sites/site_… \
-H "Authorization: Bearer $TUPPENCE_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"headless_origins": ["https://shop.example.com"]}'

Every request carries the key in the Tuppence-Publishable-Key header. Never a cookie: the basket is named by its token in the path.

Terminal window
curl -sS "https://api.tuppence.ai/storefront/v1/products?limit=3" \
-H "Tuppence-Publishable-Key: $TUPPENCE_PUBLISHABLE_KEY"
RouteWhat it answers
GET /siteThe store’s name, currency, address, brand tokens, menus and policy links
GET /products, GET /products/{slug}Products online, each priced all-in with its variants and availability
GET /collections, GET /collections/{slug}Collections and their products
POST /searchProducts matching {query}, with filters
GET /availability?variants=…in_stock, low_stock, out_of_stock or preorder per variant, never a count
POST /carts, GET /carts/{token}A new basket and its cart_token; the basket
POST /carts/{token}/lines, PATCH or DELETE …/lines/{id}Add, change or remove a line
POST /carts/{token}/discounts, …/gift_cardsA discount code; a gift card
POST /carts/{token}/delivery_optionsThe ways to deliver to {address}, with prices and dates
POST /checkouts, GET or POST /checkouts/{token}The checkout for a basket, and its snapshot
GET /checkouts/{token}/fields, POST …/payWhat the payment fields need; pay the snapshot’s due
GET /checkouts/{token}/order, GET /orders/{status_token}The order a payment made; its status page as data

The full reference, every field and error, is the Storefront API reference.

Every write answers the whole basket: its lines, totals (subtotal, discount, delivery, tax, total, due) and delivery_options. Show those as they come. Never add up prices or work out tax in your own code; a total sent in a request is ignored.

Keep the cart_token in the browser’s localStorage (a functional value, so no consent banner). A basket that has expired answers 404: forget the token and start a new one.

Your shop front takes the payment itself, on your page, through @tuppence/js:

  1. POST /checkouts with the cart_token, contact, shipping_address and delivery. The answer’s snapshot is what the shopper is shown and charged: its lines, delivery, tax and due.
  2. GET /checkouts/{token}/fields gives what @tuppence/js needs to mount its card fields. Card details go from those fields to the card network, never through your code or ours.
  3. POST /checkouts/{token}/pay with {amount: snapshot.due, confirmation_token}, then poll GET /checkouts/{token}/order until it has order.status_url.

Both POST /checkouts and pay need an Idempotency-Key header: a new one for each attempt, the same one when you retry it, so a retry never pays twice.

Rather not take the payment yourself? Send the shopper to your Tuppence store’s own checkout for the same basket: <your store's address>/checkout/c/<cart_token>. A drop product always goes there (409 drop_requires_hosted_checkout, with the link), so its queue stays fair.

In test mode, pay also takes payment_method: "pm_card_gb", a test card, with no card fields.

Each allowed origin is set up for Apple Pay and Google Pay when you add it. Apple also asks your site for one file, /.well-known/apple-developer-merchantid-domain-association. Serving it is your site’s part: it is the same file for every site, so copy Tuppence’s. Without it Apple Pay doesn’t appear, and nothing errors. Tuppence’s Next.js starter serves it for you.

StatusCodeWhy
403origin_not_allowedThe browser’s origin isn’t in Allowed origins. fix_url opens the setting
403storefront_api_offThe Storefront API is switched off. fix_url opens the setting
404resource_missingNo such product, or the basket expired
409drop_requires_hosted_checkoutA drop sells only on your store’s own checkout; the error has the link
429rate_limitedToo many calls from one address. Wait for Retry-After

Your live store answers to pk_live_… once your account is live. Add your origins again in live mode, and swap the key in your site’s settings. Nothing else changes.

Docs assistant

Open as a page
Loading the assistant…