# Storefront API

The endpoints the BaxPay storefront itself uses, open to any front end: a custom site, a mobile
app or a kiosk. They need no API key and only expose what the shop already shows the public.

All paths are under `https://api.baxpay.com/v1`. Each shop is addressed by its `slug` (the
`{slug}` in `https://{slug}.baxpay.com`). Prices are always computed on the server from the
merchant's catalog: what the client sends is never trusted.

> **No API key, but rate limited**
> These endpoints are anonymous and limited per IP address: 120 requests a minute for reads, 60 for
> quotes and order pages, 20 for checkouts. A `429` means wait and retry. The shop must be
> published (see [Turning the shop on](https://developers.baxpay.com/online-shop#turning-the-shop-on)).

## A checkout in four calls

`GET /shop/{slug}/catalog` lists products, variants, add-ons and categories.
`POST /shop/{slug}/quote` returns the priced lines, delivery and pickup options and any discount.
`POST /shop/{slug}/checkout` creates the order and returns a `payUrl`.
Redirect to `payUrl`. After paying, the customer lands on the shop's order page; read it with `GET /shop/{slug}/orders/{token}`.

> **Responses and browsers**
> Like every BaxPay endpoint, successful responses are wrapped: the result is in `data` (see
> [conventions](https://developers.baxpay.com/conventions)). The API answers browsers only from BaxPay's own origins, so call it
> from your server, or proxy `/v1` through your own domain, rather than from a custom site's front
> end.

## Resolve a shop

- `GET /shop/resolve` — ?host=shop.example.com

Finds a shop from a host name (`{slug}.baxpay.com` or a verified custom domain). Without `?host=`,
the request's `Host` header is used. Returns the shop's public face:

```bash
curl "https://api.baxpay.com/v1/shop/resolve?host=maison-lumiere.baxpay.com"
```

```json
{
  "success": true,
  "data": {
    "slug": "maison-lumiere",
    "displayName": "…",
    "language": "fr",
    "currency": "EUR",
    "productsEnabled": true,
    "theme": {},
    "settings": {}
  }
}
```

- `slug` (string): Shop address.
- `displayName` (string): The business's trading name.
- `logoUrl, iconUrl` (string | null): Public image URLs.
- `theme` (object): Colors, fonts and homepage sections chosen by the merchant.
- `settings` (object): Delivery and pickup, contact, social links, policies (terms, returns, shipping, privacy, legal notice, cookies), SEO and checkout options.
- `language` (string): The shop's language (`fr`, `en`, `de`…). Use it as the default locale.
- `currency` (string): `EUR`.

## Read the catalog

- `GET /shop/{slug}/catalog`
- `GET /shop/{slug}/products/{productId}` — one product + up to 4 related

Only active products sold `ONLINE` or `BOTH` are listed (up to 500).

```bash
curl https://api.baxpay.com/v1/shop/maison-lumiere/catalog
```

```json
{
  "success": true,
  "data": {
    "slug": "maison-lumiere",
    "displayName": "…",
    "categories": [],
    "products": [{ "id": "…", "name": "…", "currency": "EUR", "priceType": "FIXED" }]
  }
}
```

One product comes with its shop and up to 4 related products:

```bash
curl https://api.baxpay.com/v1/shop/maison-lumiere/products/6f0e2c4a-…
```

```json
{
  "success": true,
  "data": {
    "slug": "maison-lumiere",
    "displayName": "…",
    "product": { "id": "6f0e2c4a-…", "name": "…", "unitAmount": 12.5, "priceType": "FIXED", "booking": null },
    "related": []
  }
}
```

Each product looks like this (shown with variants and add-ons):

```json
{
  "id": "…",
  "name": "Lampe Arc",
  "description": "Brass and linen, 160 cm.",
  "unitAmount": 189,
  "currency": "EUR",
  "priceType": "FIXED",
  "quantityStep": 1,
  "categoryId": "…",
  "supplyType": "GOODS",
  "imageUrl": "https://cdn.baxpay.com/…",
  "trackInventory": true,
  "stockHint": "low",
  "variants": [{ "id": "…", "name": "Black", "sku": "ARC-BLK", "unitAmount": 199, "imageUrl": null }],
  "modifierGroups": [
    { "id": "…", "name": "Bulb", "minSelect": 0, "maxSelect": 1,
      "modifiers": [{ "id": "…", "name": "Warm LED", "priceDelta": 9 }] }
  ],
  "booking": null
}
```

- `unitAmount` (number): Price in **major units** (euros), VAT included. A variant with its own `unitAmount` overrides it.
- `priceType` (FIXED | VARIABLE): `VARIABLE` (a gift card, a donation): the customer sets the price, sent as `unitAmount` in the cart.
- `stockHint` (string): `in_stock`, `low` (5 or fewer), `out` or `unlimited` (stock not tracked), at the shop's stock location.
- `supplyType` (GOODS | SERVICES): Services never need delivery.
- `booking` (object | null): Set on bookable services: the customer must pick a time slot. See [Bookings](https://developers.baxpay.com/online-shop/bookings).

## Price a cart

- `POST /shop/{slug}/quote`

```bash
curl -X POST https://api.baxpay.com/v1/shop/maison-lumiere/quote \
  -H "Content-Type: application/json" \
  -d '{ "items": [{ "productId": "6f0e2c4a-…", "quantity": 2 }], "country": "FR", "discountCode": "WELCOME10" }'
```

```json
{
  "success": true,
  "data": {
    "subtotalMinor": 2500,
    "hasGoods": true,
    "lines": [{ "productId": "6f0e2c4a-…", "quantity": 2, "unitMinor": 1250, "lineMinor": 2500, "supplyType": "GOODS" }],
    "discount": null,
    "discountError": { "reason": "NOT_FOUND" },
    "options": [{ "id": "standard", "kind": "SHIPPING", "priceMinor": 590 }]
  }
}
```

The request body in full:

```json
{
  "items": [
    { "productId": "…", "variantId": "…", "quantity": 1, "modifierIds": ["…"] }
  ],
  "country": "FR",
  "discountCode": "WELCOME10",
  "email": "camille@example.fr"
}
```

- `items` (array, required): 1 to 50 lines: `productId`, optional `variantId`, `quantity`, `modifierIds` (add-ons), `unitAmount` for `VARIABLE` products, and `booking` for bookable services.
- `country` (string): ISO code: limits shipping rates to those serving that country.
- `discountCode` (string): A code to check. A refused code is reported, not an error.
- `email` (string): Lets "once per customer" codes be checked.

```json
{
  "subtotalMinor": 18900,
  "hasGoods": true,
  "lines": [
    { "productId": "…", "name": "Lampe Arc", "quantity": 1, "unitMinor": 18900, "lineMinor": 18900,
      "supplyType": "GOODS", "imageUrl": "https://…", "vatRate": 20 }
  ],
  "discount": { "code": "WELCOME10", "kind": "PERCENT", "discountMinor": 1890, "freeShipping": false },
  "discountError": null,
  "countries": ["BE", "FR"],
  "options": [
    { "id": "standard", "kind": "SHIPPING", "label": "Standard delivery", "priceMinor": 0, "detail": "2-4" },
    { "id": "pickup-1", "kind": "PICKUP", "label": "Workshop", "priceMinor": 0, "detail": "12 rue des Lampes, Paris" }
  ],
  "requirePhone": false,
  "allowNote": true
}
```

Amounts in the quote and order are in **minor units** (cents). `options[].detail` is the delivery
time in days for shipping, or the address for pickup. A cart of services only gets one option of
kind `NONE`. Free-over thresholds use the subtotal after the discount.

`discountError` is `null` or `{ reason, minSubtotalMinor? }`, where `reason` is one of `NOT_FOUND`,
`NOT_STARTED`, `EXPIRED`, `USED_UP`, `ALREADY_USED`, `MIN_SUBTOTAL` or `NOT_ELIGIBLE`.

## Check out

- `POST /shop/{slug}/checkout`

```bash
curl -X POST https://api.baxpay.com/v1/shop/maison-lumiere/checkout \
  -H "Content-Type: application/json" \
  -d '{
    "items": [{ "productId": "6f0e2c4a-…", "quantity": 1 }],
    "customer": { "name": "Camille Martin", "email": "camille@example.fr" },
    "fulfilmentOptionId": "standard",
    "address": { "line1": "12 rue des Lampes", "postalCode": "75011", "city": "Paris", "country": "FR" },
    "locale": "fr"
  }'
```

```json
{
  "success": true,
  "data": { "orderNumber": 1001, "orderToken": "kW3c…", "payUrl": "https://pay.baxpay.com/plk_…", "totalMinor": 1840, "currency": "EUR" }
}
```

All the fields:

```json
{
  "items": [{ "productId": "…", "quantity": 1 }],
  "customer": { "name": "Camille Martin", "email": "camille@example.fr", "phone": "+33 6 12 34 56 78" },
  "fulfilmentOptionId": "standard",
  "address": { "line1": "12 rue des Lampes", "postalCode": "75011", "city": "Paris", "country": "FR" },
  "note": "Please ring twice.",
  "locale": "fr",
  "discountCode": "WELCOME10"
}
```

- `items` (array, required): Same lines as the quote. Stock is checked again.
- `customer` (object, required): `name` (2–100 characters), `email`, and `phone` (required when the quote said `requirePhone`).
- `fulfilmentOptionId` (string): An `options[].id` from the quote. Required when the cart has goods.
- `address` (object): Required for shipping: `line1`, `postalCode`, `city`, `country` (one the rate serves; French postal codes have 5 digits), optional `line2`.
- `note` (string): Up to 500 characters, when the shop allows notes.
- `locale` (string): The customer's language: their order emails use it.
- `discountCode` (string): Checked again; a refused code fails the checkout.

```json
{
  "orderNumber": 1042,
  "orderToken": "kW3c…",
  "payUrl": "https://pay.baxpay.com/plk_…",
  "totalMinor": 17010,
  "currency": "EUR"
}
```

Redirect the customer to `payUrl`. The link stays open for a while and holds the stock; an unpaid
order stays `PENDING_PAYMENT`. Once paid, the customer is sent back to the shop's order page
(`https://{shop}/order/{orderToken}`) and receives the order confirmation by email.

> **Keep the order token private**
> `orderToken` is the customer's key to their order page. Show it only to that customer, never in
> logs or analytics.

## Show the order

- `GET /shop/{slug}/orders/{token}`

```bash
curl https://api.baxpay.com/v1/shop/maison-lumiere/orders/kW3c…
```

```json
{
  "success": true,
  "data": { "number": 1001, "status": "PENDING_PAYMENT", "totalMinor": 1840, "currency": "EUR", "payUrl": "https://pay.baxpay.com/plk_…", "bookings": [] }
}
```

The customer may also drop an unpaid order: `POST /shop/{slug}/orders/{token}/cancel`.

Returns what the customer may see: `number`, `status` (`PENDING_PAYMENT`, `PAID`,
`READY_FOR_PICKUP`, `SHIPPED`, `COMPLETED`, `CANCELLED`), `items`, `subtotalMinor`,
`discountMinor`, `discountCode`, `shippingMinor`, `totalMinor`, `currency`, the delivery choice
and pickup point, the dates of each step, `trackingNumber` / `trackingUrl`, the order's
[bookings](https://developers.baxpay.com/online-shop/bookings), and `payUrl` while it is still unpaid.

## Errors

| Status | When | Body |
|--------|------|------|
| `400` | Empty or invalid cart, not enough stock, invalid customer or address, no delivery option chosen | `{ "message": "Only 2 × Lampe Arc left." }` |
| `400` | Refused discount code at checkout | `{ "message": "…", "reason": "EXPIRED" }` |
| `403` | Online shop not available on the platform | `{ "message": "…" }` |
| `404` | Unknown or unpublished shop, product or order | `{ "message": "…" }` |
| `409` | A booked time was taken meanwhile | `{ "message": "…", "reason": "SLOT_TAKEN" }` |
| `429` | Too many requests | — |

```bash
curl -X POST https://api.baxpay.com/v1/shop/maison-lumiere/quote \
  -H "Content-Type: application/json" \
  -d '{ "items": [] }'
```

`message` is a string, or an array of strings for validation errors (as above). It's in English and
safe to show; use `reason` when you show your own text. See [Errors](https://developers.baxpay.com/errors) for the shape.

## Related

- [Online shop](https://developers.baxpay.com/online-shop) — orders, domains, roles
- [Bookings](https://developers.baxpay.com/online-shop/bookings) — slots and booking pages
- [Payment links](https://developers.baxpay.com/payment-links) — the page behind `payUrl`
