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

# Storefront tutorial: your own shop on btab

Build a shop on your own domain that sells btab products. You start with a test key and a ready-made starter shop. In about 30 minutes you place a test order. Nothing is charged and nothing is sent.

```mermaid
flowchart TD
  K[Test key<br/>btab dashboard] --> S[Starter shop<br/>localhost:3000]
  S -->|products, cart, freight| A[Storefront API]
  S -->|Check out| C[Checkout page<br/>Place test order]
  C -->|Back to your shop| T[Your thank-you page]
```

What you build:
1. A product list and product pages, with prices, stock and options from btab.
2. A cart that btab prices, and a freight price by postcode.
3. A "Check out" button that sends the buyer to the store's checkout, and a thank-you page they come back to.

The full API reference is the [Storefront API guide](/developers/storefront-api.md).

## 1. Get a test key
1. Sign in to your btab dashboard. No account yet? Sign up at https://btab.app: a store is ready at once.
2. Go to **Settings → Developers → Publishable keys → Make a key**.
3. Tick **Test key**. (If the Storefront API is not open to your store yet, the form makes a test key anyway.)
4. Under **Allowed sites**, enter `http://localhost:3000`.
5. Press **Make key**. Copy the key now: it starts with `btab_pk_test_` and is shown once.

You should see the key in the list with a **test** badge.

A test key reads btab's demo store, a shop of made-up goods. Its checkout ends in **Place test order**. Nothing is charged, delivered or invoiced.

## 2. Run the starter shop
You need Node.js 20 and git.

```bash
git clone https://github.com/SteveDeals/btab-storefront-starter.git
cd btab-storefront-starter
npm install
cp .env.example .env.local
```

Open `.env.local` and paste your key:

```bash
NEXT_PUBLIC_BTAB_PUBLISHABLE_KEY=btab_pk_test_…
NEXT_PUBLIC_SITE_URL=http://localhost:3000
```

Start it:

```bash
npm run dev
```

Open http://localhost:3000. You should see five made-up products from the demo store:

| Product | What it shows |
|---|---|
| Demo Mug | a plain product |
| Demo Cushion | three colours: Red, Blue, Green |
| Demo Lamp | **Sold out**: the button is off |
| Demo Boardroom Table | **Enquire**: sold by quote, no Add to cart |
| Demo Oversized Crate | a large item with no size data (see Known issues) |

Other ways to run it:
- **Docker:** `docker compose --env-file .env.local up --build`, then open http://localhost:3000.
- **GitHub Codespaces:** open the repo in a codespace. Add the codespace's address (for example `https://<name>-3000.app.github.dev`) to your key's **Allowed sites**, and set `NEXT_PUBLIC_SITE_URL` to it.

Seeing "The shop is not available right now"? The page shows the reason under it while you develop:
- `INVALID_KEY`: the key in `.env.local` is wrong or was revoked.
- `ORIGIN_NOT_ALLOWED`: the address in your browser is not on the key's allowed sites.
- `NETWORK_ERROR` in the browser: a site that is not allowed looks like this too, because the browser hides the refusal.

## 3. Cart, freight and a test order
1. Open **Demo Cushion**, choose **Blue**, press **Add to cart**.
2. Open **Demo Mug**, press **Add to cart**. The cart shows both lines and a subtotal that btab worked out.
3. Under **Delivery**, enter postcode `2000` and press **Get freight price**. You should see a delivery price, for example `Delivery $16.95`.
4. Press **Check out**. The starter asks btab for a checkout session and sends you to its `continue_url`: the demo store's checkout page, with your cart filled in.
5. Press **Place test order**.
6. Press the **Back to …** link. You land on http://localhost:3000/thanks, which says **Thank you**.

How the starter does it (`lib/btab.ts` wraps each call):

| Step | Call |
|---|---|
| Product list | `GET /products` |
| Product page | `GET /products/{id}` |
| Add to cart | `POST /carts`, then `PATCH /carts/{id}` |
| Freight | `POST /carts/{id}/shipping-estimate` with `postal_code` |
| Quote request for the table | `POST /carts/{id}/enquiry` |
| Check out | `POST /checkout_sessions` with `cart_id` and `return_url` |
| Thank-you page | `GET /checkout_sessions/{id}`: `complete` |

**Where is the test order?** A test order belongs to btab's demo store, not to your store. So it is not in your dashboard's orders. You see it in two places:
- The thank-you page reads the checkout session: its status is `complete`.
- If you subscribe to the `checkout_session.completed` webhook (Settings → Developers → Webhooks), your server gets it with `"livemode": false`. See [Webhooks](/developers/webhooks.md).

## 4. WooCommerce
Already run a WooCommerce shop? The plugin copies btab products into it as real WooCommerce products: title, price, photos, stock, options and categories. The photos go into your own Media Library. Code and issues: https://github.com/SteveDeals/btab-for-woocommerce.

You need WordPress 6.5 or later, WooCommerce 9.0 or later and PHP 7.4 or later.

1. Add your shop's address (for example `https://www.yourshop.com.au`) to your test key's **Allowed sites** (step 1).
2. Open https://github.com/SteveDeals/btab-for-woocommerce, press **Code → Download ZIP**. (Until release zips are attached to the repo, this source zip is the install file.)
3. In WordPress: **Plugins → Add New → Upload Plugin**, choose the zip, **Install**, **Activate**.
4. Go to **WooCommerce → btab**. Paste your key, press **Save**, then **Test connection**. You should see the store's name.
5. Press **Sync now**. Under **Products** you should see the five demo products. The lamp says out of stock, the table offers an enquiry instead of Add to cart, and the cushion has three colours.

What it does today, and what is still being built:

| | |
|---|---|
| Product import, photos, options, sold out, sold by quote | works |
| Freight priced by btab at the cart | being built |
| **Place order** hands the cart to the store's checkout | being built |
| Paid orders copied back into WooCommerce | being built |

Until the hand-off ships, a WooCommerce shop cannot place a test order. Use the starter shop (steps 2 and 3) to see the whole flow.

## 5. Keep btab out of your buyer's view
The buyer pays on the store's checkout page. Give it your own address:
1. In the btab dashboard, go to **My store → Your web address → Already have a domain?** and enter a subdomain such as `checkout.yourshop.com.au`.
2. At your DNS provider, add the record the dashboard shows you, then wait until the dashboard says the domain is live.

From then on, `continue_url` opens on `checkout.yourshop.com.au`. The page carries your store's name and design.

In your own code:
- Name things after your shop, not after btab.
- Load photos through your own server if you want no btab address in the page. The starter serves every photo from `/photo/<token>` on the shop's own address (`app/photo/[token]/route.ts`).

## 6. Going live
1. Make a **live key** (`btab_pk_live_…`) in **Settings → Developers**. Live keys open when the Storefront API is open to your store. If **Make a key** offers only test keys, ask btab to open it.
2. List every address the key is used on under **Allowed sites**: `https://www.yourshop.com.au`, and `http://localhost:3000` if you still develop against it. A key takes up to 5.
3. Set `return_url` to a page on one of those sites. A `return_url` anywhere else is refused with `RETURN_URL_NOT_ALLOWED`.
4. Point your checkout subdomain at btab (step 5).
5. Swap the key in your settings and deploy.

A live key's checkout takes real payment. The order arrives in your btab dashboard like any other order from your store.

## 7. Limits, errors and what the API never returns
**Allowed sites.** A browser call is accepted only from the key's allowed sites, compared as the whole address: `https://evil-yourshop.com.au` is not `https://yourshop.com.au`. Answers carry CORS for those sites only. A call from a server (no `Origin` header) is accepted. The key's powers are narrow on purpose, so a publishable key is safe in page code.

**Never put a store API key (`btab_live_…`) in page code.** The Storefront API refuses one (`STORE_API_KEY_REFUSED`). Revoke it if it was ever exposed.

**Rate limits.** Over a limit, the answer is `429` with `Retry-After` in seconds.

| What | Limit |
|---|---|
| Reads | 120 a minute per buyer address per key; 3000 a minute per key |
| Cart changes | 20 a minute per buyer address per key |
| Enquiries | 8 per 10 minutes per buyer address per store |

Calls from your own server all share your server's address. Call from the browser, or cache reads on the server, as the starter does for product pages (60 seconds).

**Errors you will meet.** Every refusal has a `code`. The starter turns each one into a plain sentence (`BtabError.friendly` in `lib/btab.ts`).

| Code | Meaning | What to do |
|---|---|---|
| `ORIGIN_NOT_ALLOWED` | the page's site is not on the key | add it under Allowed sites |
| `RETURN_URL_NOT_ALLOWED` | `return_url` is not on the key's sites | use a page on an allowed site |
| `CART_NOT_FOUND` | the cart expired (72 hours after its last change) or the id is wrong | start a new cart |
| `CART_NOT_BUYABLE` | a line is sold out, enquiry only, or needs an option | show `unavailable_reason` per line |
| `TOO_MANY_BAD_KEYS` | too many calls with a wrong key from one address | fix the key, wait a minute |

On a cart line, `unavailable_reason` says why it cannot be bought: `sold_out`, `low_stock`, `enquiry_only`, `needs_option` or `not_available`. An `enquiry_only` product is sold by quote: send its lines with `POST /carts/{id}/enquiry`.

**What the API never returns.** It returns what the store's own shop window shows: names, descriptions, photos, prices, stock, options, reviews. It never returns cost prices, supplier details, other stores' data, buyers or orders.

## Known issues
Written as they stand on 2026-10-21.
- **The large crate quotes $0 freight.** The demo store's Oversized Crate has no weight or size. It should answer "needs a quote" (`refusal_code: SHIPPING_VOLUME_UNKNOWN`). Instead it quotes `shipping_cents: 0` with `free_over_met: true`, although the demo store has no free-shipping threshold. The store's own checkout gives the same answer, so it is not a Storefront API fault.
- **Test orders are not in your dashboard.** They belong to the demo store. Use the checkout session's status or the webhook (step 3).
- **The WooCommerce plugin imports products but cannot check out yet.** Freight at the cart and the hand-off at Place order are being built. Install it from the repo's source zip until release zips are attached.
- **The starter reads products on its own server.** The product list and product pages are read there and cached for 60 seconds, so those reads share your server's address (see Rate limits). Cart, freight and checkout calls go from the buyer's browser, so each buyer counts against their own limit.
- **Codespaces addresses must be added one by one.** A key's allowed sites are exact addresses, with no wildcards.

## Help
- API reference: [Storefront API](/developers/storefront-api.md)
- Webhooks: [Webhooks](/developers/webhooks.md)
- What changed: [Changelog](/developers/changelog.md)

Checked against btab-api `origin/main` @ 13e2ef972 on 2026-10-21.
