Toast POS
Our physical market channel. One of three channels reconciled into a single Supabase source of truth (the others: Shopify online, pop-up sales).
Docs: https://doc.toasttab.com/doc/devguide/
Environment
TOAST_API_HOST= # https://ws-api.toasttab.com (prod) / sandbox host
TOAST_CLIENT_ID=
TOAST_CLIENT_SECRET= # SECRET
TOAST_RESTAURANT_GUID= # scopes every request
TOAST_WEBHOOK_SECRET= # SECRET
Auth
Client-credentials OAuth. The token is short-lived — cache it and refresh on expiry rather than fetching one per request.
async function toastToken() {
const res = await fetch(`${process.env.TOAST_API_HOST}/authentication/v1/authentication/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
clientId: process.env.TOAST_CLIENT_ID,
clientSecret: process.env.TOAST_CLIENT_SECRET,
userAccessType: "TOAST_MACHINE_CLIENT",
}),
});
const { token } = await res.json();
return token.accessToken; // cache until token.expiresIn
}
Every subsequent call needs both headers:
Authorization: Bearer <token>
Toast-Restaurant-External-ID: <TOAST_RESTAURANT_GUID>
Omitting the restaurant GUID returns 403, not an empty result — which reads as an auth problem when it is a scoping problem.
Reading orders
GET /orders/v2/ordersBulk?startDate=…&endDate=…
Dates are ISO-8601 with timezone. Toast reports in the restaurant's local timezone, Supabase stores UTC — convert explicitly at the boundary or the daily totals drift by a day's worth of late-evening sales.
Menu: GET /menus/v2/menus for the published menu; /config/v2/… for the raw
configuration entities behind it.
Reconciliation
The whole premise is that Toast, Shopify and pop-up sales disagree, and Supabase arbitrates. Two rules that make that tractable:
- Store the Toast GUID on every imported row. It is the only stable key; names and prices change.
- Import is idempotent. Upsert on the GUID. Toast's bulk endpoint overlaps ranges on re-run, and a plain insert duplicates a day of sales.
Gotchas
- Missing
Toast-Restaurant-External-ID→ 403. - Token cached too long → 401 mid-batch. Refresh on
expiresIn. - Timezone. Local vs UTC is the source of most reconciliation mismatches.
- Re-running an import without upsert duplicates sales.
- Sandbox and production have different hosts and different GUIDs.