# Integration Checklist

Read first: [Order Injection Guide](05-order-injection-guide.md) · [Status Webhook Guide](06-status-webhook-guide.md) · [Message & Error Codes](07-message-and-error-codes.md)

This is the normative go-live gate for a **Digital Ordering API** integration. Every item is objectively verifiable — you either did it or you did not — and LoyaltyPlant runs an equivalent checklist during certification. Work through it against the contract; field-level truth is the [Digital Ordering API specification](/digital-ordering/api/), which this checklist links to and never restates.

Remember the inversion: **you host the five `/rest/*` endpoints and LoyaltyPlant is the HTTP client**; the order-status webhook is the one call that runs from your side to LoyaltyPlant (see [Ordering Flows](03-ordering-flows.md)). Items marked **(conditional)** apply only if you implement that capability — skip them otherwise and note the skip in your test report.

## 1. Contract conformance

- [ ] All five endpoints are hosted under your `{vendorBaseUrl}` at the exact paths: `GET /rest/healthcheck`, `GET /rest/menu`, `GET /rest/menu/revision`, `POST /rest/order`, `POST /rest/orders`.
- [ ] `{vendorBaseUrl}` is reachable from LoyaltyPlant over TLS (HTTPS), and the per-environment base URL is taken from onboarding — not hard-coded to any host seen in this documentation or the spec.
- [ ] Every response is `application/json`; request and response payloads validate against the schemas in the [spec](/digital-ordering/api/) — field types, required fields, and enum values match (the spec lint passes).
- [ ] The `SalesOutletId` request header is resolved to the correct outlet/menu/configuration on your side, and is configurable per outlet — not hard-coded.
- [ ] The menu response uses the documented shape: the catalog wrapped under `menu`, with `requestId`.

## 2. Authentication

There are **three** distinct auth directions; certify each one separately. The full model is in [General Requirements](02-general-requirements.md).

- [ ] **Request, LP → you.** On `/menu`, `/menu/revision`, `/healthcheck`, `/order` and `/orders` — **every** LoyaltyPlant→vendor call, including the healthcheck — the `AuthorizationToken` header is validated as `SHA256-hex(apiKey)` (lowercase hex) for the call's `SalesOutletId`, and a mismatch is rejected.
- [ ] **Response, you → LP.** The HTTP responses of `/order`, `/orders`, `/menu`, `/menu/revision`, and `/healthcheck` carry an `AuthorizationToken` response header equal to `SHA256-hex(apiKeyResponse)` (the response key) — verified present and correct, because LoyaltyPlant discards the whole response (treating the operation as failed) when it is missing or wrong.
- [ ] **Healthcheck response carries the response token too** — verify `/rest/healthcheck` returns `AuthorizationToken: SHA256-hex(apiKeyResponse)`, like every other LoyaltyPlant→vendor response.
- [ ] **Webhook, you → LP.** The status webhook you POST carries `SalesOutletId` plus an `AuthorizationToken` equal to `SHA256-hex(apiKey)` — the **same** hashed token as every LoyaltyPlant→vendor call. Verified against a real outlet key, the webhook is accepted.

> **Note:** All three auth directions use `SHA256-hex` of the relevant key — the request and the webhook both use `SHA256-hex(apiKey)`, the response uses `SHA256-hex(apiKeyResponse)`.

## 3. Healthcheck

- [ ] `GET /rest/healthcheck` returns a flat JSON map of service-name → `UP`/`DOWN`, listing the real services whose failure would break order acceptance (e.g. POS, DB) — not a blanket `UP`.
- [ ] When **any** service reports `DOWN`, the outlet is correctly treated as unavailable: LoyaltyPlant runs the healthcheck before every order and does **not** inject an order if any service is `DOWN`.
- [ ] The check is fail-safe: no response, a timeout, an error, or an unparseable body must result in the outlet being treated as unavailable rather than letting orders through. The healthcheck accounts for the absence of an internet connection and other failures.

## 4. Menu

- [ ] The full menu is returned to `GET /rest/menu` with categories, items, reusable modifier groups, and the modifier pool, each referenced by `refId` within the response.
- [ ] Category nesting never exceeds **two levels** (Category → Subcategory → Item, or Category → Item); a root category contains **either** subcategories **or** items, not both; subcategories contain items only.
- [ ] Sort order is supplied: `rootSortOrder` on root categories and `sortOrder` on items/subcategories/modifiers, so LoyaltyPlant can order the menu as your POS intends.
- [ ] Modifier groups carry `min`/`max`/`free` (scalar integers): `min > 0` makes a selection from the group mandatory; `min = 0` makes the group optional; `free` modifiers are not charged.
- [ ] A `revision` (epoch timestamp) is returned in the `GET /rest/menu` response, and `GET /rest/menu/revision` returns the **same** value, so LoyaltyPlant can cache the menu and skip re-pulling an unchanged one.
- [ ] The stop-list works by omission: an item absent from the next `GET /rest/menu` response is automatically marked unavailable in LoyaltyPlant — there is no separate delete call.
- [ ] Titles and descriptions for categories, items and modifiers are multilingual, keyed by ISO 639-1 language code (e.g. `en`, `es`, `fr`).
- [ ] Per-item prices are returned in `pricesByOrderingType`, with tax percentages broken down by ordering type (`PICKUP`, `EAT_IN`, `DELIVERY`) in each price's `taxes` array.
- [ ] **(conditional)** If your POS uses combos, they are handled per-POS as ordinary items + modifier groups (with a B1G1 flag where applicable) — a generic combo contract is **not** part of the Digital Ordering API and is not expected on the wire.

## 5. Orders

- [ ] An order is only ever injected after a healthy healthcheck: verify your POS never receives a `POST /rest/order` it has not just reported it can handle (LoyaltyPlant healthchecks immediately before every create).
- [ ] `POST /rest/order` accepts and honors the standard order types: **Pickup**, **Delivery** (with the `address` block and coordinates), **Eat-in** (with `additionalInfo.tableNumber`), and **Drive-through** (per outlet — like pickup, with no address). The `OrderingType` enum additionally defines **CATERING**, which has no current production usage (verify against your POS model before relying on it). A reader should expect to see all five values in the spec enum. Dine-in at a table is `EAT_IN` + `tableNumber`.
- [ ] Order line items with their `modifiers` are accepted, and an order-level `comment` is accepted.
- [ ] Reward items in `presents[]` are added to the order at price **0** (e.g. a 100% discount or an explicit zero price) — they never increase the total the customer pays.
- [ ] The `payment.charge` breakdown is honored: order `discount` (a fixed amount; discount codes are not supported), `tips`, `deliveryFee`, plus `subtotal`/`tax`/`total`.
- [ ] The order's `deliveryAt` timing is honored: `ASAP` goes to preparation immediately; `CUSTOM` is prepared for the given `datetime`.
- [ ] The `createOrder` response returns `posId` and a customer-facing `queueNumber`.
- [ ] **The `/order` (and `/orders`) responses carry the response `AuthorizationToken`** = `SHA256-hex(apiKeyResponse)` — verify it is present, or LoyaltyPlant discards the response and the order is treated as failed (cross-check §2).
- [ ] `POST /rest/orders` returns the current `state.status` and `queueNumber` for each requested `posId`, and includes a `state.reason` whenever the status is `CANCELED` or `FAILED`.
- [ ] **(conditional)** If your POS charges card-in-store or card-in-app, the `payment.method` is read and the masked `pan` is handled as masked (never a clear PAN).

## 6. Status webhooks

- [ ] The webhook is POSTed to `{lpBaseUrl}/digitalordering/pos/v2/integrated` on **every** order status change, carrying the minimal payload LoyaltyPlant acts on: `orders[].id` (the `posId`), `orders[].queueNumber`, and `orders[].state.status`.
- [ ] `state.status` is always one of the nine `OrderStatus` values, matched exactly — an unknown value (typo, wrong case, unsupported status) is **not** rejected: it is caught per-order, logged, and skipped (that order's state is not advanced), while the webhook still returns HTTP 200 `accepted: true`. It does **not** produce `INTERNAL_ERROR`/500. Detect a silently skipped status via the `getOrders` poll path; do **not** retry it with backoff.
- [ ] The `WebhookResponse` is processed by branching on `accepted`: only HTTP 200 with `accepted: true` means delivered. The four failure codes are handled per their HTTP status — `READ_ERROR`/`INVALID_JSON`/`VALIDATION_ERROR` (400) and `INTERNAL_ERROR` (500).
- [ ] Retries are applied correctly: a deterministic 400 (`VALIDATION_ERROR`/`INVALID_JSON`/`READ_ERROR`) is fixed before resending, while `INTERNAL_ERROR` (500) and transport failures are retried with backoff. Re-delivering an already-applied status is a safe no-op.
- [ ] **(conditional)** The poll path is supported as the safety net: `POST /rest/orders` returns the current state so a dropped webhook is reconciled on the next poll cycle (recommended — run push with poll as a backstop).

## 7. Performance

- [ ] `POST /rest/order`, `POST /rest/orders` and `GET /rest/healthcheck` respond well within LoyaltyPlant's client timeouts of **5 s connect / 60 s read**.
- [ ] `GET /rest/menu` and `GET /rest/menu/revision` respond within the menu timeouts of **30 s connect / 180 s read**.
- [ ] HTTP redirects on these calls are accounted for — LoyaltyPlant follows a lax redirect policy when calling you.

## 8. Onboarding & certification

LoyaltyPlant verifies the integration before it goes live; this section mirrors that verification process so there are no surprises at the gate.

- [ ] LoyaltyPlant is notified **about a week ahead** of the planned completion date, so the verification slot and test plan can be scheduled; if the date slips, a new one is agreed.
- [ ] The certification test cases are run on your side and a **test report** is produced against the LoyaltyPlant test plan.
- [ ] Access to your test environment is provided to LoyaltyPlant, which verifies the integration against its test plan; flagged cases are fixed and re-verified until all pass.
- [ ] Once every case passes, LoyaltyPlant considers the integration complete and ready for deployment.
- [ ] The **first deployment** to a POS terminal happens with LoyaltyPlant representatives present, or is recorded on video and shared with LoyaltyPlant.

> **Note:** Some onboarding values are confirmed at kickoff, not in this contract: your per-environment `{vendorBaseUrl}` and `{lpBaseUrl}`, the request and response API keys (`apiKey` / `apiKeyResponse`), the menu sync interval, and any `additionalInfo.customFields`. Your LoyaltyPlant integration manager provisions these and shares the test plan, test app, and certification test cases.

---

Once every applicable item is checked and your test report is complete, contact your LoyaltyPlant integration manager (or **integrations@loyaltyplant.com**) to schedule the certification run.

← Back to [Overview](01-overview-and-concepts.md)
