---
title: "Quickstart"
description: "From an empty page to a placed order, in the browser and on your server."
---

> Documentation Index
> Fetch the complete documentation index at: https://developer.unipage.be/llms.txt
> Use this file to discover all available pages before exploring further.

# Quickstart

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](/storefront/keys/).

For a complete storefront, start from the
[example repository](https://github.com/UnipageEU/storefront-example): 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](/storefront/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.

```js
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}`](/storefront/api/products/retrieve/) 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.

```js
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.

```js
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.

```js
// 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](/storefront/payments/).

## 5. Pay and confirm (browser and server)

```js
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}`](/storefront/api/orders/retrieve/) with the secret key
and show `status` and `isPaid`, or wait for the
[`order.paid` webhook](/storefront/webhooks/).

## Next steps

- [Checkout flow](/storefront/checkout-flow/): every step with its request and response.
- [Errors and rate limits](/storefront/errors/): what to do with each error code.
- [Customer messages](/storefront/customer-messages/): send the order mails yourself.

Source: https://developer.unipage.be/storefront/quickstart/index.mdx
