# General Requirements

Read first: [Overview and Concepts](01-overview-and-concepts.md)

This page collects everything that is true of *every* **Digital Ordering API** call, so the capability guides that follow can stay focused on one flow at a time: how requests travel, how each direction is authenticated, how LoyaltyPlant identifies a sales outlet, the field conventions that apply across operations, and where you build and test against. Field-level truth — schemas, types, enums, per-operation examples — lives in the [Digital Ordering API specification](/digital-ordering/api/); this page links to it and never restates schemas.

> **Read this twice.** Digital Ordering inverts the usual mental model. **You (the vendor / POS integration) host the five `/rest/...` endpoints; LoyaltyPlant is the HTTP client that calls them.** The only call that runs the other way is the order-status **webhook** you POST to LoyaltyPlant. The timeouts below are therefore *your* response budget, and the request `AuthorizationToken` is something you *validate*, not something you send.

## 1. Transport and connectivity

You host a base URL — written `{vendorBaseUrl}` throughout this set — and LoyaltyPlant appends `/rest/...` to it to form each endpoint, e.g. `{vendorBaseUrl}/rest/order`. The base URL must be reachable from LoyaltyPlant over TLS; its real value is provisioned during onboarding (§6).

| Property | Value |
|---|---|
| Protocol | HTTPS |
| Method | `GET` for `/rest/healthcheck`, `/rest/menu`, `/rest/menu/revision`; `POST` for `/rest/order`, `/rest/orders` |
| URL shape | `{vendorBaseUrl}/rest/{endpoint}` |
| Body media type | `application/json` (LoyaltyPlant sends and accepts `application/json;charset=UTF-8` on the order and healthcheck calls) |
| Character encoding | UTF-8 |
| Redirects | LoyaltyPlant follows HTTP redirects (a "lax" redirect policy) |

### 1.1 Timeouts — your response budget

LoyaltyPlant applies fixed client-side timeouts when it calls you. Because LoyaltyPlant is the client, these are the windows *your* endpoints must respond within — exceed them and LoyaltyPlant treats the call as failed (an unavailable outlet on healthcheck, a discarded menu, an order that was not injected). The connect and read timeouts differ by operation family:

| Operation | Connect | Read |
|---|---|---|
| `POST /rest/order`, `POST /rest/orders`, `GET /rest/healthcheck` | 5 s | 60 s |
| `GET /rest/menu`, `GET /rest/menu/revision` | 30 s | 180 s |

> **Note:** The menu read window is the most generous (180 s) because a full menu can be large; even so, return a revision quickly on `GET /rest/menu/revision` so LoyaltyPlant can skip the heavy menu pull when nothing changed (see the [Menu Sync Guide](04-menu-sync-guide.md)). The 60 s order/healthcheck window is comfortable for a single POS write — do not block it on slow downstream calls.

## 2. Authentication — three directions

This is the keystone section. The Digital Ordering API has **three distinct authentication directions**, and the most common integration mistake is conflating them. All three hash the shared key with SHA-256 — directions (a) and (c) hash the `apiKey`, direction (b) hashes the `apiKeyResponse`. Read the table, then the per-direction detail.

| # | Direction | On which calls | `AuthorizationToken` value | What enforces it |
|---|---|---|---|---|
| (a) | LoyaltyPlant → you, on the **request** | `/menu`, `/menu/revision`, `/healthcheck`, `/order`, `/orders` | `SHA256-hex(apiKey)`, lowercase hex | You validate it; reject a mismatch |
| (b) | You → LoyaltyPlant, on the **response** | `/order`, `/orders`, `/menu`, `/menu/revision`, `/healthcheck` | `SHA256-hex(apiKeyResponse)`, lowercase hex | LoyaltyPlant verifies it; missing/wrong → whole response discarded |
| (c) | You → LoyaltyPlant, on the **webhook** | `POST {lpBaseUrl}/digitalordering/pos/v2/integrated` | `SHA256-hex(apiKey)`, lowercase hex | LoyaltyPlant verifies it; mismatch → `VALIDATION_ERROR` |

Two shared secrets are in play. The **`apiKey`** is used on directions (a) and (c); a *second*, distinct key, the **`apiKeyResponse`**, is used only on direction (b). Both are assigned per outlet during onboarding (§3, §6).

### 2.1 (a) The request token LoyaltyPlant sends you

On every call LoyaltyPlant makes to your endpoints, it sends two headers:

```http
SalesOutletId: 4198
AuthorizationToken: a3f5...e1   (64 lowercase hex chars = SHA256-hex of the apiKey)
```

The `AuthorizationToken` is the lowercase SHA-256 hex of the shared `apiKey` LoyaltyPlant holds for that outlet. Compute the same hash from the key *you* hold for the `SalesOutletId` and compare; reject the call if it does not match. LoyaltyPlant sends this on `/menu`, `/menu/revision`, `/healthcheck`, `/order`, and `/orders`.

```text
authorizationToken = lowercase_hex( SHA256( apiKey ) )
```

### 2.2 (b) The response token you must echo back

On every LoyaltyPlant→vendor response — `/order`, `/orders`, `/menu`, `/menu/revision`, and `/healthcheck` — your HTTP **response** MUST carry an `AuthorizationToken` header equal to the lowercase SHA-256 hex of the **`apiKeyResponse`**. LoyaltyPlant verifies this header and, if it is **missing or does not match, discards the entire response and treats the operation as failed** — the menu is not imported, the order is treated as not created, the poll yields nothing.

```http
AuthorizationToken: 9b74...c0   (64 lowercase hex chars = SHA256-hex of the apiKeyResponse)
```

```text
responseAuthorizationToken = lowercase_hex( SHA256( apiKeyResponse ) )
```

> **Note:** The `apiKeyResponse` is a *separate* key from the request `apiKey` and may be set to a different value. In practice, most integrations configure the two to the same value — that is allowed, but do not rely on it: treat the response token as `SHA256-hex(apiKeyResponse)` and the request token as `SHA256-hex(apiKey)`, and let onboarding decide whether the two keys coincide.

### 2.3 (c) The webhook token you send

The order-status webhook is the one call that travels from you to LoyaltyPlant. It carries two headers: `SalesOutletId` (numeric) and an `AuthorizationToken`. The webhook `AuthorizationToken` is `SHA256-hex(apiKey)` — the **same** hashed value as the request token in direction (a). LoyaltyPlant compares it case-insensitively.

```http
SalesOutletId: 4198
AuthorizationToken: <SHA256-hex(apiKey), lowercase hex>
```

> **Note:** The webhook uses the **same** `SHA256-hex(apiKey)` token as the LoyaltyPlant→vendor request (direction (a)) — all three auth directions are on the hashed scheme, so there is no separate raw-key rule to special-case. Full webhook header rules and the validation order are in the [Status Webhook Guide](06-status-webhook-guide.md).

### 2.4 Validating a request — the rhythm in pseudocode

The same skeleton applies to all of your `/rest/...` handlers:

```text
on incoming LP -> vendor request:
    salesOutletId = header "SalesOutletId"            # numeric
    settings      = lookup_outlet(salesOutletId)      # apiKey + apiKeyResponse
    expected = lowercase_hex(SHA256(settings.apiKey))
    if header "AuthorizationToken" != expected:       # case-insensitive
        reject the request
    ... build the response body ...
    set response header "AuthorizationToken" =
        lowercase_hex(SHA256(settings.apiKeyResponse))
    return response

when you push a status update to LoyaltyPlant (the webhook):
    set header "SalesOutletId"      = <numeric outlet id>
    set header "AuthorizationToken" = lowercase_hex(SHA256(settings.apiKey))   # same hashed token as the request (direction a)
    POST {lpBaseUrl}/digitalordering/pos/v2/integrated
```

## 3. Outlet identity

Every LoyaltyPlant→vendor call, and every webhook you send, carries a numeric **`SalesOutletId`** header identifying the physical sales outlet the call targets. Resolve it on your side to the correct sales outlet, menu, prices, and the `apiKey` / `apiKeyResponse` pair for that outlet.

- **One outlet, one menu, one configuration.** Because each request is scoped by `SalesOutletId`, the same brand can serve different menus, prices, and availability at different outlets — your `GET /rest/menu` response and `POST /rest/order` handling must vary per outlet.
- **`SalesOutletId` must be numeric on the webhook.** LoyaltyPlant parses the webhook's `SalesOutletId` header as an integer; a non-numeric value is rejected with `VALIDATION_ERROR` before any status is applied.
- **Configurability is a certification item.** `SalesOutletId` (and the two keys) must be configurable per outlet on your side, never hard-coded — see the [Integration Checklist](08-integration-checklist.md).
- **LoyaltyPlant provides the `SalesOutletId` values.** You do not invent them. The test outlet's id comes in your onboarding package (§6); the production `SalesOutletId`s, mapped to your physical outlets, are provided by LoyaltyPlant when a mutual client is provisioned for launch (see [Overview §4.5](01-overview-and-concepts.md)).

## 4. Conventions

These rules hold across all operations. They are what a reader most often gets wrong from the schemas alone. The spec ([`do-api-v2-spec.yaml`](/digital-ordering/api/)) is authoritative for exact field types and required-ness.

### 4.1 Field types and `null` handling

JSON fields use standard types: strings, integers, and decimal `number`. LoyaltyPlant omits `null` fields from the wire rather than sending explicit nulls, and empty menu sub-objects are omitted on serialization; **plan for fields and sub-objects to be simply absent** rather than present-and-null. (The top-level menu arrays — `categories`, `modifierGroups`, `modifiers`, `items` — are an exception: they may still appear as empty arrays `[]` rather than being absent when nothing is present.)

### 4.2 The menu `revision` (epoch timestamp)

The menu `revision` is an **epoch timestamp** (`int64`, seconds), returned identically by `GET /rest/menu` (inside `menu.revision`) and `GET /rest/menu/revision`. LoyaltyPlant uses it to decide whether to re-import: it re-pulls the full menu when the revision changed, when it has no cached value, or when the returned revision is *lower* than its cache. The two endpoints must agree on the value. Full behavior is in the [Menu Sync Guide](04-menu-sync-guide.md).

### 4.3 Money and decimals

Monetary fields (`price`, `value`, `subtotal`, `tax`, `total`, `deliveryFee`, `tips`, `discount`) are JSON `number` (decimal). On a `createOrder` request the charge breakdown's `tax`, `subtotal`, and `total` are always sent; reward items in `presents[]` must be charged **0** (they never increase the total the customer pays).

> **Note:** The protocol does not declare a currency, decimal scale, or rounding rule on the wire — monetary values are plain decimals, and the outlet's currency is configured on the LoyaltyPlant side. Agree the decimal precision and rounding to use with LoyaltyPlant at onboarding and confirm it at certification.

### 4.4 Timestamps

Time-of-fulfilment timestamps (`deliveryAt.datetime`) are **ISO 8601** strings, sent only when `deliveryAt.type = CUSTOM`; for `ASAP` no datetime is sent. The menu `revision` is the one timestamp that is an epoch integer, not ISO (§4.2).

### 4.5 Multilingual text

Menu `title` and `description` are **multilingual objects** keyed by ISO 639-1 language code (e.g. `{"en": "Cheeseburger", "es": "Hamburguesa con queso"}`), not plain strings. At least one language is expected on a required title. Provide every language the outlet serves.

### 4.6 Order types

`Order.type` is one of **`PICKUP`** (collect at the counter), **`DELIVERY`** (courier delivery — `address` with `coordinates` and `addressString1` is required), **`EAT_IN`** (dine-in, optionally with `additionalInfo.tableNumber`), and **`DRIVE_THROUGH`** (drive-through pickup). The enum also carries `CATERING` (defined in the enum but with no current production usage — verify against your POS model before relying on it). For dine-in at a table use `EAT_IN` plus `tableNumber`. Details and examples per type are in the [Order Injection Guide](05-order-injection-guide.md).

## 5. Versioning

This documentation set and the [Digital Ordering API specification](/digital-ordering/api/) describe the current contract for new integrations. There is no version header or content negotiation: the endpoints are always `/rest/menu`, `/rest/order`, and so on.

## 6. Environments and test tooling

The base URL you host (`{vendorBaseUrl}`) and the LoyaltyPlant webhook base URL (`{lpBaseUrl}`) are real, per-environment values provisioned during onboarding — examples in this set and the `servers` entry in the spec are illustrative placeholders, not endpoints to hardcode. The deployed webhook path is `{lpBaseUrl}/digitalordering/pos/v2/integrated`.

> **Note:** Your test sales outlet, its `SalesOutletId`, the `apiKey` / `apiKeyResponse` pair, and the test `{lpBaseUrl}` come with your onboarding package. Confirm the exact test host and credentials with LoyaltyPlant at onboarding.

## 7. Glossary

Terms used throughout this documentation set. Field-level definitions live in the spec; this table gives the meaning.

| Term | Meaning |
|---|---|
| **SalesOutletId** | Numeric id of one physical **sales outlet**, sent as a header on every LoyaltyPlant→vendor call and on the webhook; resolve it to the sales outlet's menu, configuration, and keys. |
| **sales outlet** | A single physical store, identified by its `SalesOutletId`. (If you also integrate the In-Store Loyalty API, note that it calls the same physical store an `establishmentId`.) |
| **apiKey** | The shared outlet secret used to authenticate requests: hashed (`SHA256-hex`) on both LoyaltyPlant→vendor calls and the webhook (§2.3). |
| **apiKeyResponse** | The response-side outlet secret (which may differ from `apiKey`); its `SHA256-hex` is the `AuthorizationToken` your responses must carry on `/order`, `/orders`, `/menu`, `/menu/revision`, and `/healthcheck` (§2.2). |
| **revision** | The menu version, an epoch-timestamp `int64` returned by both menu endpoints; LoyaltyPlant compares it to its cache to decide whether to re-import (§4.2). |
| **refId** | A response-scoped reference key, unique within a single `/rest/menu` response, by which menu parents reference children (an item's category, an embedded group's `basedOn`). May be reused across separate responses. |
| **posId** | The POS's own id for an order, returned from `createOrder`; LoyaltyPlant stores it and uses it on `getOrders` and the webhook. |
| **queueNumber** | The customer-facing queue / pickup number the POS assigns and echoes on reads and the webhook (declared as string; numeric accepted). |
| **webhook** | The single vendor→LoyaltyPlant push: `POST {lpBaseUrl}/digitalordering/pos/v2/integrated`, reporting an order's status change (§2.3, [Status Webhook Guide](06-status-webhook-guide.md)). |

---

**Next:** [Ordering Flows →](03-ordering-flows.md)
