Developer guide

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.

Written with the code it describes, in the same pull request. Plain Markdown: /developers/storefront-tutorial.md

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.

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.

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.

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:

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

Start it:

npm run dev

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

ProductWhat it shows
Demo Muga plain product
Demo Cushionthree colours: Red, Blue, Green
Demo LampSold out: the button is off
Demo Boardroom TableEnquire: sold by quote, no Add to cart
Demo Oversized Cratea 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):

StepCall
Product listGET /products
Product pageGET /products/{id}
Add to cartPOST /carts, then PATCH /carts/{id}
FreightPOST /carts/{id}/shipping-estimate with postal_code
Quote request for the tablePOST /carts/{id}/enquiry
Check outPOST /checkout_sessions with cart_id and return_url
Thank-you pageGET /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.

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 quoteworks
Freight priced by btab at the cartbeing built
Place order hands the cart to the store's checkoutbeing built
Paid orders copied back into WooCommercebeing 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.

WhatLimit
Reads120 a minute per buyer address per key; 3000 a minute per key
Cart changes20 a minute per buyer address per key
Enquiries8 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).

CodeMeaningWhat to do
ORIGIN_NOT_ALLOWEDthe page's site is not on the keyadd it under Allowed sites
RETURN_URL_NOT_ALLOWEDreturn_url is not on the key's sitesuse a page on an allowed site
CART_NOT_FOUNDthe cart expired (72 hours after its last change) or the id is wrongstart a new cart
CART_NOT_BUYABLEa line is sold out, enquiry only, or needs an optionshow unavailable_reason per line
TOO_MANY_BAD_KEYStoo many calls with a wrong key from one addressfix 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

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