This page builds the smallest working storefront: read the restaurant and its menu in the browser with the public key, quote a cart, and place the order from a small server with the secret key. You need both keys, see Keys and CORS.
For a complete storefront, start from the
example repository: a Vue
(vue/) and a React (react/) storefront that do the same thing, and the
Node server (server/) that holds the secret key and receives
webhooks.
1. Read the restaurant and the menu (browser)
Every browser call sends the public key in X-Unipage-Key, from an origin
you registered with it.
const API = "https://api.unipage.eu/storefront/v1";
const headers = { "X-Unipage-Key": "upg_sf_pk_..." };
async function api(path, options = {}) {
const res = await fetch(`${API}${path}`, { ...options, headers: { ...headers, ...options.headers } });
const body = await res.json();
if (!res.ok) throw body.error; // { code, message, fields? }
return body;
}
const store = await api("/store");
if (store.orderTypes.length === 0) {
// The restaurant does not take orders right now.
}
const orderType = store.orderTypes[0]; // "TAKEOUT" or "DELIVERY"
const menu = await api(`/menu?orderType=${orderType}`);
// menu.categories[].products[]: { id, name, price, available, soldOut, hasOptions, ... }A product with hasOptions: true has option groups: read them with
GET /products/{id} and send the
chosen options on the cart line.
2. Pick a slot (browser)
Ask the schedule for the products in the cart, so the slots respect their preparation times.
const cart = [{ productId: "p7d2x", quantity: 2 }];
const ids = [...new Set(cart.map((line) => line.productId))].join(",");
const schedule = await api(`/schedule?orderType=${orderType}&products=${ids}`);
const slot = schedule.days
.flatMap((day) => day.slots)
.find((s) => s.available);
// slot: { time: "12:15", at: 1792750500, available: true }3. Quote the cart (browser)
The quote is priced by the same code that prices the order: show its lines
and total, and only offer checkout when canCheckout is true.
const quote = await api("/cart/quote", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderType, lines: cart, slotAt: slot.at }),
});
// quote: { lines, subtotal, charges, deliveryCost, serviceCost, couponDiscount, total, canCheckout, errors, ... }4. Place the order (your server)
The browser sends the cart, slot and customer to your own server, which adds
the secret key and the returnUrl. The browser never sees the secret key and
never chooses how the order is paid.
// server.js: node server.js, with UNIPAGE_SECRET_KEY in the environment.
import http from "node:http";
const API = "https://api.unipage.eu/storefront/v1";
const SHOP = "https://shop.example.com"; // one of the public key's origins
http.createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/api/checkout") {
res.writeHead(404).end();
return;
}
let raw = "";
for await (const chunk of req) raw += chunk;
const { orderType, slotAt, lines, customer } = JSON.parse(raw || "{}");
const upstream = await fetch(`${API}/orders`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.UNIPAGE_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
orderType,
slotAt,
lines,
customer, // { firstName, lastName, email, phone }
paymentType: "online",
returnUrl: `${SHOP}/order/{orderId}`,
}),
});
// 201 { order, next }, or Unipage's own error ({ error: { code, ... } }).
res.writeHead(upstream.status, { "Content-Type": "application/json" });
res.end(await upstream.text());
}).listen(3000);paymentType must be one of the restaurant’s payment.methods from
GET /store (online or offline), or external when you take the payment
yourself. See Payments.
5. Pay and confirm (browser and server)
const res = await fetch("/api/checkout", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ orderType, slotAt: slot.at, lines: cart, customer }),
});
const { order, next, error } = await res.json();
if (error) {
// SLOT_UNAVAILABLE, PRODUCT_UNAVAILABLE, ...: send the shopper back to that step.
} else if (next.type === "payment") {
window.location.href = next.url; // the restaurant's payment page
} else {
// "confirmation": placed, nothing left to pay through Unipage.
}After paying, the shopper comes back on
https://shop.example.com/order/<order id>?status=success (or
?status=failed). That query string is only a hint: have your server read
GET /orders/{id} with the secret key
and show status and isPaid, or wait for the
order.paid webhook.
Next steps
- Checkout flow: every step with its request and response.
- Errors and rate limits: what to do with each error code.
- Customer messages: send the order mails yourself.