<!-- Generated from Btab's docs/api/STOREFRONT_API.md. The same guide as a web page: https://btab.app/developers/storefront-api -->

# Storefront API

Build your store's own website on any domain with btab's products, cart and freight. The buyer pays on your store's btab checkout.

How it works:
1. Your site reads products, prices and stock with a **publishable key**.
2. It builds a **cart on btab**. btab prices every line.
3. It asks for **freight by postcode**.
4. It opens a **checkout session** and sends the buyer to its `continue_url`.
5. The buyer pays on your store's **btab checkout**.
6. Your server gets the `checkout_session.completed` webhook, and the buyer goes back to your `return_url`.

## 1. Make a publishable key
In the btab dashboard: **Settings → Developers → Publishable keys → Make a key**. List every site the key is used on, for example `https://www.yourshop.com.au` and `http://localhost:3000` (up to 5). The key is shown once.

- A publishable key (`btab_pk_live_…`) is safe in page code. It can read your shop window, build carts, ask for freight, send an enquiry and open a checkout session. It cannot read buyers or orders, and it cannot change anything in your store.
- It works only from the sites you list. Answers carry CORS for those sites only.
- **Never put a store API key (`btab_live_…`) in page code.** The Storefront API refuses one.
- A **test key** (`btab_pk_test_…`) works on btab's demo store and ends in "Place test order". Nothing is charged or sent.

Send the key in the `X-Btab-Storefront-Key` header (or `Authorization: Bearer`).

## 2. Read the shop window
Base URL: `https://api.btab.app/api/storefront/v1`

| Request | Answer |
|---|---|
| `GET /site` | your store's public settings |
| `GET /products` | the product list (filters as on your store) |
| `GET /products/{id}` | one product, with options |
| `GET /products/{id}/reviews`, `/related` | reviews, related products |
| `GET /categories`, `/search?q=`, `/facets`, `/collections/{slug}` | browsing |

```js
const KEY = 'btab_pk_live_…';
const api = (path, init = {}) =>
  fetch(`https://api.btab.app/api/storefront/v1${path}`, {
    ...init,
    headers: { 'X-Btab-Storefront-Key': KEY, 'Content-Type': 'application/json', ...(init.headers || {}) },
  }).then((r) => r.json());

const { products } = await api('/products?limit=24');
```

## 3. Build a cart
Send product ids, options and quantities, never a price. btab prices every line the way your checkout does.

```js
const { cart } = await api('/carts', {
  method: 'POST',
  headers: { 'Idempotency-Key': crypto.randomUUID() },
  body: JSON.stringify({ items: [{ product_id: 123, variant_id: 456, quantity: 2 }] }),
});
// cart.id = 'cart_…' (keep it; it is the cart's secret)
// cart.lines[]: unit_price_cents, line_total_cents, available, enquiry_only, needs_option, max_quantity
// cart.subtotal_cents, cart.prices_include_gst
```

- `PATCH /carts/{id} { items }` replaces the lines. `GET /carts/{id}` prices them again.
- A cart lives 72 hours after its last change.
- `Idempotency-Key` is optional on `POST /carts`, `POST /carts/{id}/enquiry` and `POST /checkout_sessions`. Use a UUID. A retry with the same key and body returns the first answer.

## 4. Freight and quotes
```js
const { estimate } = await api(`/carts/${cart.id}/shipping-estimate`, {
  method: 'POST',
  body: JSON.stringify({ postal_code: '2000' }),
});
// estimate.available, estimate.shipping_cents
// estimate.refusal_code = 'SHIPPING_VOLUME_UNKNOWN' → "needs a quote"
```

Items marked `enquiry_only` cannot be bought online. Send them as a quote request:

```js
await api(`/carts/${cart.id}/enquiry`, {
  method: 'POST',
  body: JSON.stringify({ email: 'buyer@example.com', name: 'Ann', message: 'For 12 people' }),
});
```

## 5. Send the buyer to checkout
```js
const { checkout_session } = await api('/checkout_sessions', {
  method: 'POST',
  headers: { 'Idempotency-Key': crypto.randomUUID() },
  body: JSON.stringify({ cart_id: cart.id, return_url: 'https://www.yourshop.com.au/thanks' }),
});
window.location.href = checkout_session.continue_url;
```

- `continue_url` opens your store's btab checkout with the cart filled in. It works for 30 minutes.
- `return_url` must be on one of the key's sites. After paying, the buyer sees a "Back to …" link to it.
- `GET /checkout_sessions/{id}` shows `open`, `complete` or `expired`.

## 6. Know when it is paid
Subscribe to the `checkout_session.completed` webhook (Settings → Developers → Webhooks). It fires once, when the order is paid:

```json
{ "event": "checkout_session.completed", "data": { "livemode": true,
  "checkout_session": { "id": "cs_…", "status": "complete", "order_number": "ORD-…" } } }
```

`order.paid` and `order.refunded` are also offered to stores with the Storefront API. Verify every delivery's `X-Btab-Signature` (see [WEBHOOKS.md](/developers/webhooks.md)).

## 7. Test first
Make a **test key**. It reads btab's demo store (made-up goods). Its checkout session ends in "Place test order": an order marked as a test, never charged or delivered. The webhook says `"livemode": false`.

## Limits
| What | Limit |
|---|---|
| Reads | 120 a minute per buyer address per key; 3000 a minute per key |
| Cart writes | 20 a minute per buyer address per key |
| Enquiries | 8 per 10 minutes per buyer address per store |
| Requests without a valid key | 30 a minute per address |

Over a limit, the answer is `429` with `Retry-After`.

## Errors
Every refusal has a `code`, for example `ORIGIN_NOT_ALLOWED`, `STORE_API_KEY_REFUSED`, `CART_NOT_FOUND`, `CART_NOT_BUYABLE`, `RETURN_URL_NOT_ALLOWED`, `WEAK_IDEMPOTENCY_KEY`, `IDEMPOTENCY_KEY_REUSED`.
