# API reference (/docs/api-reference)
Base URL: `https://api.cashiar.app/v1/storefront`. Every request sends the shop's publishable key: `Authorization: Bearer pk_live_…` ([Authentication](/docs/concepts/authentication)).
## Shop and catalogue [#shop-and-catalogue]
| Endpoint | |
| -------- | - |
| | The shop, its branch and hours, pickup and delivery, and collections. |
| | Products on sale, a page at a time. |
| | One product with its versions, packs, prices and extras. |
## Orders [#orders]
| Endpoint | |
| -------- | - |
| | Place an order for pickup or delivery. |
| | Where an order is. |
## Bookings [#bookings]
| Endpoint | |
| -------- | - |
| | Services customers can book. |
| | Free start times for a service on a day. |
| | Book one or more services. |
The whole API is also an [OpenAPI document](/openapi/storefront.json), to generate types or a client.
# Build with AI (/docs/build-with-ai)
An AI app builder can make your shop's website from one prompt. Cashiar still runs the shop behind it: the products, prices and stock it shows, and the orders, bookings and payments it takes, are the ones in your Cashiar app.
### Open Build with AI [#open-build-with-ai]
In Cashiar, open **Settings › Developers**, make your key if you have none, and find **Build with AI**. The prompt there has your key and your shop's name, and asks for the pages your shop needs: products, cart and checkout when you sell, booking when you take bookings.
### Send it to an AI app builder [#send-it-to-an-ai-app-builder]
Choose **Open in Lovable** or **Open in v0**: the prompt opens there, ready to send. For any other builder (Bolt, Cursor, Replit), choose **Copy prompt** and paste it.
### Keep the key to your website [#keep-the-key-to-your-website]
Once the builder has published your website, copy its address (like `https://your-shop.lovable.app`) and add it under **Allowed sites**. Then no other site can use your key from a browser. Add the builder's preview address too while you are still changing it.
### Place a test order [#place-a-test-order]
Order something on your new website. It shows in Cashiar as an online order, and **Settings › Developers** shows the requests and when the key was last used.
## The prompt [#the-prompt]
What Build with AI sends, with your shop's name and key in place:
```txt title="Prompt"
Build a website for Ama Shop, on the Cashiar Storefront API. Cashiar already runs the shop: products, prices, stock, orders, bookings and payments live there.
Read the whole API first: https://docs.cashiar.app/llms-full.txt
- API: https://api.cashiar.app/v1/storefront
- Send this header on every request: Authorization: Bearer pk_live_your_key
- The key is publishable: it is safe in browser code. Do not add a backend, a database or sign-in.
Pages:
- Home and shop: the products from GET /products (search with q, next pages with cursor/nextCursor), each with its photo, name and price.
- Product page: GET /products/{id}; let the customer pick a version and a quantity.
- Cart: kept in the browser (localStorage); lines are { variantId, quantity }.
- Checkout: name, phone, pickup or delivery (and the address and zone for delivery), as GET /shop allows; then POST /orders with an Idempotency-Key header (a new UUID for each checkout). When payNow is chosen and the answer has a payUrl, send the customer there to pay.
- Order page: the order number and total from the answer, and where it is from GET /orders/{id}.
- Booking: the services from GET /services, free times for a day from GET /services/times, then POST /bookings with an Idempotency-Key header.
Rules:
- Read GET /shop first for the name, logo, currency, pickup, delivery and payment options, and opening hours.
- Money is in minor units: divide by 100 and format it in the shop's currency.
- Show the cart's sum while shopping, but confirm the total the order answers with: it has the taxes and delivery.
- Show the API's error message to the customer when a request fails.
- Make it fast, clean and good on a phone first.
```
## For AI tools [#for-ai-tools]
Two files give an AI tool the whole API at once:
| File | What it holds |
| ---- | ------------- |
| [`/llms.txt`](/llms.txt) | Every page of these docs, by title, with its link. |
| [`/llms-full.txt`](/llms-full.txt) | Every guide in one file, then the OpenAPI document. |
Every page is also there as Markdown: add `.md` to its address, like [`/docs/quickstart.md`](/docs/quickstart.md).
After the first build, ask the builder for what you want: "add a WhatsApp button", "show sold out products last". Keep the rules above: the total the order answers with, and the error messages, keep orders right.
# Changelog (/docs/changelog)
## 2026-11-02: Storefront API v1 [#2026-11-02-storefront-api-v1]
The first version of the public Storefront API:
- Publishable keys in **Settings › Developers**, with allowed sites.
- `GET /shop`, `GET /products`, `GET /products/{id}`.
- `POST /orders` with `Idempotency-Key`, and `GET /orders/{id}`.
- `GET /services`, `GET /services/times` and `POST /bookings`.
# Authentication (/docs/concepts/authentication)
Every request carries the shop's **publishable key** as a bearer token:
```http
GET /v1/storefront/shop HTTP/1.1
Host: api.cashiar.app
Authorization: Bearer pk_live_Fd8sK2qX9mVb1LnP4rT7wY0z
```
## Getting a key [#getting-a-key]
The shop's owner (or anyone allowed to change **Settings › Online store**) opens **Settings › Developers** in Cashiar:
- **Create key** makes the shop's key. A shop has one key at a time.
- **Copy** puts it on the clipboard.
- **Regenerate** makes a new key. **The old key stops working at once**, so update your site or app straight away.
The **Online store** add-on must be on, and the store open, for the API to answer.
## What a key can do [#what-a-key-can-do]
A publishable key is safe in a browser, a mobile app or any public code. It can only do what a customer on the shop's store can do:
| It can | It cannot |
| ---------------------------------------------- | ------------------------------------------------- |
| Read the shop, its products, prices and stock | See costs, suppliers, staff or other customers |
| Read services and their free times | Change products, prices, stock or settings |
| Place orders and bookings | See or change other orders, except one it placed |
| Read an order it placed, by the order's id | Refund, void or take payments |
## Allowed sites [#allowed-sites]
By default any website may use the key. To keep it to your own, list your sites under **Allowed sites** in **Settings › Developers**, one per line:
```txt
https://www.amashop.com
https://amashop.com
```
A browser request from any other site then gets `403 Forbidden`. Mobile apps and servers send no `Origin` header, so the list doesn't limit them; the key alone decides.
Send the key in the `Authorization` header only. A key in a URL ends up in browser history and server logs.
## CORS [#cors]
The Storefront API answers browsers on any site (`Access-Control-Allow-Origin` echoes your origin) and uses no cookies. You don't need a proxy: call it straight from your frontend.
## Errors [#errors]
| Status | When |
| ------ | ---- |
| `401 Unauthorized` | No key, a malformed key, or a key that was regenerated. |
| `403 Forbidden` | The key is used from a site not in its allowed sites. |
| `404 Not Found` | The shop has no Online store add-on, or Cashiar has paused it. |
# Errors (/docs/concepts/errors)
Errors use HTTP status codes and a JSON body. `message` is written for people: you can show it to customers as it is.
```json title="400 Bad Request"
{
"statusCode": 400,
"error": "Bad Request",
"message": "Pick a day in the next 30 days"
}
```
A body that doesn't fit the endpoint's schema says which fields are wrong:
```json title="400 Bad Request"
{
"statusCode": 400,
"message": "Validation failed",
"issues": {
"formErrors": [],
"fieldErrors": {
"customerPhone": ["Enter a valid phone number"],
"lines": ["Add at least one item"]
}
}
}
```
## Status codes [#status-codes]
| Status | Meaning | What to do |
| ------ | ------- | ---------- |
| `200 OK`, `201 Created` | It worked. | |
| `400 Bad Request` | The request is wrong: a missing field, a sold-out item, a time no longer free. | Show `message`, or the field errors next to their fields. |
| `401 Unauthorized` | No key, or not a current one. | Check the key in **Settings › Developers**. |
| `403 Forbidden` | The key is not allowed on this site. | Add the site under **Allowed sites**. |
| `404 Not Found` | No such product, order or service, or the store is closed. | |
| `409 Conflict` | A request with the same `Idempotency-Key` is still running. | Wait a moment and retry. |
| `429 Too Many Requests` | Too many requests in a minute. | Back off and retry: see [Rate limits](/docs/concepts/rate-limits). |
| `5xx` | Something went wrong on Cashiar's side. | Retry with the same `Idempotency-Key`. |
## Common messages [#common-messages]
The shop's branch is outside its opening hours and the shop takes no orders while closed. `GET /shop` tells you ahead: `closedUntil` says when it opens, and `closedOrders` is `refuse`.
The owner turned the online store off (**Settings › Online store**). The catalogue answers `404` until it is back on.
For free times, a service offered at several branches needs `branchId`, and a service with options needs `variantId`.
# Idempotency (/docs/concepts/idempotency)
A dropped connection or a double tap can send an order twice. Send an **`Idempotency-Key`** header with every `POST`, and Cashiar places it once:
```bash
curl https://api.cashiar.app/v1/storefront/orders \
-H "Authorization: Bearer pk_live_your_key" \
-H "Idempotency-Key: 4f9c1b2e-7d3a-4c5f-9e1a-2b3c4d5e6f70" \
-H "Content-Type: application/json" \
-d '{ … }'
```
- **The same key again** within 24 hours answers with the first response, whatever the body says. Nothing is placed twice.
- **While the first request is still running**, a retry gets `409 Conflict`. Wait a moment and retry.
- **A request that failed** (any error) frees its key: retry with the same key.
The key is any string of 8 to 100 letters, digits, `-` or `_`. Use a new one for each order: a UUID made when the customer reaches checkout is ideal.
```js
const idempotencyKey = crypto.randomUUID(); // once per checkout
```
# Money and prices (/docs/concepts/money-and-prices)
## Amounts are minor units [#amounts-are-minor-units]
Every amount is a whole number in the currency's smallest unit: **pesewas** for Ghana cedis. `1200` is GH₵ 12.00, `9000` is GH₵ 90.00. There are never decimals, so nothing is lost to rounding.
The shop's currency is in `GET /shop` as `currency` (`GHS`, `NGN`, `KES`, `ZAR`, `USD`, `EUR` or `GBP`):
```js title="Showing an amount"
const money = (amount, currency) =>
new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(
amount / 100,
);
money(1200, 'GHS'); // "GH₵12.00"
```
## Where prices live [#where-prices-live]
A product has one or more **versions** (sizes, colours: a variant each). Each version has:
- `sellingPrice`: its price for one unit, at the shop's first price type.
- `prices`: its price at each **price type** the shop shows online.
- `sellingUnits`: the packs it also sells in (a carton of 24), each with its own `sellingPrice` and `prices`.
```json
{
"priceTypes": [
{ "id": null, "name": "Retail" },
{ "id": "9badb858-…", "name": "Wholesale" }
],
"versions": [
{
"variantId": "1db29704-…",
"sellingPrice": 1200,
"prices": [
{ "priceListId": null, "sellingPrice": 1200 },
{ "priceListId": "9badb858-…", "sellingPrice": 1000 }
],
"sellingUnits": [
{
"id": "c22d51bf-…",
"name": "Pack of 10",
"sellingPrice": 12000,
"prices": [{ "priceListId": null, "sellingPrice": 12000 }]
}
]
}
]
}
```
## Price types [#price-types]
Shops sell at more than one price: retail and wholesale, adult and kids. `priceTypes` lists those the shop offers online; `id: null` is **Retail**. Let customers pick one and send its id as `priceListId` on each order line. Leave it out for Retail.
## Taxes and delivery [#taxes-and-delivery]
The order's `total` includes the shop's taxes and any delivery fee, worked out by Cashiar. Show the line prices while the customer shops, and the `total` the order answers with at checkout.
Delivery fees are in `GET /shop`: `delivery.fee` is the usual fee, and `delivery.zones` lists places with their own fee. Send the zone's `name` as `deliveryZone` to charge it.
# Pagination (/docs/concepts/pagination)
Lists answer a page at a time with a cursor:
```json
{
"items": [ /* … */ ],
"nextCursor": "dabe0026-3fbe-427e-afc3-7c509f68d462"
}
```
Pass `nextCursor` back as `cursor` for the next page. `null` means there is none.
```js title="Every product, page by page"
async function allProducts(key) {
const items = [];
let cursor = null;
do {
const url = new URL('https://api.cashiar.app/v1/storefront/products');
url.searchParams.set('limit', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
const page = await res.json();
items.push(...page.items);
cursor = page.nextCursor;
} while (cursor);
return items;
}
```
Keep the same filters (`q`, `collection`, `sort`) on every page of one list.
# Rate limits (/docs/concepts/rate-limits)
Limits count per key, per minute:
| Requests | Limit |
| -------- | ----- |
| Reads (`GET`) | 600 a minute |
| Orders and bookings (`POST`) | 20 a minute |
Over a limit, the API answers `429 Too Many Requests` with a `Retry-After` header (seconds). Wait that long and retry.
## Staying under them [#staying-under-them]
- **Cache the catalogue.** Products and the shop change rarely: keep them a few minutes in your app, or render them on your server and cache the page.
- **Ask for what you show.** Page through products with `limit`, and load one product's page by its id.
- **Don't poll fast.** To follow an order, check its status every 30 seconds at most, or when the customer opens the page.
# Versioning (/docs/concepts/versioning)
The Storefront API is versioned in its path: `/v1/storefront`. Within v1:
- **Never removed or renamed:** endpoints, fields, and the meaning of a field.
- **May be added:** new endpoints, new optional request fields, new fields in responses, new values where a field lists values (a new delivery status).
Write your code to **ignore fields it doesn't know** and to **handle unknown values** of a listed field (show a neutral state), and it keeps working as v1 grows.
A change that would break code comes as **v2**, with v1 kept running alongside it. Every change is in the [Changelog](/docs/changelog).
# Flutter (/docs/frameworks/flutter)
```yaml title="pubspec.yaml"
dependencies:
http: ^1.2.0
uuid: ^4.4.0
```
```dart title="lib/cashiar.dart"
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:uuid/uuid.dart';
class CashiarException implements Exception {
CashiarException(this.status, this.message);
final int status;
final String message;
@override
String toString() => message;
}
class Cashiar {
Cashiar(this.key);
final String key;
static const _base = 'https://api.cashiar.app/v1/storefront';
Future> _call(String method, String path,
{Map? body, String? idempotencyKey}) async {
final uri = Uri.parse('$_base$path');
final headers = {
'Authorization': 'Bearer $key',
if (body != null) 'Content-Type': 'application/json',
if (idempotencyKey != null) 'Idempotency-Key': idempotencyKey,
};
final res = method == 'GET'
? await http.get(uri, headers: headers)
: await http.post(uri, headers: headers, body: jsonEncode(body));
final data = jsonDecode(res.body) as Map;
if (res.statusCode >= 400) {
throw CashiarException(res.statusCode, data['message'].toString());
}
return data;
}
Future> shop() => _call('GET', '/shop');
Future> products({String? collection, String? cursor}) =>
_call('GET', '/products?${Uri(queryParameters: {
if (collection != null) 'collection': collection,
if (cursor != null) 'cursor': cursor,
}).query}');
Future> placeOrder(Map order,
{String? idempotencyKey}) =>
_call('POST', '/orders',
body: order, idempotencyKey: idempotencyKey ?? const Uuid().v4());
Future> order(String id) => _call('GET', '/orders/$id');
}
```
```dart title="Placing an order"
final api = Cashiar('pk_live_your_key');
final placed = await api.placeOrder({
'customerName': 'Kojo Mensah',
'customerPhone': '024 555 0199',
'fulfilment': 'pickup',
'lines': [
{'variantId': '1db29704-09ee-4aa3-95cc-c424173e6d5a', 'quantity': 2},
],
});
// Show placed['number']; open placed['payUrl'] in the browser when it is set.
```
Amounts are minor units: format `sellingPrice / 100` with the shop's `currency` (`NumberFormat.simpleCurrency(name: 'GHS')` from `package:intl`).
# JavaScript (/docs/frameworks/javascript)
The API is plain HTTPS and JSON, so `fetch` is all you need. This small client handles the key, errors and idempotency:
```js title="cashiar.js"
const BASE = 'https://api.cashiar.app/v1/storefront';
export function cashiar(key) {
async function call(method, path, body) {
const res = await fetch(`${BASE}${path}`, {
method,
headers: {
Authorization: `Bearer ${key}`,
...(body && {
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
}),
},
body: body && JSON.stringify(body),
});
const data = await res.json();
if (!res.ok) throw Object.assign(new Error(data.message), data);
return data;
}
return {
shop: () => call('GET', '/shop'),
products: (query = {}) =>
call('GET', `/products?${new URLSearchParams(query)}`),
product: (id) => call('GET', `/products/${id}`),
order: (order) => call('POST', '/orders', order),
orderStatus: (id) => call('GET', `/orders/${id}`),
services: () => call('GET', '/services'),
freeTimes: (query) => call('GET', `/services/times?${new URLSearchParams(query)}`),
book: (booking) => call('POST', '/bookings', booking),
};
}
```
```html title="index.html"
```
The client makes a new `Idempotency-Key` for each call. To retry a failed order safely, keep the key from the first try and send it again: see [Idempotency](/docs/concepts/idempotency).
# Next.js (/docs/frameworks/nextjs)
In the App Router, fetch the catalogue in Server Components. Next.js caches it, so your pages stay fast and well under the [rate limits](/docs/concepts/rate-limits).
```bash title=".env.local"
NEXT_PUBLIC_CASHIAR_KEY=pk_live_your_key
```
```tsx title="app/lib/cashiar.ts"
const BASE = 'https://api.cashiar.app/v1/storefront';
export async function cashiar(path: string, revalidate = 60): Promise {
const res = await fetch(`${BASE}${path}`, {
headers: { Authorization: `Bearer ${process.env.NEXT_PUBLIC_CASHIAR_KEY}` },
next: { revalidate },
});
if (!res.ok) throw new Error((await res.json()).message);
return res.json();
}
```
```tsx title="app/page.tsx"
import Link from 'next/link';
import { cashiar } from './lib/cashiar';
export default async function Home() {
const [shop, { items }] = await Promise.all([
cashiar('/shop', 300),
cashiar('/products?limit=24'),
]);
return (
{shop.name}
{items.map((p) => (
{p.name}
))}
);
}
```
```tsx title="app/products/[id]/page.tsx"
import { cashiar } from '../../lib/cashiar';
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
const product = await cashiar(`/products/${(await params).id}`, 30);
return {product.name} ;
}
```
Place orders from a Client Component or a Server Action with `POST /orders`, as in [Cart and checkout](/docs/guides/cart-and-checkout). Use a short `revalidate` for stock, or none on the product page while the customer is choosing.
# React (/docs/frameworks/react)
With [TanStack Query](https://tanstack.com/query) the catalogue is cached, refetched in the background and shared between components.
```bash
npm install @tanstack/react-query
```
```tsx title="src/cashiar.ts"
import { useMutation, useQuery } from '@tanstack/react-query';
const BASE = 'https://api.cashiar.app/v1/storefront';
const KEY = import.meta.env.VITE_CASHIAR_KEY; // pk_live_…
async function call(path: string, init?: RequestInit): Promise {
const res = await fetch(`${BASE}${path}`, {
...init,
headers: { Authorization: `Bearer ${KEY}`, ...init?.headers },
});
const data = await res.json();
if (!res.ok) throw new Error(data.message);
return data;
}
export const useShop = () =>
useQuery({ queryKey: ['shop'], queryFn: () => call('/shop'), staleTime: 5 * 60_000 });
export const useProducts = (collection?: string) =>
useQuery({
queryKey: ['products', collection],
queryFn: () =>
call<{ items: Product[]; nextCursor: string | null }>(
`/products${collection ? `?collection=${collection}` : ''}`,
),
staleTime: 60_000,
});
export const usePlaceOrder = () =>
useMutation({
mutationFn: ({ order, idempotencyKey }: { order: Order; idempotencyKey: string }) =>
call('/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey },
body: JSON.stringify(order),
}),
});
```
```tsx title="src/Checkout.tsx"
import { useState } from 'react';
import { usePlaceOrder } from './cashiar';
export function Checkout({ lines }: { lines: CartLine[] }) {
// One key per checkout: a retry after a dropped connection never orders twice.
const [idempotencyKey] = useState(() => crypto.randomUUID());
const place = usePlaceOrder();
const submit = (form: FormData) =>
place.mutate(
{
idempotencyKey,
order: {
customerName: String(form.get('name')),
customerPhone: String(form.get('phone')),
fulfilment: 'pickup',
payNow: true,
lines,
},
},
{ onSuccess: (placed) => placed.payUrl && window.location.assign(placed.payUrl) },
);
return (
);
}
```
Types for `Shop`, `Product`, `Order` and `PlacedOrder` can be generated from the [OpenAPI document](/openapi/storefront.json) with a tool such as `openapi-typescript`:
```bash
npx openapi-typescript https://docs.cashiar.app/openapi/storefront.json -o src/cashiar-api.d.ts
```
# Bookings (/docs/guides/bookings)
Shops with the **Services** add-on take bookings. `GET /shop` says so with `sellsServices: true`.
### List the services [#list-the-services]
```json title="200 OK (one service)"
{
"firstDay": "2026-11-02",
"lastDay": "2026-12-01",
"payOnline": false,
"items": [
{
"id": "756eb9a8-46e3-423c-8f2b-5979029ec77e",
"name": "Haircut",
"sellingPrice": 5000,
"priceMode": "fixed",
"durationMinutes": 45,
"places": ["shop"],
"askFirst": null,
"options": [
{ "variantId": "ec4a16a4-…", "name": "Short", "imageUrl": null, "sellingPrice": 5000 },
{ "variantId": "39fab80b-…", "name": "Long", "imageUrl": null, "sellingPrice": 6000 }
],
"priceTypes": [
{ "id": "cb5fee59-…", "name": "Adult" },
{ "id": "9badb858-…", "name": "Kids" }
],
"branches": [{ "id": "42bf0908-…", "name": "Osu Store", "workingHours": [] }],
"addonGroups": []
}
]
}
```
Bookings are open from `firstDay` to `lastDay` (the next 30 days). `places` says where the service happens: `shop`, `visit` (at the customer's) or `online`. `askFirst`, when set, is a question the customer must answer.
### Find a free time [#find-a-free-time]
```json title="200 OK"
{
"branchId": "42bf0908-…",
"date": "2026-11-03",
"durationMinutes": 45,
"slots": ["08:00", "08:45", "09:30", "11:00"],
"times": [
{ "time": "08:00", "booked": false },
{ "time": "10:15", "booked": true }
]
}
```
Offer the `slots`. `times` also shows the booked ones, to draw a full day.
### Book [#book]
```bash
curl https://api.cashiar.app/v1/storefront/bookings \
-H "Authorization: Bearer pk_live_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 9d2e1f30-booking-1" \
-d '{
"customerName": "Esi Owusu",
"customerPhone": "+233 24 555 0123",
"branchId": "42bf0908-f715-4915-a58f-0e5dfe083268",
"date": "2026-11-03",
"startTime": "09:30",
"services": [
{
"serviceId": "756eb9a8-46e3-423c-8f2b-5979029ec77e",
"variantId": "ec4a16a4-f42d-434b-827e-882782074158",
"priceListId": "cb5fee59-7ee7-4d59-b567-506948182a42"
}
]
}'
```
```json title="201 Created"
{ "number": "B-0042", "status": "confirmed", "payUrl": null }
```
`status` is `confirmed`, or `new` when the shop confirms bookings itself (or the time was taken meanwhile). With `payNow: true` and services that ask for payment, `payUrl` is where the customer pays.
## Several services at once [#several-services-at-once]
A booking can hold up to 10 services, done one after the other from `startTime`. Ask for free times with the first service and `withServiceIds` for the rest, so the slot is long enough for all of them.
# Cart and checkout (/docs/guides/cart-and-checkout)
## Before checkout [#before-checkout]
Read `GET /shop` to build the checkout:
| Field | What it decides |
| ----- | --------------- |
| `pickup` | Offer **Pickup** at the branch (`branch.name`, `branch.address`). |
| `delivery` | Offer **Delivery** when it isn't `null`: `fee` is the usual fee, `zones` places with their own. |
| `payOnline` | Offer **Pay now** (mobile money, card, bank) through a payment link. |
| `payFirst` | Every order must be paid now: hide pay at pickup or on delivery. |
| `closedUntil`, `closedOrders` | Closed now: `closedUntil` says when it opens ("tomorrow at 8:00 am"). With `closedOrders: "take"` orders are made when it opens; with `refuse` none are taken. |
## Placing the order [#placing-the-order]
```bash
curl https://api.cashiar.app/v1/storefront/orders \
-H "Authorization: Bearer pk_live_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0b8c7e91-checkout-1" \
-d '{
"customerName": "Kojo Mensah",
"customerPhone": "024 555 0199",
"fulfilment": "delivery",
"deliverTo": "House 12, Lagoon Rd, Kasoa",
"deliveryZone": "Kasoa",
"payNow": true,
"note": "Call when you arrive",
"lines": [
{ "variantId": "1db29704-09ee-4aa3-95cc-c424173e6d5a", "quantity": 2 },
{
"variantId": "1db29704-09ee-4aa3-95cc-c424173e6d5a",
"sellingUnitId": "c22d51bf-e219-4175-bc54-7ba351d67e5e",
"quantity": 1
}
]
}'
```
```json title="201 Created"
{
"id": "4490c9f8-31fd-454a-ae02-b74c976d070b",
"number": "S-0067",
"total": 36500,
"payUrl": "https://checkout.paystack.com/…",
"waiting": false,
"trackUrl": "https://shop.cashiar.app/track/K7Q2…"
}
```
| Field | Meaning |
| ----- | ------- |
| `id` | Keep it to [follow the order](/docs/guides/order-status). |
| `number` | The order number the shop sees: show it to the customer. |
| `total` | What the customer pays, with taxes and delivery, in minor units. |
| `payUrl` | With `payNow`: send the customer here to pay. `null` when they pay at pickup or on delivery. |
| `waiting` | The shop accepts orders before making them: say "the shop will confirm your order". |
| `trackUrl` | For a delivery: Cashiar's page where the customer follows it. |
## Paying now [#paying-now]
With `payNow: true` (and the shop's `payOnline`), redirect the customer to `payUrl`. They pay by mobile money, card or bank on Paystack's checkout, and the payment lands on the order in Cashiar. Check it with [order status](/docs/guides/order-status): `paid` grows to `total`.
Show your cart's sum while the customer shops, but charge and confirm the `total` the order answers with: it includes the shop's taxes and the delivery fee.
## What happens in the shop [#what-happens-in-the-shop]
The order appears in Cashiar as an **open sale** from the online store: on the Sales screen and its Orders tab, in the kitchen when the shop has one, and with a rider when it is a delivery. When the shop takes orders as they come, it is made straight away; otherwise staff accept it first. The customer gets the same texts as from the Cashiar store.
# Order status (/docs/guides/order-status)
With the `id` an order answered with, read where it is:
```json title="200 OK"
{
"id": "4490c9f8-31fd-454a-ae02-b74c976d070b",
"number": "S-0067",
"status": "open",
"total": 36500,
"paid": 36500,
"delivery": {
"status": "on_the_way",
"trackUrl": "https://shop.cashiar.app/track/K7Q2…"
},
"createdAt": "2026-11-02T13:37:16.507Z"
}
```
## Status [#status]
| `status` | Meaning |
| -------- | ------- |
| `waiting` | The shop has yet to accept it. |
| `open` | Accepted: being made, packed or on its way. |
| `completed` | Paid and done. |
| `cancelled` | The shop cancelled it. |
`paid` is what is paid so far; when it reaches `total`, nothing is owed.
## Delivery [#delivery]
`delivery` is `null` for pickup. For a delivery:
| `delivery.status` | Meaning |
| ----------------- | ------- |
| `waiting` | Not packed yet. |
| `packed` | Ready for a rider. |
| `on_the_way` | With the rider. |
| `delivered` | Delivered. |
| `failed` | The rider could not deliver it. |
`trackUrl` is Cashiar's tracking page, with the rider's progress: link to it, or show `delivery.status` in your own design.
Only orders placed online (through the API or the shop's Cashiar store) can be read here, and only with the shop's own key.
# Product list (/docs/guides/product-list)
## The shop and its collections [#the-shop-and-its-collections]
Start with `GET /shop`: it has the shop's name, logo and currency, whether it is open, how orders are collected, and its **collections** (the shop's categories with something on sale, each with a photo).
```json title="200 OK (shortened)"
{
"name": "Ama Shop",
"logoUrl": "https://api.cashiar.app/photos/orgs/…/logo.png",
"currency": "GHS",
"enabled": true,
"closedUntil": null,
"pickup": true,
"delivery": { "fee": 1000, "zones": [{ "name": "Kasoa", "fee": 500 }] },
"payOnline": true,
"collections": [
{ "id": "0a9e1abe-…", "name": "Beverages", "imageUrl": "https://…" },
{ "id": "e57e1c05-…", "name": "Diapers", "imageUrl": null }
]
}
```
## Listing products [#listing-products]
```js title="One collection, newest first"
const url = new URL('https://api.cashiar.app/v1/storefront/products');
url.searchParams.set('collection', collectionId);
url.searchParams.set('sort', '-newest');
const { items, nextCursor } = await fetch(url, {
headers: { Authorization: `Bearer ${KEY}` },
}).then((res) => res.json());
```
## What to show on a card [#what-to-show-on-a-card]
| Field | Use it for |
| ----- | ---------- |
| `name`, `imageUrl` | The card's title and photo (`imageUrl` may be `null`). |
| `versions[0].sellingPrice` | The price. When versions differ, show "From" the lowest. |
| `soldOut` | Every version is out of stock: show **Sold out** and turn off Add. |
| `category` | A small label. |
Out-of-stock products show as sold out unless the shop hides them (**Settings › Online store**); then they are not listed at all.
```js title="The price to show"
const fromPrice = (product) =>
Math.min(...product.versions.map((v) => v.sellingPrice));
const hasChoices = (product) =>
product.versions.length > 1 ||
product.versions.some((v) => v.sellingUnits.length) ||
product.addonGroups.length > 0;
```
Send customers to a [product page](/docs/guides/product-page) when `hasChoices` is true; otherwise **Add** can put the first version straight in the cart.
# Product page (/docs/guides/product-page)
A product holds everything its page needs.
## Versions [#versions]
`versions` are the product's sizes or colours, one per variant. `name` is `null` when the product has only one. Each has its own `variantId`, `sellingPrice`, photo (`imageUrl`, or `null` for the product's) and stock:
| `inStock` | Meaning |
| --------- | ------- |
| `true` | In stock at the branch that fills online orders. |
| `false` | Out of stock: don't let it be added. |
| `null` | The shop doesn't count its stock: always available. |
## Pack sizes [#pack-sizes]
`sellingUnits` are the packs a version is also sold in (a carton of 24, a pack of 10), each with its own `id` and price. To sell a pack, send its `id` as `sellingUnitId` on the order line; `quantity` then counts packs.
## Price types [#price-types]
`priceTypes` are the prices the shop offers online (Retail, and others such as Wholesale). Each version and pack lists its price at each in `prices`. Show a picker when there is more than one, and send the picked `id` as `priceListId` (leave it out for Retail, whose `id` is `null`).
```js title="A line's price"
function linePrice(version, { sellingUnitId, priceListId = null }) {
const unit = sellingUnitId
? version.sellingUnits.find((u) => u.id === sellingUnitId)
: version;
const price = unit.prices.find((p) => p.priceListId === priceListId);
return price ? price.sellingPrice : unit.sellingPrice;
}
```
## Extras [#extras]
`addonGroups` are choices that change the order, such as toppings or a side:
| Field | Meaning |
| ----- | ------- |
| `mode` | `one`: pick one, `many`: pick any number. |
| `required` | The customer must pick at least one. |
| `options[].sellingPrice` | What the option adds to each item. |
| `options[].preselected` | Picked unless the customer changes it. |
Send the picked option ids as `addons` on the line. Left out, the preselected ones are used.
## Putting it in the cart [#putting-it-in-the-cart]
A cart line is exactly what an order line takes:
```ts
type CartLine = {
variantId: string;
quantity: number;
sellingUnitId?: string;
priceListId?: string;
addons?: string[];
note?: string; // "No onions", up to 200 characters
};
```
Keep the cart in your app (local storage, a store, your own database): the API has no cart of its own. Totals are worked out when you [place the order](/docs/guides/cart-and-checkout).
# Introduction (/docs)
Cashiar runs a shop's selling: its products and prices, stock at every branch, orders, deliveries, bookings and payments. Every shop with the **Online store** add-on gets a store at `.cashiar.shop`.
The **Storefront API** lets you build that store yourself. Your website, your mobile app or your kiosk reads the shop's catalogue and services straight from Cashiar and places orders and bookings that land in the shop like any other: on the Sales screen, in the kitchen, on the rider's list and in the reports.
} title="Quickstart" href="/docs/quickstart">
Your first products on screen in five minutes.
} title="Cart and checkout" href="/docs/guides/cart-and-checkout">
Turn a cart into an order, for pickup or delivery, paid now or later.
} title="Bookings" href="/docs/guides/bookings">
Let customers book a salon chair, a fitting or a table.
} title="API reference" href="/docs/api-reference">
Every endpoint, field and error, with a playground.
## What you can build [#what-you-can-build]
- **A store on your own domain**, designed exactly as you like, in React, Next.js, Vue, Svelte or plain HTML.
- **A mobile app** in Flutter, React Native or Swift that customers order from.
- **A kiosk or a QR menu** that sends orders straight to the shop.
- **Bookings** inside your own site, with live free times.
## How it works [#how-it-works]
### The shop turns on the API [#the-shop-turns-on-the-api]
In Cashiar, the owner opens **Settings › Developers** and creates a **publishable key** (`pk_live_…`). It belongs to that shop only.
### Your code sends the key [#your-code-sends-the-key]
Every request carries it: `Authorization: Bearer pk_live_…`. The key is safe in a browser or an app. It can only read what the shop's online store shows and place orders and bookings.
### Cashiar does the rest [#cashiar-does-the-rest]
Prices, taxes, stock, opening hours, delivery fees, payment links and the shop's own rules are applied for you, exactly as on the Cashiar store.
The Storefront API answers from the same code as the shop's Cashiar store. A product the store hides, a price list it doesn't offer, or an order it would refuse while closed is the same through the API.
## Base URL [#base-url]
```txt
https://api.cashiar.app/v1/storefront
```
All requests use HTTPS and JSON. Amounts are whole numbers in the currency's smallest unit (pesewas for cedis): see [Money and prices](/docs/concepts/money-and-prices).
# Quickstart (/docs/quickstart)
### Get a publishable key [#get-a-publishable-key]
In Cashiar, open **Settings › Developers** and choose **Create key**. You need the **Online store** add-on, and the store must be open (**Settings › Online store**).
Your key looks like `pk_live_Fd8sK2…`. It is meant for browsers and apps: you can commit it to your frontend.
### Make your first request [#make-your-first-request]
List the shop's products:
```bash
curl https://api.cashiar.app/v1/storefront/products?limit=3 \
-H "Authorization: Bearer pk_live_your_key"
```
```js
const res = await fetch(
'https://api.cashiar.app/v1/storefront/products?limit=3',
{ headers: { Authorization: 'Bearer pk_live_your_key' } },
);
const { items, nextCursor } = await res.json();
```
```dart
import 'dart:convert';
import 'package:http/http.dart' as http;
final res = await http.get(
Uri.parse('https://api.cashiar.app/v1/storefront/products?limit=3'),
headers: {'Authorization': 'Bearer pk_live_your_key'},
);
final page = jsonDecode(res.body);
```
You get a page of products, each with its versions, prices and stock:
```json title="200 OK"
{
"items": [
{
"id": "dabe0026-3fbe-427e-afc3-7c509f68d462",
"name": "Amoxiclav",
"unitName": "Sachet",
"category": "Tablets",
"imageUrl": "https://api.cashiar.app/photos/orgs/…/products/….png",
"priceTypes": [{ "id": null, "name": "Retail" }],
"versions": [
{
"variantId": "1db29704-09ee-4aa3-95cc-c424173e6d5a",
"name": null,
"sellingPrice": 1200,
"inStock": true,
"sellingUnits": [
{ "id": "c22d51bf-…", "name": "Pack of 10", "sellingPrice": 12000, "prices": [] }
],
"prices": [{ "priceListId": null, "sellingPrice": 1200 }]
}
],
"addonGroups": [],
"soldOut": false
}
],
"nextCursor": "dabe0026-3fbe-427e-afc3-7c509f68d462"
}
```
`sellingPrice: 1200` is **GH₵ 12.00**: amounts are in pesewas.
### Show them [#show-them]
A minimal product list in React:
```tsx title="Products.tsx"
import { useEffect, useState } from 'react';
const KEY = 'pk_live_your_key';
const money = (pesewas: number) =>
new Intl.NumberFormat('en-GH', { style: 'currency', currency: 'GHS' }).format(
pesewas / 100,
);
export function Products() {
const [products, setProducts] = useState([]);
useEffect(() => {
fetch('https://api.cashiar.app/v1/storefront/products', {
headers: { Authorization: `Bearer ${KEY}` },
})
.then((res) => res.json())
.then((page) => setProducts(page.items));
}, []);
return (
{products.map((p) => (
{p.name}: {money(p.versions[0].sellingPrice)}
{p.soldOut && ' (sold out)'}
))}
);
}
```
### Place an order [#place-an-order]
```bash
curl https://api.cashiar.app/v1/storefront/orders \
-H "Authorization: Bearer pk_live_your_key" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a9e-order-1" \
-d '{
"customerName": "Kojo Mensah",
"customerPhone": "024 555 0199",
"fulfilment": "pickup",
"lines": [{ "variantId": "1db29704-09ee-4aa3-95cc-c424173e6d5a", "quantity": 2 }]
}'
```
The order appears in the shop straight away. Read on in [Cart and checkout](/docs/guides/cart-and-checkout).
## Next steps [#next-steps]
Keys, allowed sites and what a key can do.
Versions, pack sizes, price lists and extras.
A typed client and hooks for React.
Build a shop app in Dart.
# API reference (OpenAPI)
```json
{"openapi":"3.0.0","paths":{"/v1/storefront/shop":{"get":{"description":"The shop: name, logo, currency and clock, the branch that fills online orders with its hours, whether it is open now, pickup and delivery with their fees, online payment, and the collections.","operationId":"getShop","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"timeZone":{"type":"string"},"dateFormat":{"type":"string","enum":["d MMM yyyy","dd/MM/yyyy","MM/dd/yyyy","yyyy-MM-dd"]},"timeFormat":{"type":"string","enum":["12h","24h"]},"slug":{"type":"string"},"domain":{"nullable":true,"type":"string"},"whatsapp":{"nullable":true,"type":"string"},"name":{"type":"string"},"businessType":{"nullable":true,"type":"string"},"logoUrl":{"nullable":true,"type":"string"},"bannerUrl":{"nullable":true,"type":"string"},"currency":{"type":"string"},"phone":{"nullable":true,"type":"string"},"branch":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"address":{"nullable":true,"type":"string"},"phone":{"nullable":true,"type":"string"},"workingHours":{"minItems":7,"maxItems":7,"type":"array","items":{"type":"object","properties":{"open":{"type":"boolean"},"from":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$"},"to":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$"}},"required":["open","from","to"],"additionalProperties":false}},"closures":{"maxItems":100,"type":"array","items":{"type":"object","properties":{"from":{"type":"string","format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"},"to":{"type":"string","format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"},"note":{"nullable":true,"type":"string","maxLength":120}},"required":["from","to","note"],"additionalProperties":false}}},"required":["id","name","address","phone","workingHours","closures"],"additionalProperties":false},"enabled":{"type":"boolean"},"closedUntil":{"nullable":true,"type":"string"},"closedOrders":{"type":"string","enum":["take","refuse"]},"outOfStock":{"type":"string","enum":["sold_out","hide"]},"sellsServices":{"type":"boolean"},"pickup":{"type":"boolean"},"delivery":{"nullable":true,"type":"object","properties":{"fee":{"type":"number"},"zones":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"fee":{"type":"number"}},"required":["name","fee"],"additionalProperties":false}}},"required":["fee","zones"],"additionalProperties":false},"payOnline":{"type":"boolean"},"payFirst":{"type":"boolean"},"collections":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"imageUrl":{"nullable":true,"type":"string"}},"required":["id","name","imageUrl"],"additionalProperties":false}}},"required":["timeZone","dateFormat","timeFormat","slug","domain","whatsapp","name","businessType","logoUrl","bannerUrl","currency","phone","branch","enabled","closedUntil","closedOrders","outOfStock","sellsServices","pickup","delivery","payOnline","payFirst","collections"],"additionalProperties":false}}}}},"security":[{"publishableKey":[]}],"summary":"Get the shop","tags":["Storefront"]}},"/v1/storefront/products":{"get":{"description":"Products on sale online, a page at a time: search by name, filter by collection, sort by name or newest. Pass nextCursor back as cursor for the next page.","operationId":"listProducts","parameters":[{"name":"limit","required":false,"in":"query","schema":{"default":50,"type":"integer","minimum":1,"maximum":200}},{"name":"cursor","required":false,"in":"query","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"q","required":false,"in":"query","schema":{"type":"string","maxLength":100}},{"name":"collection","required":false,"in":"query","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"sort","required":false,"in":"query","schema":{"type":"string","enum":["name","-name","newest","-newest"]}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"nullable":true,"type":"string"},"unitName":{"type":"string"},"category":{"nullable":true,"type":"string"},"imageUrl":{"nullable":true,"type":"string"},"priceTypes":{"type":"array","items":{"type":"object","properties":{"id":{"nullable":true,"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}},"versions":{"type":"array","items":{"type":"object","properties":{"variantId":{"type":"string"},"name":{"nullable":true,"type":"string"},"imageUrl":{"nullable":true,"type":"string"},"sellingPrice":{"type":"number"},"prices":{"type":"array","items":{"type":"object","properties":{"priceListId":{"nullable":true,"type":"string"},"sellingPrice":{"type":"number"}},"required":["priceListId","sellingPrice"],"additionalProperties":false}},"inStock":{"nullable":true,"type":"boolean"},"sellingUnits":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sellingPrice":{"type":"number"},"prices":{"type":"array","items":{"type":"object","properties":{"priceListId":{"nullable":true,"type":"string"},"sellingPrice":{"type":"number"}},"required":["priceListId","sellingPrice"],"additionalProperties":false}}},"required":["id","name","sellingPrice","prices"],"additionalProperties":false}}},"required":["variantId","name","imageUrl","sellingPrice","prices","inStock","sellingUnits"],"additionalProperties":false}},"addonGroups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mode":{"type":"string","enum":["one","many"]},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sellingPrice":{"type":"number"},"preselected":{"type":"boolean"}},"required":["id","name","sellingPrice","preselected"],"additionalProperties":false}}},"required":["id","name","mode","required","options"],"additionalProperties":false}},"soldOut":{"type":"boolean"}},"required":["id","name","description","unitName","category","imageUrl","priceTypes","versions","addonGroups","soldOut"],"additionalProperties":false}},"nextCursor":{"nullable":true,"type":"string"}},"required":["items","nextCursor"],"additionalProperties":false}}}}},"security":[{"publishableKey":[]}],"summary":"List products","tags":["Storefront"]}},"/v1/storefront/products/{id}":{"get":{"description":"One product with its versions, selling units, prices, extras and stock.","operationId":"getProduct","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"nullable":true,"type":"string"},"unitName":{"type":"string"},"category":{"nullable":true,"type":"string"},"imageUrl":{"nullable":true,"type":"string"},"priceTypes":{"type":"array","items":{"type":"object","properties":{"id":{"nullable":true,"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}},"versions":{"type":"array","items":{"type":"object","properties":{"variantId":{"type":"string"},"name":{"nullable":true,"type":"string"},"imageUrl":{"nullable":true,"type":"string"},"sellingPrice":{"type":"number"},"prices":{"type":"array","items":{"type":"object","properties":{"priceListId":{"nullable":true,"type":"string"},"sellingPrice":{"type":"number"}},"required":["priceListId","sellingPrice"],"additionalProperties":false}},"inStock":{"nullable":true,"type":"boolean"},"sellingUnits":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sellingPrice":{"type":"number"},"prices":{"type":"array","items":{"type":"object","properties":{"priceListId":{"nullable":true,"type":"string"},"sellingPrice":{"type":"number"}},"required":["priceListId","sellingPrice"],"additionalProperties":false}}},"required":["id","name","sellingPrice","prices"],"additionalProperties":false}}},"required":["variantId","name","imageUrl","sellingPrice","prices","inStock","sellingUnits"],"additionalProperties":false}},"addonGroups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mode":{"type":"string","enum":["one","many"]},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sellingPrice":{"type":"number"},"preselected":{"type":"boolean"}},"required":["id","name","sellingPrice","preselected"],"additionalProperties":false}}},"required":["id","name","mode","required","options"],"additionalProperties":false}},"soldOut":{"type":"boolean"}},"required":["id","name","description","unitName","category","imageUrl","priceTypes","versions","addonGroups","soldOut"],"additionalProperties":false}}}}},"security":[{"publishableKey":[]}],"summary":"Get a product","tags":["Storefront"]}},"/v1/storefront/orders":{"post":{"description":"Places an order as the online store’s checkout does: for pickup or delivery, paid now by link (payUrl) when the shop takes online payment, else at pickup or on delivery.","operationId":"createOrder","parameters":[{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Any unique string (8–100 letters, digits, - or _). A retry with the same key gets the first answer instead of placing it twice.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"customerName":{"type":"string","minLength":1,"maxLength":120},"customerPhone":{"type":"string"},"deliverTo":{"nullable":true,"type":"string","maxLength":300},"deliveryZone":{"type":"string","maxLength":60},"lines":{"minItems":1,"maxItems":50,"type":"array","items":{"type":"object","properties":{"variantId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"sellingUnitId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"priceListId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"addons":{"maxItems":30,"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"quantity":{"type":"number","minimum":0,"maximum":1000000000},"note":{"nullable":true,"type":"string","maxLength":200}},"required":["variantId","quantity"]}},"note":{"nullable":true,"type":"string","maxLength":500},"fulfilment":{"type":"string","enum":["pickup","delivery"]},"payNow":{"default":false,"type":"boolean"}},"required":["customerName","customerPhone","lines","fulfilment"]}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"total":{"type":"number"},"payUrl":{"nullable":true,"type":"string"},"waiting":{"type":"boolean"},"trackUrl":{"nullable":true,"type":"string"}},"required":["id","number","total","payUrl","waiting","trackUrl"],"additionalProperties":false}}}}},"security":[{"publishableKey":[]}],"summary":"Place an order","tags":["Storefront"]}},"/v1/storefront/orders/{id}":{"get":{"description":"Where an order placed online is: waiting for the shop, open, completed or cancelled, what is paid, and its delivery.","operationId":"getOrder","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"number":{"type":"string"},"status":{"type":"string","enum":["waiting","open","completed","cancelled"]},"total":{"type":"number"},"paid":{"type":"number"},"delivery":{"nullable":true,"type":"object","properties":{"status":{"type":"string","enum":["waiting","packed","on_the_way","delivered","failed"]},"trackUrl":{"nullable":true,"type":"string"}},"required":["status","trackUrl"],"additionalProperties":false},"createdAt":{"type":"string","format":"date-time"}},"required":["id","number","status","total","paid","delivery","createdAt"],"additionalProperties":false}}}}},"security":[{"publishableKey":[]}],"summary":"Get an order","tags":["Storefront"]}},"/v1/storefront/services":{"get":{"description":"The services customers can book, with their prices, how long they take and the branches that offer them, and the days bookings are open.","operationId":"listServices","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"nullable":true,"type":"string"},"sellingPrice":{"nullable":true,"type":"number"},"priceMode":{"nullable":true,"type":"string","enum":["fixed","from","quote"]},"durationMinutes":{"type":"number"},"places":{"type":"array","items":{"type":"string","enum":["shop","visit","online"]}},"askFirst":{"nullable":true,"type":"string"},"imageUrl":{"nullable":true,"type":"string"},"options":{"type":"array","items":{"type":"object","properties":{"variantId":{"type":"string"},"name":{"type":"string"},"imageUrl":{"nullable":true,"type":"string"},"sellingPrice":{"nullable":true,"type":"number"}},"required":["variantId","name","imageUrl","sellingPrice"],"additionalProperties":false}},"priceTypes":{"type":"array","items":{"type":"object","properties":{"id":{"nullable":true,"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}},"prices":{"type":"array","items":{"type":"object","properties":{"variantId":{"nullable":true,"type":"string"},"priceListId":{"nullable":true,"type":"string"},"sellingPrice":{"type":"number"}},"required":["variantId","priceListId","sellingPrice"],"additionalProperties":false}},"addonGroups":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"mode":{"type":"string","enum":["one","many"]},"required":{"type":"boolean"},"options":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"sellingPrice":{"type":"number"},"preselected":{"type":"boolean"}},"required":["id","name","sellingPrice","preselected"],"additionalProperties":false}}},"required":["id","name","mode","required","options"],"additionalProperties":false}},"branches":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"workingHours":{"minItems":7,"maxItems":7,"type":"array","items":{"type":"object","properties":{"open":{"type":"boolean"},"from":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$"},"to":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$"}},"required":["open","from","to"],"additionalProperties":false}}},"required":["id","name","workingHours"],"additionalProperties":false}}},"required":["id","name","description","sellingPrice","priceMode","durationMinutes","places","askFirst","imageUrl","options","priceTypes","prices","addonGroups","branches"],"additionalProperties":false}},"firstDay":{"type":"string"},"lastDay":{"type":"string"},"payOnline":{"type":"boolean"}},"required":["items","firstDay","lastDay","payOnline"],"additionalProperties":false}}}}},"security":[{"publishableKey":[]}],"summary":"List services","tags":["Storefront"]}},"/v1/storefront/services/times":{"get":{"description":"The start times free for a service on a day, at a branch that offers it.","operationId":"getFreeTimes","parameters":[{"name":"branchId","required":false,"in":"query","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"serviceId","required":true,"in":"query","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"variantId","required":false,"in":"query","schema":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},{"name":"withServiceIds","required":false,"in":"query","schema":{"anyOf":[{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},{"maxItems":50,"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}}]}},{"name":"date","required":true,"in":"query","schema":{"type":"string","format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"date":{"type":"string"},"durationMinutes":{"type":"number"},"slots":{"type":"array","items":{"type":"string"}},"times":{"type":"array","items":{"type":"object","properties":{"time":{"type":"string"},"booked":{"type":"boolean"}},"required":["time","booked"],"additionalProperties":false}},"branchId":{"type":"string"}},"required":["date","durationMinutes","slots","times","branchId"],"additionalProperties":false}}}}},"security":[{"publishableKey":[]}],"summary":"Get free times","tags":["Storefront"]}},"/v1/storefront/bookings":{"post":{"description":"Books one or more services at a free time, confirmed straight away unless the shop confirms bookings itself; payUrl when the services ask for payment when booking.","operationId":"createBooking","parameters":[{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","description":"Any unique string (8–100 letters, digits, - or _). A retry with the same key gets the first answer instead of placing it twice.","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"branchId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"customerName":{"type":"string","minLength":1,"maxLength":120},"customerPhone":{"type":"string"},"date":{"type":"string","format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"},"startTime":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$"},"place":{"type":"string","enum":["shop","visit","online"]},"note":{"nullable":true,"type":"string","maxLength":500},"services":{"minItems":1,"maxItems":10,"type":"array","items":{"type":"object","properties":{"serviceId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"variantId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"priceListId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"},"addons":{"maxItems":30,"type":"array","items":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"answer":{"nullable":true,"type":"string","maxLength":300}},"required":["serviceId"]}},"payNow":{"type":"boolean"}},"required":["customerName","customerPhone","services"]}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string"},"status":{"type":"string","enum":["new","confirmed"]},"payUrl":{"nullable":true,"type":"string"}},"required":["number","status","payUrl"],"additionalProperties":false}}}}},"security":[{"publishableKey":[]}],"summary":"Book services","tags":["Storefront"]}}},"info":{"title":"Cashiar Storefront API","description":"Read a shop’s catalogue and services, and place orders and bookings, from your own website or app.","version":"1.0.0","contact":{}},"tags":[{"name":"Storefront","description":"A shop’s catalogue, orders and bookings."}],"servers":[{"url":"https://api.cashiar.app","description":"Production"}],"components":{"securitySchemes":{"publishableKey":{"scheme":"bearer","bearerFormat":"pk_live_…","type":"http","description":"The shop’s publishable key, from Settings, Developers in Cashiar. Safe to use in a browser."}},"schemas":{}}}
```