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 usable200from you: a connection failure, a timeout, an unparseable body, or a missing/wrong responseAuthorizationToken(theSHA256-hex(apiKeyResponse)echo required on/order,/orders,/menu,/menu/revision— see General Requirements). The healthcheck is the gate: any service reportingDOWN, or no answer at all, means LoyaltyPlant does not inject the order. LoyaltyPlant retriescreateOrder(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 200 —
accepted: true. The update was processed. - HTTP 400 —
accepted: false, oneerrors[]entry with codeREAD_ERROR,INVALID_JSON, orVALIDATION_ERROR. A request-shape or auth problem. - HTTP 500 —
accepted: false, oneerrors[]entry with codeINTERNAL_ERROR. An unexpected processing failure on LoyaltyPlant's side.
- HTTP 200 —
Warning: A webhook
accepted: trueconfirms 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 still200accepted: true. Two per-order skips behave identically: an entry whoseidis not a knownposId, and an entry whosestate.statusis not one of the nineOrderStatusvalues (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.
| Status | Terminal? | Meaning |
|---|---|---|
PROCESSING | No | Order received at the POS. |
CONFIRMED_WAITING_FOR_COOKING | No | Accepted and sent to the kitchen. |
COOKING | No | Being prepared. |
AWAITING_PICKUP | No | Ready, awaiting customer pickup. |
AWAITING_DELIVERY | No | Ready, awaiting a courier. |
BEING_DELIVERED | No | Handed to the courier / out for delivery. |
PROCESSED | Yes (success) | Successfully closed at the POS. |
CANCELED | Yes | Canceled at the POS. Set state.reason. |
FAILED | Yes | Error creating/processing the order. Set state.reason. |
Note:
state.reasonis required when the status isCANCELEDorFAILED(it is internal diagnostic text, never shown to the customer). It is consumed on thegetOrdersread; the webhook payload acts onid/queueNumber/state.statusand otherwise ignores free-textreason— the one exception being a structuredstate.posErrorTypeon aCANCELED/FAILEDpush, which is consumed and recorded (see §2.3). Astate.statusstring 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 still200accepted: true, exactly like the unknown-posIdcase in the §1 Warning.state.statusis 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 theINTERNAL_ERRORpath). Detect a silently skipped status via thegetOrderspoll 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.
code | HTTP | Meaning |
|---|---|---|
READ_ERROR | 400 | The request body could not be read off the socket. |
INVALID_JSON | 400 | The body was malformed or incomplete JSON. |
VALIDATION_ERROR | 400 | Header/auth validation failed: missing AuthorizationToken, missing SalesOutletId, non-numeric SalesOutletId, unknown outlet, or a wrong SHA256-hex(apiKey) token. |
INTERNAL_ERROR | 500 | An 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_REQUESTis 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 seeINVALID_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 }; theerrorsfield isnull/absent rather than an empty array. Branch onacceptedand the HTTP status, not on the presence oferrors.
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
createOrderresponse returnsstate: { status: FAILED, reason, posErrorType }instead of aposId(see Order Injection Guide §3.7). - Later — when an already-injected order is cancelled or fails, you set
state.posErrorTypeon theCANCELED/FAILEDstatus you report on the webhook and ongetOrders(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.
posErrorType | Meaning |
|---|---|
ITEM_UNAVAILABLE | An ordered item is unknown to the POS (wrong/unmapped product id) or out of stock. |
ITEM_MODIFIER_INVALID | A modifier sent for an item is not valid for it. |
ITEM_MODIFIER_MISSING | A required modifier group is not satisfied (below its minimum / a missing mandatory modifier). |
ITEM_MODIFIER_EXCESS | More modifiers sent for a group than it allows. |
ITEM_SIZE_INVALID | An item's size / variant is invalid or unknown to the POS. |
ITEM_PRICE_MISMATCH | An item's price disagrees between LoyaltyPlant and the POS. |
DISCOUNT_INVALID | A discount or coupon could not be applied by the POS. |
DISCOUNT_MISMATCH | The POS applied the discount but its computed amount disagrees with LoyaltyPlant's. |
DELIVERY_ADDRESS_INVALID | Delivery address is malformed / invalid / too long for the POS. |
DELIVERY_ADDRESS_UNAVAILABLE | The address is not within any configured POS delivery zone. |
DELIVERY_AREA_UNAVAILABLE | No delivery zone is configured for the establishment at all. |
TIME_UNAVAILABLE | Requested pickup/delivery time is invalid or cannot be fulfilled. |
BAD_REQUEST | The order was rejected as malformed/invalid (generic) with no more specific cause. |
UNKNOWN | A 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
UNKNOWNwith a descriptive free-textreason— do not invent your own literal. An unrecognized value is treated asUNKNOWN(the order still fails on thestatusyou sent).posErrorTypeis 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.
| Situation | What LoyaltyPlant does | What you should do |
|---|---|---|
Healthcheck reports any service DOWN, or does not answer | Retries 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 posId | Skips 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: true — no 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 / FAILED | Surfaces 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 alreadyPROCESSED. 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 →