# Order Injection Guide

Read first: [General Requirements](02-general-requirements.md) · [Ordering Flows](03-ordering-flows.md)

This guide walks you through implementing the two **order** endpoints LoyaltyPlant calls on your service once a customer checks out: `getHealthcheck` (the availability probe LoyaltyPlant runs before every order) and `createOrder` (the call that injects a paid order into your POS), plus `getOrders` (the reconciliation read LoyaltyPlant uses to poll order state). Remember the inverted contract: **you host these `/rest/...` endpoints and LoyaltyPlant is the HTTP client** — it calls you.

Field-level truth — every field's type, required-ness, and enum values — lives in the [Digital Ordering API specification](/digital-ordering/api/); this guide links to it and never restates schemas. The operations covered here are [`getHealthcheck`](/digital-ordering/api/), [`createOrder`](/digital-ordering/api/), and [`getOrders`](/digital-ordering/api/).

The whole sequence you implement on this side:

```mermaid
sequenceDiagram
    actor Customer
    participant LP as LoyaltyPlant
    participant POS as YOUR POS (you host /rest/*)

    Customer->>LP: Builds the cart, pays, confirms
    LP->>POS: GET /rest/healthcheck
    POS-->>LP: { "POS": "UP", "DB": "UP" }
    LP->>POS: POST /rest/order (full order)
    POS->>POS: Create the order / kitchen ticket
    POS-->>LP: 200 { posId, queueNumber } + AuthorizationToken header
    LP-->>Customer: Order confirmed, shows the queue number
    loop Reconciliation when a webhook is missed
        LP->>POS: POST /rest/orders (posIds)
        POS-->>LP: current state per order + AuthorizationToken header
    end
```

The key point of the sequence: the healthcheck always runs first, the create response **must** carry the response `AuthorizationToken` header, and `getOrders` is the safety net LoyaltyPlant uses to catch up on state when a status webhook was missed.

## 1. Prerequisites

You need four things from LoyaltyPlant, all assigned at onboarding and defined in full in [General Requirements](02-general-requirements.md): your hosted base URL `{vendorBaseUrl}`, the numeric `SalesOutletId` header, and the two keys — the request key `apiKey` and the response key `apiKeyResponse`. The request- and response-token mechanics are covered there too; this guide assumes you have already implemented request-token validation and focuses on the order payloads and the one response-token rule that most often goes wrong (§3.6).

## 2. Step 1 — `GET /rest/healthcheck`

LoyaltyPlant calls [`getHealthcheck`](/digital-ordering/api/) — `GET {vendorBaseUrl}/rest/healthcheck` — **immediately before every order injection**, and only proceeds with `createOrder` if your answer says you can take the order. This is not a periodic liveness ping; it runs on the critical path of each individual order.

Return a flat JSON object whose keys are service names **you** choose and whose values are `UP` or `DOWN`. There is no fixed key set — list the real services whose failure would stop you fulfilling an order (for example your POS, your database, your kitchen system). The full shape is the `HealthcheckResponse` schema in the spec.

```json
{
  "POS": "UP",
  "DB": "UP",
  "KITCHEN": "UP"
}
```

The decision rule LoyaltyPlant applies to your answer:

- **All values `UP`** → LoyaltyPlant proceeds to `createOrder`.
- **Any value `DOWN`** → LoyaltyPlant treats the outlet as unavailable and **does not create the order**. One `DOWN` blocks the whole order, regardless of how many services are `UP`.
- **No response, a non-parseable body, or an error** → also treated as unavailable; the order is not created. The availability check is fail-safe — a broken healthcheck stops orders rather than letting them through blindly.

> **Note:** Like `/order` and `/orders`, the healthcheck **response must carry the `AuthorizationToken` header** equal to `SHA256-hex(apiKeyResponse)` — LoyaltyPlant verifies it and discards the response if it is missing or wrong. (LoyaltyPlant also sends the request `AuthorizationToken` to you; the response token is the one *you* set.)
>
> **Warning:** Report honestly. A blanket `"POS": "UP"` while your kitchen system is actually down means LoyaltyPlant will inject orders you cannot fulfil. Wire each reported service to a real liveness check, not a hardcoded constant.

## 3. Step 2 — `POST /rest/order`

When the customer has paid and confirmed, LoyaltyPlant injects the order by calling [`createOrder`](/digital-ordering/api/) — `POST {vendorBaseUrl}/rest/order`. The request body is the full order, wrapped under an `order` key (`CreateOrderRequest` → `Order` in the spec). Your POS creates the order and returns the POS order id and a customer-facing queue number.

The full request example is the `pickup` / `delivery` / `eatIn` named examples on `createOrder` in the spec, and the response is `CreateOrderResponseExample`. Rather than repeat the schema, the rest of this section walks the `Order` payload **by concern**, so you know what each block means and which fields are mandatory in which case.

> **Note:** Required-ness on the order is **direction-dependent and business-driven**, so the spec deliberately does not mark most order fields as schema-`required`. On a `createOrder` **request** LoyaltyPlant always sends `type`, `deliveryAt`, `client`, `payment` (with the `charge` totals), `additionalInfo`, and `items` — plus `address` when `type = DELIVERY`. Treat that set as required input even though the schema does not enforce it.

### 3.1 Items and modifiers

The paid order lines arrive in `items[]`. Each line carries an `id` (the menu item `id` you published in `GET /rest/menu`), a `price`, a `count`, and an optional `modifiers[]` array; each modifier carries its own `id` (a menu modifier `id`), `price`, and `count`. The only strictly required field on each line and modifier is `id` — match it to your catalog and build the kitchen ticket from it.

> **Note:** The `id` on an order line maps to the menu item's **global** `id` (the `id` field, not the response-scoped `refId`) from your `getMenu` response. Resolve orders against that product identity, **never** against the response-scoped `refId`. See the [Menu Sync Guide](04-menu-sync-guide.md) for the `id` / `code` / `refId` distinction.
>
> **Note:** Which menu identity that `id` carries is a **per-outlet onboarding choice**. By default LoyaltyPlant sends the menu item's `id`; if your outlet is configured with the `sendPosCode` option, LoyaltyPlant instead sends the menu item's **`code`** (the outlet-local product code) in the very same `id` field. The field is always named `id` on the wire — there is no separate `code` field on an order line — so the switch is invisible in the payload shape. It applies uniformly to items, rewards, and modifiers. Your `getMenu` response publishes both `id` and `code` on every entity (both required; see the [Menu Sync Guide](04-menu-sync-guide.md) §3.2), so choose whichever identity is more natural for your POS — the one it already keys products by — and have LoyaltyPlant set the outlet to match at onboarding, then resolve order lines against it.

### 3.2 The client

`client` carries the customer's contact details: `name`, `email`, and `phoneNumber` are always sent (and are marked `required` on the `Client` schema); `lastName` is optional. Use these for delivery contact and order identification — they are the customer as entered in the LoyaltyPlant app.

### 3.3 Payment methods and the charge breakdown

`payment.method.type` is one of three payment instruments, and your POS must record the right tender against the order:

- **`CASH`** — the customer pays cash on collection or delivery; nothing is charged in-app.
- **`BANK_CARD_IN_APP`** — the card was already charged inside the LoyaltyPlant app; the order arrives **prepaid**. Masked card details may be present in `payment.method.cardInfo` (`cardHolderName`, masked `pan` — never a full clear PAN).
- **`BANK_CARD_IN_STORE`** — the customer will pay by card at your POS; collect payment on your side.

The money breakdown is in `payment.charge`. On a create request, `tax`, `subtotal`, and `total` are always sent (and are the `required` fields on the `Charge` schema). `tips`, `deliveryFee`, and `discount` are optional and appear when relevant — `deliveryFee` typically only for delivery, `discount` when a discount was applied in-app. The `total` is the grand total the customer pays; build your POS check to match it rather than recomputing from line items, which may differ after app-side discounts and rounding.

> **Note:** Monetary fields are JSON numbers (decimals) with no declared currency, scale, or rounding rule at the protocol level. The outlet's currency and the decimal precision to use are agreed with LoyaltyPlant at onboarding; confirm them and verify at certification. If your POS works in integer minor units (e.g. cents), convert on the way in — these are decimal values (in production, up to 3 fractional digits have been observed) and `charge.total` is authoritative.

### 3.4 Discounts, gifts, and presents

A discount already applied in the app reaches you as the single `charge.discount` total — it is reflected in the `total` you receive; do not re-apply it.

Reward items the customer redeemed with loyalty points arrive in a **separate `presents[]` array** (not in `items[]`). They share the `OrderItem` shape, but **your POS must charge 0 for them** — a present never increases the total the customer pays. Add them to the order at zero price.

> **Note:** Combos have no generic wire model in the Digital Ordering API. Where a combo exists it is handled per-POS as ordinary items plus modifier groups (and, for the iiko adapter, a buy-one-get-one flag) — model it as an item with embedded modifier groups; there is no generic combo contract to implement.

### 3.5 Order type, timing, delivery address, and metadata

**Order type.** `type` decides the fulfilment experience. The supported types are:

- **`PICKUP`** — the customer collects at the counter and shows the queue number; no address.
- **`DELIVERY`** — courier delivery; the `address` block (with `coordinates` and `addressString1`) is **required**, and a `deliveryFee` may be present.
- **`EAT_IN`** — dine-in, optionally tied to a table via `additionalInfo.tableNumber`.
- **`DRIVE_THROUGH`** — the customer collects from a drive-through lane; like `PICKUP`, no delivery `address` is sent. Applies to outlets that have a drive-through lane.

The exact wire tokens are `PICKUP`, `DELIVERY`, `EAT_IN`, and `DRIVE_THROUGH` (note `PICKUP`, not `PICK_UP`). The `OrderingType` enum additionally defines `CATERING`, which is code-backed but has **no current production usage** — do not implement against it unless LoyaltyPlant confirms your outlet uses it. For dine-in at a table use `EAT_IN` with `additionalInfo.tableNumber`. The authoritative enum is `OrderingType` in the spec.

**Delivery address.** When `type = DELIVERY`, `address` is sent with `coordinates` (`latitude`/`longitude`) and `addressString1` (the primary single-line address) at minimum; `zipCode`, `country`, `city`, `street`, `house`, `flat`, and `addressString2` may also be present. Use the coordinates for routing and the address strings for the courier.

**Timing — `deliveryAt`.** Every order says when it is wanted: `type = ASAP` (prepare as soon as possible) or `type = CUSTOM` with a `datetime` (ISO 8601, in the outlet's timezone). On `ASAP` the `datetime` is omitted; schedule preparation against whichever mode you receive.

**Metadata — `additionalInfo`.** Carries `tableNumber` (dine-in), `loyaltyPlantOrderId` (LoyaltyPlant's internal numeric order id — useful for support correlation), and `customFields` (free-form string key/value pairs agreed at onboarding). Store `loyaltyPlantOrderId` against your POS order so support can cross-reference.

### 3.6 The response — `posId`, `queueNumber`, and the mandatory response token

On success, return HTTP 200 with the POS order id and the queue number (`CreateOrderResponse`):

```json
{
  "posId": "POS-9F3A21",
  "queueNumber": "A17"
}
```

- **`posId`** — your POS's own id for the created order. LoyaltyPlant stores it and uses it on `getOrders` and the status webhook, so it must be the id you can later look the order up by.
- **`queueNumber`** — the customer-facing pickup/queue number, declared as a string (numeric values are accepted).

Once LoyaltyPlant accepts your response, it marks the order `CONFIRMED_WAITING_FOR_COOKING` and the customer is told the order is on its way to the kitchen. From there, status flows back via the webhook (see [Status Webhook Guide](06-status-webhook-guide.md)).

> **Warning:** **Your 200 response to `createOrder` MUST carry an `AuthorizationToken` response header equal to `SHA256-hex(apiKeyResponse)`.** If the header is missing or wrong, LoyaltyPlant discards your entire response and treats the order as **failed** — even though your POS already created it. This is the single most common cause of a "my order was lost" report: the kitchen has the ticket, but LoyaltyPlant never saw a valid response, so the customer's order shows as failed. Set this header on the success response, computed from `apiKeyResponse` (not `apiKey`). The header rules are detailed in [General Requirements](02-general-requirements.md).
>
> **Note:** On acceptance, the create response is the **bare** `{ "posId": ..., "queueNumber": ... }` object — no `order` wrapper and no `requestId`. (LoyaltyPlant's deserializer also tolerates an `order`-wrapped form with an optional `requestId`, but the bare object is the contract.) Return the bare object shown here; for the rejection shape see [§3.7](#37-rejecting-an-order-at-injection-structured-failure).
### 3.7 Rejecting an order at injection (structured failure)

If your POS **cannot accept** the order, return a structured rejection instead of a `posId`: HTTP 200, **still carrying the response `AuthorizationToken` header**, with a `state` block whose `status` is `FAILED`, plus a free-text `reason` and an optional structured `posErrorType`:

```json
{
  "state": {
    "status": "FAILED",
    "reason": "Item 42 is out of stock",
    "posErrorType": "ITEM_UNAVAILABLE"
  }
}
```

LoyaltyPlant then marks the order `FAILED`, records the `posErrorType` against it (surfaced in LP CRM and reports — see [Message & Error Codes §2.3](07-message-and-error-codes.md#23-pos-error-reasons-poserrortype)), and does **not** confirm the customer. `posErrorType` is **optional** but recommended; choose the closest value from the curated list, and if none fits send `UNKNOWN` with a descriptive `reason`. Omit `posErrorType` entirely and LoyaltyPlant classifies the failure itself.

> **Warning:** A rejection is still a response — it **must** carry the response `AuthorizationToken` header (`SHA256-hex(apiKeyResponse)`). Without it, the rejection is indistinguishable from a transport failure and is discarded (the token rule in §3.6 applies to rejections too, not only to acceptances).

## 4. Step 3 — `POST /rest/orders`

[`getOrders`](/digital-ordering/api/) — `POST {vendorBaseUrl}/rest/orders` — is how LoyaltyPlant **pulls** the current state of orders it already created. It is a read, expressed as a `POST` because the id list travels in the body. The request is a list of `posId` values under `orderPosIds`; the response is one `Order` per id, with the current `state` filled in.

Request (`GetOrdersRequest`):

```json
{
  "orderPosIds": ["POS-9F3A21", "POS-9F3A22"]
}
```

For each requested `posId`, return its `Order` with at least `id` (echo the `posId`), `queueNumber`, and `state.status`; on a read, those plus the line items are the meaningful fields. The full response is the `GetOrdersResponseExample` / `GetOrdersCanceledExample` on `getOrders` in the spec.

> **Note:** `Order.id` means different things by direction. On the **`getOrders` response** (this endpoint), `Order.id` is **your `posId`** — echo back the same `posId` LoyaltyPlant sent in `orderPosIds`, because LoyaltyPlant keys the poll match on it (it reads `order.getId()` and matches each returned order to the `posId` it requested). This is the opposite of the **`createOrder` request**, where `order.id` is **LoyaltyPlant's** order id. So: on the create request, `order.id` = LoyaltyPlant's id; on the `getOrders` response (and the webhook), `id` = your `posId`. (The `getOrders` response is the bare `{ "orders": [...] }` object — each entry an `Order` with `id`, `queueNumber`, and `state`. A server-generated `requestId` is optional and not required; the live integrations observed omit it.)

### 4.1 The status enum

`state.status` is one of **nine** `OrderStatus` values — the same set used by the status webhook:

| Status | Meaning |
|---|---|
| `PROCESSING` | Order received at the POS. |
| `CONFIRMED_WAITING_FOR_COOKING` | Accepted and sent to the kitchen. |
| `COOKING` | Being prepared. |
| `AWAITING_PICKUP` | Ready, awaiting customer pickup. |
| `AWAITING_DELIVERY` | Ready, awaiting a courier. |
| `BEING_DELIVERED` | Handed to the courier / out for delivery. |
| `PROCESSED` | Successfully closed at the POS (terminal success). |
| `CANCELED` | Canceled at the POS (terminal; set `reason`). |
| `FAILED` | Error creating/processing the order (terminal; set `reason`). |

When the status is `CANCELED` or `FAILED`, also set `state.reason` — a free-text explanation. It is **internal information not shown to the customer**, but it helps LoyaltyPlant and you diagnose the case. The `reason` is required on those two terminal statuses. You may additionally set the structured `state.posErrorType` (the same curated enum used on a `createOrder` rejection — see [Message & Error Codes §2.3](07-message-and-error-codes.md#23-pos-error-reasons-poserrortype)); LoyaltyPlant records it against the order alongside the free-text `reason`.

> **Note:** Your `/orders` response **must** also carry the response `AuthorizationToken` header (`SHA256-hex(apiKeyResponse)`) — the same rule as `createOrder` (§3.6). Without it, LoyaltyPlant discards the read and learns nothing about the orders' state.

## 5. Message codes in responses

The success contract for these three endpoints is HTTP 200. There is no per-operation business-error *envelope* on `getHealthcheck` / `createOrder` / `getOrders` — LoyaltyPlant treats a missing/unparseable body, a `DOWN` healthcheck, or a missing/wrong response token as a transport-level failure and does not create or update the order (see §7–§8). The one structured business outcome carried on these endpoints is a `createOrder` **rejection** — a token-valid `200` whose body is `state.status = FAILED` with an optional `posErrorType` (see [§3.7](#37-rejecting-an-order-at-injection-structured-failure)) — and the matching `posErrorType` on a `CANCELED` / `FAILED` `getOrders` read. The one place the Digital Ordering API defines explicit error **codes** is the status webhook your POS sends to LoyaltyPlant (`READ_ERROR` / `INVALID_JSON` / `VALIDATION_ERROR` / `INTERNAL_ERROR`). Those codes, their HTTP statuses, and how to react to each are catalogued in [Message and Error Codes](07-message-and-error-codes.md).

## 6. Test transcripts

You can exercise both order endpoints against a mock LoyaltyPlant client with `curl`, simulating what LoyaltyPlant sends to your service. Substitute your real `{vendorBaseUrl}`, `SalesOutletId`, and tokens from onboarding.

> **Note:** The base URL, `SalesOutletId`, and the request/response API keys are provided by LoyaltyPlant at onboarding. Substitute your real values for the placeholders below.

```bash
VENDOR_BASE_URL="{vendorBaseUrl}"
SALES_OUTLET_ID=1234
API_KEY="<your apiKey>"

# LoyaltyPlant computes the request token as SHA256-hex(apiKey)
REQ_TOKEN=$(printf '%s' "$API_KEY" | shasum -a 256 | cut -d' ' -f1)

# --- Step 1: healthcheck (runs before every order) ---
curl -s "$VENDOR_BASE_URL/rest/healthcheck" \
  -H "SalesOutletId: $SALES_OUTLET_ID" \
  -H "AuthorizationToken: $REQ_TOKEN"
# expect: {"POS":"UP","DB":"UP","KITCHEN":"UP"}  (any DOWN blocks the order)
```

Pickup order (cash) — the simplest happy path:

```bash
curl -s -X POST "$VENDOR_BASE_URL/rest/order" \
  -H "Content-Type: application/json" \
  -H "SalesOutletId: $SALES_OUTLET_ID" \
  -H "AuthorizationToken: $REQ_TOKEN" \
  -d '{
    "order": {
      "id": "LP-ORD-1001",
      "type": "PICKUP",
      "client": { "name": "Maria", "lastName": "Lopez", "email": "maria@example.com", "phoneNumber": "+34911223344" },
      "deliveryAt": { "type": "ASAP" },
      "payment": { "method": { "type": "CASH" }, "charge": { "subtotal": 6.90, "tax": 0.69, "total": 7.59 } },
      "additionalInfo": { "loyaltyPlantOrderId": 5550001 },
      "items": [ { "id": "1", "price": 6.90, "count": 1, "modifiers": [ { "id": "9001", "price": 0.00, "count": 1 } ] } ]
    }
  }'
# Your service must respond 200 { "posId": "POS-9F3A21", "queueNumber": "A17" }
# AND set response header: AuthorizationToken: SHA256-hex(apiKeyResponse)
```

Delivery order (card-in-app, with a reward present) — note the required `address` block and the zero-price `presents[]`:

```bash
curl -s -X POST "$VENDOR_BASE_URL/rest/order" \
  -H "Content-Type: application/json" \
  -H "SalesOutletId: $SALES_OUTLET_ID" \
  -H "AuthorizationToken: $REQ_TOKEN" \
  -d '{
    "order": {
      "id": "LP-ORD-1002",
      "type": "DELIVERY",
      "client": { "name": "John", "lastName": "Smith", "email": "john@example.com", "phoneNumber": "+14155550123" },
      "deliveryAt": { "type": "CUSTOM", "datetime": "2026-06-13T19:30:00Z" },
      "address": { "coordinates": { "latitude": 40.41678, "longitude": -3.70379 }, "city": "Madrid", "street": "Gran Via", "house": "28", "flat": "4B", "addressString1": "Gran Via 28, 4B", "zipCode": "28013", "country": "ES" },
      "payment": { "method": { "type": "BANK_CARD_IN_APP", "cardInfo": { "cardHolderName": "JOHN SMITH", "pan": "411111******1111" } }, "charge": { "subtotal": 13.80, "deliveryFee": 2.50, "tax": 1.38, "tips": 2.00, "total": 19.68 } },
      "additionalInfo": { "loyaltyPlantOrderId": 5550002 },
      "presents": [ { "id": "42", "price": 0.00, "count": 1 } ],
      "items": [ { "id": "1", "price": 6.90, "count": 2 } ]
    }
  }'
```

Eat-in order (card-in-store, at a table) — dine-in uses `EAT_IN` plus `additionalInfo.tableNumber`:

```bash
curl -s -X POST "$VENDOR_BASE_URL/rest/order" \
  -H "Content-Type: application/json" \
  -H "SalesOutletId: $SALES_OUTLET_ID" \
  -H "AuthorizationToken: $REQ_TOKEN" \
  -d '{
    "order": {
      "id": "LP-ORD-1003",
      "type": "EAT_IN",
      "client": { "name": "Aisha", "email": "aisha@example.com", "phoneNumber": "+971501234567" },
      "deliveryAt": { "type": "ASAP" },
      "payment": { "method": { "type": "BANK_CARD_IN_STORE" }, "charge": { "subtotal": 7.50, "tax": 0.75, "total": 8.25 } },
      "additionalInfo": { "tableNumber": 12, "loyaltyPlantOrderId": 5550003 },
      "items": [ { "id": "1", "price": 7.50, "count": 1 } ]
    }
  }'
```

Status pull — LoyaltyPlant reconciling two orders by their `posId`:

```bash
curl -s -X POST "$VENDOR_BASE_URL/rest/orders" \
  -H "Content-Type: application/json" \
  -H "SalesOutletId: $SALES_OUTLET_ID" \
  -H "AuthorizationToken: $REQ_TOKEN" \
  -d '{ "orderPosIds": ["POS-9F3A21", "POS-9F3A22"] }'
# Your service must respond 200 with one order per id, each carrying state.status,
# AND set response header: AuthorizationToken: SHA256-hex(apiKeyResponse)
```

These payloads mirror the `pickup` / `delivery` / `eatIn` request examples and the `byIds` request example on the `createOrder` and `getOrders` operations in the spec — keep your fixtures in sync with the spec, not with this guide.

## 7. Common mistakes

- ❌ **Creating the order but not setting the response `AuthorizationToken` header** (or computing it from `apiKey` instead of `apiKeyResponse`). LoyaltyPlant discards your 200, the order shows as failed, and the kitchen ticket is orphaned. ✅ Always set `AuthorizationToken: SHA256-hex(apiKeyResponse)` on the `/order` and `/orders` responses.
- ❌ **Reporting a hardcoded `"UP"` healthcheck** while a backing service is actually down — LoyaltyPlant injects orders you cannot fulfil. ✅ Wire each service status to a real check.
- ❌ **Charging for `presents[]`** — a reward item billed at full price double-charges the customer. ✅ Add presents at price 0.
- ❌ **Dropping the `address` block on a `DELIVERY` order** — the courier has no destination. ✅ For `DELIVERY`, require `coordinates` + `addressString1`.
- ❌ **Returning a `posId` you cannot later look up.** LoyaltyPlant uses that exact `posId` on `getOrders` and the webhook; a throwaway value breaks reconciliation and status. ✅ Return your durable POS order id.
- ❌ **Omitting `state.reason` on `CANCELED`/`FAILED`** in a `getOrders` response — LoyaltyPlant and support cannot diagnose the failure. ✅ Always include a `reason` on those terminal statuses.
- ❌ **Sending an `OrderStatus` value outside the nine** (typo, custom state) on a read or webhook — LoyaltyPlant cannot parse it. ✅ Map your internal states onto exactly the nine `OrderStatus` values.

## 8. Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| **"My order was lost"** — the kitchen has the ticket but the customer's order shows as failed | Your 200 response to `createOrder` was missing or had a wrong `AuthorizationToken` header, so LoyaltyPlant discarded it. | Set `AuthorizationToken: SHA256-hex(apiKeyResponse)` on the success response; verify the key is `apiKeyResponse`, not `apiKey`. |
| LoyaltyPlant never sends `createOrder` | Your `getHealthcheck` returned a `DOWN` value, no body, or an unparseable body — any of these blocks the order. | Return HTTP 200 with a valid flat `{ "service": "UP" }` map and all services `UP` when you can actually take orders. |
| `createOrder` requests are rejected before reaching your logic | The request `AuthorizationToken` (which LoyaltyPlant sends as `SHA256-hex(apiKey)`) failed your validation — wrong key on your side. | Validate against `SHA256-hex(apiKey)`; confirm the `apiKey` value with LoyaltyPlant. |
| Order created, but `BANK_CARD_IN_APP` total mismatches your line totals | The app applied a discount or tip; trust `charge.total` rather than recomputing. | Build the POS check to `charge.total`; record `BANK_CARD_IN_APP` as prepaid. |
| `getOrders` returns orders but LoyaltyPlant ignores them | The `/orders` response was missing the response `AuthorizationToken` header, so LoyaltyPlant discarded the read. | Set the response token on `/orders` exactly as on `/order`. |
| Order times out on LoyaltyPlant's side during creation | Your POS-creation work exceeded the 60 s read timeout. | Acknowledge fast and do slow work asynchronously; respond within the read budget. |

> **Note:** LoyaltyPlant retries a failed injection internally — up to **3 attempts** (the initial call plus 2 retries) on its `PosUnavailableException`, **500 ms** apart — so a transient `DOWN` or a dropped response may be re-attempted, but a persistently wrong response token will fail every retry. Treat the response-token rule as non-negotiable.

---

**Next:** [Status Webhook Guide →](06-status-webhook-guide.md)
