# Ekx Toast Pos

> Toast POS API integration for a physical market sales channel — OAuth client credentials, restaurant GUID scoping, orders and menu endpoints, and webhook verification. Use when syncing in-person sales or menu items from Toast into Supabase, or when reconciling the physical channel against Shopify and pop-up sales.

- Skill: `ekinoxis-evm/ekx-toast-pos` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ekinoxis-evm/ekx-toast-pos`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ekinoxis-evm/ekx-toast-pos/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: Ekinoxis-evm (https://skillmd.com/u/ekinoxis-evm)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/ekinoxis-evm/ekx-toast-pos

---


# 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

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

```ts
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:

1. **Store the Toast GUID on every imported row.** It is the only stable key; names and prices change.
2. **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

1. **Missing `Toast-Restaurant-External-ID`** → 403.
2. **Token cached too long** → 401 mid-batch. Refresh on `expiresIn`.
3. **Timezone.** Local vs UTC is the source of most reconciliation mismatches.
4. **Re-running an import** without upsert duplicates sales.
5. **Sandbox and production have different hosts and different GUIDs.**

