Toast (Partner Integration)
Status: connector shipping in SP23. This guide is written from the implementation plan’s
pinned contract (docs/superpowers/specs/2026-07-20-sp23-toast-connector-design.md); its wording
is checked against the as-shipped code at SP23’s final review, so treat every claim here as the
committed design, not a promise about what has merged in any given moment.
Toast is architecturally different from every other connector on this platform. Square, Clover, Lightspeed’s order webhooks, LithosPOS, and the generic connector all push order data at this platform and let it derive loyalty effects. Toast’s order payloads carry no loyalty identifier at all — the only place a Toast register ever mentions a guest’s loyalty identity is a single, partner-hosted Loyalty Integration API endpoint that Toast calls, synchronously, at specific points in a check’s lifecycle. So Toast inverts, the same way Lightspeed does, but richer: one endpoint, seven transaction types, a hard 5-second response budget.
The three surfaces
| Surface | Direction | Role |
|---|---|---|
| The loyalty endpoint | Toast calls us, synchronously, inside its 5s budget | member search, balance/offers, signup, redemption commit, accrual, reversal — the only member-linked path |
| Connection lifecycle | dashboard → us; us → Toast (auth, location listing) | machine-client credentials, restaurantGuid/managementGroupGuid mapping, the partners webhook, token refresh |
| Orders reconciliation (poll) | us → Toast, on a schedule | a gap-fill safety net over Toast’s own Orders API — catches accruals whose loyalty call never arrived, ingested anonymously (see below) |
The register flow
The core lifecycle a Toast check walks through, in order:
- Search — the cashier looks a guest up (phone, email, or a scanned loyalty identifier).
Toast sends
LOYALTY_SEARCH; the response is zero or more matched accounts, never a guess. - Inquire — once a guest is attached to the check, Toast asks for their balance and any
offers they can currently redeem (
LOYALTY_INQUIRE). - Redeem — the guest applies a reward at checkout (
LOYALTY_REDEEM); this commits against the ledger in the same request — there is no “maybe it applied” state. - Accrue — the check closes and payment completes (
LOYALTY_ACCRUE); points are earned through the same pipeline every other connector’s transactions flow through.
Two more types exist outside that everyday flow:
- Signup (
LOYALTY_SIGNUP) — kiosk/guest-display enrollment: a new guest joins the program right at the register. - Reverse (
LOYALTY_REVERSE) — undoes a prior accrual or redemption (a void, a corrected check).
Toast-Transaction-Type |
Fires when | What this platform does |
|---|---|---|
LOYALTY_SEARCH |
Cashier looks up a guest | Member-matching chain over the search criteria Toast sends (phone, email, or a loyalty identifier tried as loyalty number then QR) → accounts[], possibly empty. Never a partial guess. |
LOYALTY_INQUIRE |
A guest is attached to the check | Current balance + offers[] — this guest’s currently-affordable rewards, autoApply always false in v1 (the register applies it, not us). |
LOYALTY_SIGNUP |
Kiosk/guest-display enrollment | Creates the member + phone/email identifier(s) + utility-only consent rows (never an inferred marketing opt-in, CLAUDE.md §9) → the new loyaltyIdentifier. A guest who already exists → ERROR_ALREADY_REGISTERED. |
LOYALTY_REDEEM |
Guest applies a reward | The existing two-phase quote→commit domain, both phases in this one request → ACCEPT with appliedRedemptions[] or rejectedRedemptions[] plus an honest userMessage (insufficient points, reward not applicable). Never partial-silent. |
LOYALTY_ACCRUE |
Check closes, payment completes | ACCEPT immediately; the transaction is persisted and enqueued into the same pipeline every connector uses — points land asynchronously, correct by the guest’s next LOYALTY_INQUIRE. |
LOYALTY_REVERSE |
A prior accrual or redemption is undone | Accrual reversals ride the existing proportional-refund machinery. Redemption reversal is not supported in v1 — an honest ERROR_TRANSACTION_CANNOT_BE_REVERSED-class response, ASSUMPTION(toast-cert)-tagged (see below). |
LOYALTY_TRANSFER |
A guest tries to move points to another guest | Honest, permanent-in-v1 unsupported error (ERROR_INVALID_TRANSFER) — there is no code path that moves points between two guests, or between two tenants (the same posture as coalitions’ groundwork). |
What’s built vs. what’s user-gated
Built + stub-proven this SP — real code, exercised against Toast’s public OpenAPI spec
(toast-integrations-loyalty-api.yaml) turned into fixtures, and wiremock stand-ins for Toast’s
own APIs (auth login, the JWT public-key endpoint, partner restaurant listing, orders-bulk) — never
against Toast’s real, live systems:
- The loyalty endpoint’s full 7-type dispatch, JWT verification, and both idempotency layers (see below).
- The
pos-gatewayadapter:Toast-Signaturewebhook verification, event mapping (accrual/reversal → canonical transactions, orders-poll → anonymous canonicals), and theConnectorAdaptercontract suite (Toast as the 6th provider alongside Square, Clover, Lightspeed, LithosPOS, generic). - Connection lifecycle: the dashboard’s Integrations → Toast card (credentials-paste connect form, disconnect), the combined token-refresh cron leg, the orders-poll reconciliation cron leg, and partners-webhook handling (integration added/removed/settings-updated).
User-gated — cannot run against a real Toast restaurant without it, and nothing here is faked in the meantime:
- Toast partner application approval. This platform is pre-application as of this SP — see the partner path below.
- Sandbox credentials and a sandbox host, issued at Dev Kickoff, after the application and agreement stages clear.
- Production credentials, issued at Certification — Toast’s own roughly one-hour demo review,
which is also the point every
ASSUMPTION(toast-cert)item below gets confirmed or corrected against real traffic.
Until credentials exist, the dashboard’s Toast card still renders honestly: the connect form is
real, but submitting it reaches the same POST /v1/pos-connections route every other
manually-created provider uses, and — like every provider other than LithosPOS — that route 400s
until its Toast branch ships (see Connect a restaurant below). Nothing
about the UI pretends a connection exists that doesn’t.
The partner application path
Toast’s own gate, not this platform’s — every stage below is Toast’s process, run once per partner (not per merchant):
| Stage | What happens |
|---|---|
| Application | Apply to Toast’s partner program. |
| Agreement | Contractual terms with Toast. |
| Dev Kickoff | Toast issues sandbox credentials and a sandbox host. |
| Certification | Toast issues production credentials after a live review (their own ~1-hour demo review is, in effect, Toast’s own end-to-end test of this integration). |
| Alpha → Beta → GA | Staged rollout to real merchants. |
Connect a restaurant
POST /v1/pos-connections
Auth: dashboard session or apiKey with connectors:manage — the same route and permission every
other connection type uses.
Unlike Square, Clover, and Lightspeed (an OAuth redirect: GET /v1/pos/<provider>/connect) or
LithosPOS (one optional label field), Toast is credentials-paste: the merchant (or this
platform’s own operator, pre-certification) pastes the machine-client credentials Toast issued.
The dashboard’s Integrations → Toast card collects:
| Field | Notes |
|---|---|
| Client ID | Write-only — never echoed back after saving. PosConnectionResponse has no credentials field to echo it into, even by accident. |
| Client secret | Write-only, same as above. |
| Restaurant GUID | Required; this becomes the connection’s provider_account_ref — the routing key Toast-Restaurant-External-ID resolves against on every loyalty-endpoint call. |
| Sandbox host override | Optional. Honest gated copy, not a guessed value: “Leave empty for production. Toast issues sandbox credentials after partner approval.” Toast doesn’t publish a sandbox hostname format before Dev Kickoff, so this field carries no example/placeholder pretending to know one — production’s confirmed default is ws-api.toasttab.com. |
The intended shape (connect lifecycle work, not yet landed as of this guide — see the
status note at the top): credentials are validated with a real
POST /authentication/v1/authentication/login call, encrypted at rest, and never appear in any
GET/response again — the identical “credentials never in any response” rule every other
connection type already follows. Location metadata (restaurant name, management group) is
expected to populate the connection’s mapping once Toast’s partner-restaurant listing is synced;
until then, the dashboard shows exactly what it already knows — the restaurant GUID — and an
honest “—” for anything it doesn’t, never a fabricated value.
Idempotency: two layers, different keys
Toast’s own retry semantics differ by transaction type — REDEEM is never retried,
REVERSE retries reuse the same Toast-Transaction-GUID, and ACCRUE retries arrive with a
different one. One idempotency key can’t cover all three, so this connector runs two, deliberately:
- Transport replay — the existing edge-api idempotency response store, keyed
toast:<Toast-Transaction-GUID>. A retried request with the same GUID (Toast’s documented behavior forREVERSE) gets the byte-identical cached response back, stored 48h — Toast’s own requirement (“must return cached duplicate response”). - Business/ledger dedupe —
canonical_transactions.external_id = toast-order-<orderGuid>-<checkGuid>. This is what actually protectsACCRUE: since a retried accrual carries a new GUID, the transport layer never sees it as a duplicate, but the order/check pair is stable across the retry — and across this connector’s own orders-poll gap-fill (both paths converge on the identical external id) — so the ledger’s ownUNIQUEconstraint makes double-earning structurally impossible, not just unlikely.
webhook_events delivery ids for the pipeline entries this endpoint creates follow the same
order/check pairing — accrue:<orderGuid>:<checkGuid> / reverse:<orderGuid>:<checkGuid> — so
even a different-GUID ACCRUE retry is caught a second time, at insert, before it ever reaches
the queue.
REDEEM skips both layers by design — Toast never retries it — and instead commits synchronously,
in-request, keyed toast-redeem-<orderGuid>-<checkGuid>-<rewardId>, through the same quote→commit
domain the till API uses.
Latency posture
Toast’s own budget: 500ms average, 5 seconds hard — past 5s, Toast may close the connection
and resend. This connector holds to that by keeping every code path free of external fetches: the
JWT verification key is KV-cached (GET /usermgmt/v1/oauth/token_key, refetched once on a
verification failure to handle key rotation, not on every request), and LOYALTY_ACCRUE
deliberately does not wait for points to actually post — it accepts immediately and lets the
same asynchronous pipeline every other connector uses earn the points, correct by the guest’s next
LOYALTY_INQUIRE. LOYALTY_REDEEM is the one type that must do real work before responding (a
ledger commit) and is budgeted as the heaviest of the seven.
The anonymous orders-reconciliation note
Toast’s Orders API (GET /orders/v2/orders/{guid}, ordersBulk) is a completely separate,
loyalty-blind surface — order payloads never carry a loyalty identifier, full stop. That’s why
the loyalty endpoint above has to be the primary earn path in the first place, and it’s also why
this connector’s poll leg can only ever be a reconciliation safety net, not a source of
member-linked earning:
- Orders whose
orderGuid/checkGuidmatch a canonical transaction the loyalty endpoint already ingested are recognized and skipped — no double-booking. - Orders that genuinely never reached the loyalty endpoint (Toast’s own retry ladder stops after
two attempts; a check that closed past the 5s budget without a successful
ACCRUEis exactly the gap this leg exists for) are ingested as anonymous canonical transactions — zero points, no member — the same honest anonymous-transaction posture CLAUDE.md §7 describes for every connector’s guest sales. They still feed analytics and remain eligible for a later enrolled-backfill pass; they are never guessed into a member’s history.
Assumptions pending certification
Toast’s certification review (the partner path’s final stage) is the first point this platform can
observe real traffic from a live register. Until then, these items are ASSUMPTION(toast-cert)
in the code — implemented against the best available public documentation, fixture-pinned, and
flagged for re-verification rather than silently treated as settled:
- JWT claims. Toast’s public docs confirm signature verification (RS256, against the published
key) and don’t document an
iss/audclaim contract. v1 validates signature + expiry + successful restaurant resolution only; claim tightening is a certification-time follow-up, not a v1 gap that was overlooked. - Offer-amount units.
checkResponse.offers[].amount’s unit — dollars, cents, or points — isn’t pinned by the public spec. Implemented against a stated assumption, fixture-pinned, confirmed (or corrected) against a real register at certification. transactionStatuserror-code completeness. The nineERROR_*codes this connector knows about are every one confirmed from Toast’s public documentation — not asserted to be Toast’s complete list. Toast may define additional codes this connector hasn’t seen yet.Toast-Signatureconcatenation order. The partners-webhook signature is documented as an HMAC-SHA256 over the body and a timestamp; the exact byte-level concatenation is implemented from the public doc’s phrasing and re-checked against real signed traffic at certification, the same as every other assumption on this list.
API reference
The loyalty endpoint (POST /webhooks/toast/loyalty) and the partners webhook
(POST /webhooks/toast) are infrastructure endpoints, deliberately absent from the public
OpenAPI spec — the same treatment every inverted or provider-signed callback on this platform
gets (Square’s and Clover’s own OAuth callbacks, Lightspeed’s inverted endpoints). Both are covered
by the existing /webhooks/* production route; nothing new to open at the edge. Their wire
contract is this page.
POST /v1/pos-connections and its siblings (list, get, patch the location mapping, disconnect)
are public — full request/response schemas are in the API reference under the
PosConnections tag.
v1 limits
- Redemption reversal isn’t supported. A
LOYALTY_REVERSEreferencing a priorLOYALTY_REDEEMgets an honest unsupported error, not a silent no-op — see the register flow table above. LOYALTY_TRANSFERis permanently unsupported in v1 — no code path moves points between two guests.- Restaurant mapping starts manual. The merchant enters the restaurant GUID directly; automatic discovery from Toast’s partner-level restaurant listing is expected once real credentials exist, not before.
- No bulk historical import. Orders-poll reconciliation is a forward-looking gap-fill, not a backfill of a restaurant’s pre-connection history.
- The live leg is entirely user-gated. See What’s built vs. what’s user-gated above — nothing here runs against a real Toast restaurant without a completed partner application.
Next steps
- Webhook verification — this platform’s own outbound signature
scheme (a different, unrelated recipe from Toast’s own
Toast-Signature, which this connector verifies, not issues). - Generic connector — the canonical transaction shape Toast’s accruals and orders-poll events both map into.
- LithosPOS connector — a second worked example of this platform’s connector framework, on the more familiar webhook-push shape Toast’s own order events (but not its loyalty calls) resemble.
- API reference —
PosConnectionResponse,PosConnectionCreateRequest, and every other/v1/pos-connectionsschema. infra/DEPLOY.md— deploy-time notes for this connector (migrations 0034–0035, the two cron legs it joins, no new platform secret — see its §8 smoke item).