Skip to content

Store prompt kit

Each prompt here asks a coding agent for one part of a shop front: a product grid, a product page, a basket and a checkout, all on the Storefront API. Under each prompt is the code it should lead to in its plainest form. That code runs against the API every time these docs are built, so the calls it makes are known to work. When an agent writes something different, compare its calls with these.

Use them with Claude Code, Lovable or v0.

You need your store’s publishable key (pk_test_…, from Developers › API keys) and at least one product online. The Storefront API is on unless you switched it off under Store › Settings › Developers. A brand new test store has no products yet; this puts a candle online with your secret key. Run it on your own computer, never in a browser.

const API = "https://api.tuppence.ai";
const call = async (path, body) => {
const res = await fetch(`${API}${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TUPPENCE_SECRET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path}: ${res.status} ${await res.text()}`);
return res.json();
};
const candle = await call("/v1/products", {
name: "Sea salt candle",
kind: "retail",
amount: 1200,
channels: ["till", "online"],
});
const photo = await call("/v1/media", {
data: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAIAAAAlC+aJAAAAT0lEQVR42u3PQQkAAAgEsGtiRIsY2gi+hcEKLNXzWgQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQELgtLWqEATB/LVQAAAABJRU5ErkJggg==",
alt_text: "A photo of the candle",
});
await call(`/v1/products/${candle.id}`, { media: [photo.id] });
const online = await call(`/v1/products/${candle.id}/publish_online`, {});
console.log("Online:", online.id, online.name);

Paste this first, once per project. It is what keeps a generated shop honest: the prices, totals and delivery dates a shopper sees are always Tuppence’s own.

This site is a shop front for a Tuppence store. Use the Tuppence Storefront API:
https://api.tuppence.ai/storefront/v1, described at
https://docs.tuppence.ai/docs/openapi.storefront.json and https://docs.tuppence.ai/docs/llms.txt.
- Send the header Tuppence-Publishable-Key with the key from NEXT_PUBLIC_TUPPENCE_PUBLISHABLE_KEY.
It is the only key. Never use or ask for a secret key (sk_ or rk_).
- Show prices, totals, tax and delivery dates exactly as the API answers them. Never compute a
price, a total, a tax or a delivery date in the site's own code. Amounts are in minor units.
- Keep the basket's cart_token in localStorage under tuppence_cart_<publishable key>.
- Take card details only through @tuppence/js (npm install @tuppence/js). Never add a card field.
Make the home page a grid of the store's products from GET /storefront/v1/products: each card
shows the first image (with its alt text), the title and price.amount formatted in
price.currency, and links to /products/<slug>. Follow next_cursor when has_more is true.

What the page should call:

const API = "https://api.tuppence.ai/storefront/v1";
const headers = { "Tuppence-Publishable-Key": process.env.TUPPENCE_PUBLISHABLE_KEY };
const money = ({ amount, currency }) =>
new Intl.NumberFormat("en-GB", { style: "currency", currency }).format(amount / 100);
const res = await fetch(`${API}/products?limit=24`, { headers });
const { data: products, has_more, next_cursor } = await res.json();
for (const product of products) {
console.log(`${product.title}: ${money(product.price)}, /products/${product.slug}`);
}
if (has_more) console.log("More from", next_cursor);
Add /products/[slug]: load GET /storefront/v1/products/<slug> and show its images, title,
description, price and availability. When it has options, show a picker over its variants and
use the chosen variant's own price. Add JSON-LD Product and Offer from the same answer.

What the page should call:

const API = "https://api.tuppence.ai/storefront/v1";
const headers = { "Tuppence-Publishable-Key": process.env.TUPPENCE_PUBLISHABLE_KEY };
const money = ({ amount, currency }) =>
new Intl.NumberFormat("en-GB", { style: "currency", currency }).format(amount / 100);
// In the page, the slug comes from the address: /products/sea-salt-candle.
const { data: [first] } = await (await fetch(`${API}/products?limit=1`, { headers })).json();
const product = await (await fetch(`${API}/products/${first.slug}`, { headers })).json();
console.log(`${product.title}: ${money(product.price)} (${product.availability})`);
for (const variant of product.variants) {
console.log(` variant ${variant.id}: ${money(variant.price)}`);
}
Add an "Add to basket" button on the product page and a /basket page. Start a basket with
POST /storefront/v1/carts and keep its cart_token. Add with POST /carts/<token>/lines
{variant, quantity}, change with PATCH /carts/<token>/lines/<id> {quantity} and remove with
DELETE /carts/<token>/lines/<id>. Every answer is the whole basket: show its lines and totals
as they are. If GET /carts/<token> answers 404, forget the token and start a new basket.

What the page should call:

const API = "https://api.tuppence.ai/storefront/v1";
const headers = {
"Tuppence-Publishable-Key": process.env.TUPPENCE_PUBLISHABLE_KEY,
"Content-Type": "application/json",
};
const money = (amount, currency) =>
new Intl.NumberFormat("en-GB", { style: "currency", currency }).format(amount / 100);
const { data: [product] } = await (await fetch(`${API}/products?limit=1`, { headers })).json();
const { cart_token } = await (
await fetch(`${API}/carts`, { method: "POST", headers, body: "{}" })
).json();
const cart = await (
await fetch(`${API}/carts/${cart_token}/lines`, {
method: "POST",
headers,
body: JSON.stringify({ variant: product.variants[0].id, quantity: 2 }),
})
).json();
for (const line of cart.lines) {
console.log(`In your basket: ${line.quantity} × ${line.name}, ${money(line.amount, cart.currency)}`);
}
console.log(`Total: ${money(cart.totals.total, cart.currency)}`);
Add /checkout for the basket. Ask for email, name and a delivery address. When the postcode is
filled in, POST /carts/<token>/delivery_options {address} and show each option's name, price
and earliest to latest dates. Then POST /storefront/v1/checkouts {cart_token, contact,
shipping_address, delivery} and show snapshot.lines, snapshot.delivery_amount, snapshot.tax and
snapshot.due. Take the card with @tuppence/js: GET /checkouts/<token>/fields gives what its
Payment Element needs, then POST /checkouts/<token>/pay {amount: snapshot.due,
confirmation_token}. Send a new Idempotency-Key header (a UUID) with each checkout and pay
request, and the same one again when retrying it. Poll GET /checkouts/<token>/order until it has order.status_url, then go
there. In test mode only, offer to pay with the test card pm_card_gb instead.
Add /checkout for the basket. Ask for email, name and a delivery address. When the postcode is
filled in, POST /carts/<token>/delivery_options {address} and show each option's name, price
and earliest to latest dates. Then POST /storefront/v1/checkouts {cart_token, contact,
shipping_address, delivery} and show snapshot.lines, snapshot.delivery_amount, snapshot.tax and
snapshot.due. Take the card with @tuppence/js: GET /checkouts/<token>/fields gives what its
Payment Element needs, then POST /checkouts/<token>/pay {amount: snapshot.due,
confirmation_token}. Send a new Idempotency-Key header (a UUID) with each checkout and pay
request, and the same one again when retrying it. Poll GET /checkouts/<token>/order until it has order.status_url, then go
there. In test mode only, offer to pay with the test card pm_card_us instead.
Add /checkout for the basket. Ask for email, name and a delivery address. When the postcode is
filled in, POST /carts/<token>/delivery_options {address} and show each option's name, price
and earliest to latest dates. Then POST /storefront/v1/checkouts {cart_token, contact,
shipping_address, delivery} and show snapshot.lines, snapshot.delivery_amount, snapshot.tax and
snapshot.due. Take the card with @tuppence/js: GET /checkouts/<token>/fields gives what its
Payment Element needs, then POST /checkouts/<token>/pay {amount: snapshot.due,
confirmation_token}. Send a new Idempotency-Key header (a UUID) with each checkout and pay
request, and the same one again when retrying it. Poll GET /checkouts/<token>/order until it has order.status_url, then go
there. In test mode only, offer to pay with the test card pm_card_de instead.

What the page should call (here with the test card, as a store in test mode allows):

const API = "https://api.tuppence.ai/storefront/v1";
const headers = {
"Tuppence-Publishable-Key": process.env.TUPPENCE_PUBLISHABLE_KEY,
"Content-Type": "application/json",
};
// A write that can make an order carries an Idempotency-Key, so a retry never pays twice.
const send = async (method, path, body) => {
const res = await fetch(`${API}${path}`, {
method,
headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path}: ${res.status} ${await res.text()}`);
return res.json();
};
const { data: [product] } = await (await fetch(`${API}/products?limit=1`, { headers })).json();
const { cart_token: token } = await send("POST", "/carts", {});
await send("POST", `/carts/${token}/lines`, { variant: product.variants[0].id, quantity: 1 });
const address = { name: "Sam Shopper", line1: "2 Park Row", city: "Leeds", postcode: "LS1 5HD", country: "GB" };
const quoted = await send("POST", `/carts/${token}/delivery_options`, {
address: { country: "GB", postcode: address.postcode },
});
const delivery = quoted.delivery_options.find((o) => o.selected) ?? quoted.delivery_options[0];
const checkout = await send("POST", "/checkouts", {
cart_token: token,
contact: { email: "sam.shopper@example.test", name: address.name },
shipping_address: address,
delivery: delivery.key,
});
console.log("To pay:", checkout.snapshot.due, checkout.snapshot.currency);
// In the browser this is { amount, confirmation_token } from @tuppence/js's Payment Element.
await send("POST", `/checkouts/${token}/pay`, { amount: checkout.snapshot.due, payment_method: "pm_card_gb" });
for (let tries = 0; tries < 30; tries += 1) {
const { order } = await (await fetch(`${API}/checkouts/${token}/order`, { headers })).json();
if (order?.status_url) {
console.log(`Ordered: #${order.number} ${order.status_url}`);
process.exit(0);
}
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error("No order yet");
const API = "https://api.tuppence.ai/storefront/v1";
const headers = {
"Tuppence-Publishable-Key": process.env.TUPPENCE_PUBLISHABLE_KEY,
"Content-Type": "application/json",
};
// A write that can make an order carries an Idempotency-Key, so a retry never pays twice.
const send = async (method, path, body) => {
const res = await fetch(`${API}${path}`, {
method,
headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path}: ${res.status} ${await res.text()}`);
return res.json();
};
const { data: [product] } = await (await fetch(`${API}/products?limit=1`, { headers })).json();
const { cart_token: token } = await send("POST", "/carts", {});
await send("POST", `/carts/${token}/lines`, { variant: product.variants[0].id, quantity: 1 });
const address = { name: "Sam Shopper", line1: "2 Park Row", city: "Leeds", postcode: "LS1 5HD", country: "GB" };
const quoted = await send("POST", `/carts/${token}/delivery_options`, {
address: { country: "GB", postcode: address.postcode },
});
const delivery = quoted.delivery_options.find((o) => o.selected) ?? quoted.delivery_options[0];
const checkout = await send("POST", "/checkouts", {
cart_token: token,
contact: { email: "sam.shopper@example.test", name: address.name },
shipping_address: address,
delivery: delivery.key,
});
console.log("To pay:", checkout.snapshot.due, checkout.snapshot.currency);
// In the browser this is { amount, confirmation_token } from @tuppence/js's Payment Element.
await send("POST", `/checkouts/${token}/pay`, { amount: checkout.snapshot.due, payment_method: "pm_card_us" });
for (let tries = 0; tries < 30; tries += 1) {
const { order } = await (await fetch(`${API}/checkouts/${token}/order`, { headers })).json();
if (order?.status_url) {
console.log(`Ordered: #${order.number} ${order.status_url}`);
process.exit(0);
}
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error("No order yet");
const API = "https://api.tuppence.ai/storefront/v1";
const headers = {
"Tuppence-Publishable-Key": process.env.TUPPENCE_PUBLISHABLE_KEY,
"Content-Type": "application/json",
};
// A write that can make an order carries an Idempotency-Key, so a retry never pays twice.
const send = async (method, path, body) => {
const res = await fetch(`${API}${path}`, {
method,
headers: { ...headers, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path}: ${res.status} ${await res.text()}`);
return res.json();
};
const { data: [product] } = await (await fetch(`${API}/products?limit=1`, { headers })).json();
const { cart_token: token } = await send("POST", "/carts", {});
await send("POST", `/carts/${token}/lines`, { variant: product.variants[0].id, quantity: 1 });
const address = { name: "Sam Shopper", line1: "2 Park Row", city: "Leeds", postcode: "LS1 5HD", country: "GB" };
const quoted = await send("POST", `/carts/${token}/delivery_options`, {
address: { country: "GB", postcode: address.postcode },
});
const delivery = quoted.delivery_options.find((o) => o.selected) ?? quoted.delivery_options[0];
const checkout = await send("POST", "/checkouts", {
cart_token: token,
contact: { email: "sam.shopper@example.test", name: address.name },
shipping_address: address,
delivery: delivery.key,
});
console.log("To pay:", checkout.snapshot.due, checkout.snapshot.currency);
// In the browser this is { amount, confirmation_token } from @tuppence/js's Payment Element.
await send("POST", `/checkouts/${token}/pay`, { amount: checkout.snapshot.due, payment_method: "pm_card_de" });
for (let tries = 0; tries < 30; tries += 1) {
const { order } = await (await fetch(`${API}/checkouts/${token}/order`, { headers })).json();
if (order?.status_url) {
console.log(`Ordered: #${order.number} ${order.status_url}`);
process.exit(0);
}
await new Promise((r) => setTimeout(r, 1000));
}
throw new Error("No order yet");

Docs assistant

Open as a page
Loading the assistant…