Skip to main content

Message & Error Codes

Read first: Status Webhook Guide

This is the reference catalog of the codes and statuses that travel across the Digital Ordering API contract: the HTTP statuses LoyaltyPlant uses on the webhook, the order statuses that move through both directions, the webhook error codes LoyaltyPlant returns to your POS, and the POS error reasons (PosErrorType) you report when an order fails. Field-level definitions live in the spec — the OrderStatus, WebhookErrorCode, and WebhookResponse schemas in the Digital Ordering API specification — this page adds the meaning and the recommended handling.

1. The golden rule: business outcome vs transport error

The Digital Ordering API has two failure surfaces, and they live on opposite sides of the inverted contract. Keep them separate.

  • On the five /rest/* endpoints you host, LoyaltyPlant is the client. There is no LoyaltyPlant-defined error schema for these calls — your service owns its own error surface. LoyaltyPlant treats an order/menu call as failed when it cannot get a usable 200 from you: a connection failure, a timeout, an unparseable body, or a missing/wrong response AuthorizationToken (the SHA256-hex(apiKeyResponse) echo required on /order, /orders, /menu, /menu/revision — see General Requirements). The healthcheck is the gate: any service reporting DOWN, or no answer at all, means LoyaltyPlant does not inject the order. LoyaltyPlant retries createOrder (up to 3 attempts, 500 ms apart) on this unavailability before declaring the order not-injected, so a brief healthcheck blip does not necessarily lose the order. None of these is signalled by a code in this catalog — they are transport/availability outcomes.
  • On the one webhook you POST to LoyaltyPlant, LoyaltyPlant is the server — and here it defines an explicit code contract. The HTTP status is authoritative for transport-level acceptance, and the body carries WebhookResponse { accepted, errors[] }:
    • HTTP 200accepted: true. The update was processed.
    • HTTP 400accepted: false, one errors[] entry with code READ_ERROR, INVALID_JSON, or VALIDATION_ERROR. A request-shape or auth problem.
    • HTTP 500accepted: false, one errors[] entry with code INTERNAL_ERROR. An unexpected processing failure on LoyaltyPlant's side.

Warning: A webhook accepted: true confirms LoyaltyPlant accepted the request, not that every order in it was applied. A batch (orders[] with several entries) is processed per-order, and a problem with one entry skips just that entry (logged, not an error) while the rest apply — the response is still 200 accepted: true. Two per-order skips behave identically: an entry whose id is not a known posId, and an entry whose state.status is not one of the nine OrderStatus values (a misspelled or unsupported status). Neither produces an error code; both are silently dropped with a success response. To detect a skipped order, rely on the poll path (getOrders) — see §3 and the Status Webhook Guide.

2. The codes the Digital Ordering API actually uses

2.1 Order statuses

Every order moves through the nine OrderStatus values. Your POS reports them on the webhook (state.status) and on getOrders reads; LoyaltyPlant maps them to what the customer sees in the app. Per-value meaning is on the OrderStatus enum in the spec; the table restates it for reference.

StatusTerminal?Meaning
PROCESSINGNoOrder received at the POS.
CONFIRMED_WAITING_FOR_COOKINGNoAccepted and sent to the kitchen.
COOKINGNoBeing prepared.
AWAITING_PICKUPNoReady, awaiting customer pickup.
AWAITING_DELIVERYNoReady, awaiting a courier.
BEING_DELIVEREDNoHanded to the courier / out for delivery.
PROCESSEDYes (success)Successfully closed at the POS.
CANCELEDYesCanceled at the POS. Set state.reason.
FAILEDYesError creating/processing the order. Set state.reason.

Note: state.reason is required when the status is CANCELED or FAILED (it is internal diagnostic text, never shown to the customer). It is consumed on the getOrders read; the webhook payload acts on id / queueNumber / state.status and otherwise ignores free-text reason — the one exception being a structured state.posErrorType on a CANCELED / FAILED push, which is consumed and recorded (see §2.3). A state.status string that is not one of these nine values does not fail the webhook: it is skipped for that one order — logged, not applied — and the response is still 200 accepted: true, exactly like the unknown-posId case in the §1 Warning. state.status is read as a plain string, so the body always parses; the enum coercion happens per-order and a failure to recognize the value is caught and the order skipped (it never reaches the INTERNAL_ERROR path). Detect a silently skipped status via the getOrders poll path — see §3.

2.2 Webhook error codes

When LoyaltyPlant rejects a status webhook, the errors[].code is one of four wire string literals (not a numeric catalog, and not a Java enum). Each carries a fixed HTTP status. This is the authoritative catalog; the Status Webhook Guide shows them in the context of the request/response flow.

codeHTTPMeaning
READ_ERROR400The request body could not be read off the socket.
INVALID_JSON400The body was malformed or incomplete JSON.
VALIDATION_ERROR400Header/auth validation failed: missing AuthorizationToken, missing SalesOutletId, non-numeric SalesOutletId, unknown outlet, or a wrong SHA256-hex(apiKey) token.
INTERNAL_ERROR500An unexpected error escaped while processing the update (an uncaught exception on LoyaltyPlant's side). Note: an unrecognized state.status is not one of these — it is caught and the order skipped (see §2.1), not surfaced as a 500.

The validation order behind a VALIDATION_ERROR, each step producing HTTP 400: missing AuthorizationToken → missing SalesOutletId → non-numeric SalesOutletId → no group/integrated/settings configuration for the outlet → token mismatch (Invalid AuthorizationToken for salesOutletId=X).

A rejection body — here, an auth-token failure (HTTP 400). The description is free text and varies; the code is the stable contract.

{
"accepted": false,
"errors": [
{
"code": "VALIDATION_ERROR",
"description": "Invalid AuthorizationToken for salesOutletId=4198"
}
]
}

Note: INVALID_REQUEST is not a Digital Ordering API code. It is returned only by the separate R-Keeper webhook on a different path; the Digital Ordering webhook never emits it. If you see INVALID_REQUEST, you are hitting the wrong endpoint. The Digital Ordering webhook returns only the four codes above.

Note: On a successful webhook the body is { "accepted": true }; the errors field is null/absent rather than an empty array. Branch on accepted and the HTTP status, not on the presence of errors.

2.3 POS error reasons (PosErrorType)

The codes in §2.2 describe transport problems with a webhook request. They say nothing about why an order failed at the POS. For that, the Digital Ordering API contract carries an optional, structured posErrorType on the order state, next to the free-text reason. You report it at the two moments an order can fail at your side:

  • At injection — when you cannot accept the order, your createOrder response returns state: { status: FAILED, reason, posErrorType } instead of a posId (see Order Injection Guide §3.7).
  • Later — when an already-injected order is cancelled or fails, you set state.posErrorType on the CANCELED / FAILED status you report on the webhook and on getOrders (see the Status Webhook Guide).

reason stays free text (human-readable diagnostic detail); posErrorType is the structured classification LoyaltyPlant records against the order and surfaces in LP CRM, reports, and alerting. It is internal — never shown to the customer.

The accepted values are a curated allow-list of reasons a POS can meaningfully self-report. LoyaltyPlant infers the transport / availability / lifecycle reasons (POS unreachable, rate-limited, restaurant never accepted the order, …) on its own side; you do not send those.

posErrorTypeMeaning
ITEM_UNAVAILABLEAn ordered item is unknown to the POS (wrong/unmapped product id) or out of stock.
ITEM_MODIFIER_INVALIDA modifier sent for an item is not valid for it.
ITEM_MODIFIER_MISSINGA required modifier group is not satisfied (below its minimum / a missing mandatory modifier).
ITEM_MODIFIER_EXCESSMore modifiers sent for a group than it allows.
ITEM_SIZE_INVALIDAn item's size / variant is invalid or unknown to the POS.
ITEM_PRICE_MISMATCHAn item's price disagrees between LoyaltyPlant and the POS.
DISCOUNT_INVALIDA discount or coupon could not be applied by the POS.
DISCOUNT_MISMATCHThe POS applied the discount but its computed amount disagrees with LoyaltyPlant's.
DELIVERY_ADDRESS_INVALIDDelivery address is malformed / invalid / too long for the POS.
DELIVERY_ADDRESS_UNAVAILABLEThe address is not within any configured POS delivery zone.
DELIVERY_AREA_UNAVAILABLENo delivery zone is configured for the establishment at all.
TIME_UNAVAILABLERequested pickup/delivery time is invalid or cannot be fulfilled.
BAD_REQUESTThe order was rejected as malformed/invalid (generic) with no more specific cause.
UNKNOWNA POS-stage failure you cannot classify into one of the above.

Note: The list is curated but not frozen. If your POS can report a meaningful failure reason that none of these values captures, contact LoyaltyPlant to have it added to the enum. Until it is added, send UNKNOWN with a descriptive free-text reason — do not invent your own literal. An unrecognized value is treated as UNKNOWN (the order still fails on the status you sent). posErrorType is optional: omitting it falls back to LoyaltyPlant's own classification of the failure, so sending it only ever improves the recorded reason.

3. What LoyaltyPlant does on each failure

How LoyaltyPlant reacts — and therefore what your integration should do — depends on which surface failed.

SituationWhat LoyaltyPlant doesWhat you should do
Healthcheck reports any service DOWN, or does not answerRetries createOrder up to 3 times (500 ms apart), then — if still unavailable — treats the outlet as unavailable and does not inject the order (fail-safe).Report honest UP/DOWN. The customer is not given a confirmed order, so a true DOWN prevents a lost ticket rather than causing one.
createOrder fails, times out, or returns no usable 200 (including a missing/wrong response AuthorizationToken)Discards the response and treats the order as not created; the customer is not confirmed.Respond within the timeouts and echo the response AuthorizationToken (SHA256-hex(apiKeyResponse)) on createOrder (see General Requirements).
Webhook returns VALIDATION_ERROR / INVALID_JSON / READ_ERROR (400)Does not apply the update; the customer's app does not advance. The failure is deterministic.Fix, then resend — a byte-identical retry fails identically. Check the AuthorizationToken (SHA256-hex(apiKey)), the numeric SalesOutletId, and the JSON shape. Do not loop.
Webhook returns INTERNAL_ERROR (500)Does not apply the update; the error is transient on LoyaltyPlant's side.Retry with backoff. If it persists, let polling reconcile the state and report the recurring 500 to LoyaltyPlant.
Webhook batch entry has an unknown posIdSkips that entry (logs it), applies the rest, and still answers 200 accepted: true.Send the posId you returned from createOrder (not LoyaltyPlant's order id), and rely on polling to catch any skipped order.
Webhook batch entry has an unrecognized state.status (misspelled or unsupported)Skips that entry (logs it), applies the rest, and still answers 200 accepted: trueno INTERNAL_ERROR, no 500.Send state.status as one of the nine OrderStatus values exactly. A 200 is not proof the status was applied — reconcile via the getOrders poll path; do not retry-with-backoff (there is no failure to retry).
A webhook is never delivered (timeout, connection error, repeated failure)Polls your getOrders on a schedule and reconciles to the current state on the next cycle.Keep the poll path (getOrders) working so a dropped webhook degrades the customer experience instead of breaking it.
An order reaches terminal CANCELED / FAILEDSurfaces the terminal state to the customer and reverses the loyalty points and rewards tied to the order.Report the terminal status promptly and include state.reason on the getOrders read so LoyaltyPlant and you can diagnose.

Note: LoyaltyPlant's lifecycle transitions are idempotent by status: it cancels only if the order is not already CANCELED, and completes only if it is not already PROCESSED. Re-delivering a status it has already applied — whether via a retry or a push/poll overlap — is a safe no-op, so there is no de-duplication key for you to send.


Next: Integration Checklist →