Error Handling & Edge Cases
Robust error handling is essential for a reliable integration. This chapter covers the HTTP status codes you will encounter, the error response formats, and guidance for handling edge cases.
HTTP Status Code Reference
| Status | Meaning | Typical Causes |
|---|---|---|
| 200 | Success | Request processed successfully |
| 400 | Bad Request | Invalid state name, malformed payload, missing required fields |
| 401 | Unauthorized | Missing, invalid, or expired API key |
| 404 | Not Found | Resource (order, command) does not exist |
| 409 | Conflict | Optimistic locking conflict (concurrent modification) |
| 429 | Too Many Requests | Rate limit exceeded (not currently implemented, but plan for it) |
| 500 | Internal Server Error | Unexpected server error |
| 503 | Service Unavailable | Temporary server unavailability |
Error Response Shapes
The API uses two different error response formats.
ProblemDetail (RFC 7807)
Most errors from the GlobalExceptionHandler use the standard ProblemDetail format:
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "Invalid order state name",
"instance": "/orders/12345/statusupdate"
}The type field is typically "about:blank". The title is a short classification. The detail contains a human-readable explanation. The instance is the request URI that caused the error.
Common ProblemDetail responses:
| Scenario | Status | Title | Detail |
|---|---|---|---|
| Invalid state name | 400 | Illegal argument | No enum constant ... |
| Missing required field | 400 | Illegal argument | Location name is required. |
| Invalid order ID (order does not exist) | 400 | Illegal argument | Order does not exist |
| Validation error | 400 | Validation error | (validation message) |
| Database constraint violation | 400 | Constraint violation | Generic constraint message |
| License error | 401 | License error | (license-related message) |
| Database operation error | 400 | Something went wrong during database operation. | (error details) |
| Internal server error | 500 | Internal server error | {uuid}: {message} |
Each 500 response includes a UUID for tracing. Log this UUID when troubleshooting.
Legacy 401 Responses
Authentication errors return two possible shapes:
From AuthorizationInterceptor (missing/invalid key):
HTTP/1.1 401 Unauthorized
Content-Type: text/plain
{"error" : "Session not valid."}Note: This response has Content-Type: text/plain, not application/json, even though the body is JSON. The response includes a random delay of 500-1499ms.
From UnAuthorizedException handler (expired session):
{
"message": "Unauthorized",
"uid": null
}Both indicate the same thing: the API key is not valid. Re-authenticate or obtain a new key.
Edge Cases
SSE Reconnection with Offset Parameters
When the SSE connection drops:
- Detect the drop via
onerrorcallback (EventSource) or connection timeout. - Implement exponential backoff: 1s, 2s, 4s, 8s, capped at 60s.
- On reconnection, pass the last processed order number and command ID:
/notifications/service/stream?orderNumber=12345&commandNumber=42 - Poll after reconnecting. Check an existing key before replacing it; a stream closure alone is not evidence of an invalid credential.
Use the connection-check and reconnection procedure.
Network Partitioning
Do not wait for the server's configured emitter timeout to detect a broken connection. Monitor event arrival and enforce a local idle/read threshold. Missing keepalives do not guarantee an immediate transport error.
When the server is reachable, use bounded fallback polling and reconnect SSE with backoff. When it is unreachable, back off both operations. After recovery, poll pending work and reconcile durable processing records before resubmitting anything to the register.
Duplicate Detection
Use separate durable processing records for orders and commands, scoped to the installation and location. Their number fields refer to different identifier sequences. An in-memory set cannot protect against duplicates after a restart.
Coordinate workers before external submission. Record the external result and acknowledgement outcome separately. If the result is unknown, reconcile it with the register before retrying. Choose retention based on your recovery requirements; an arbitrary one-hour expiry is not a guarantee against duplicates.
Partial Data — Nullable Fields
Fields without a value are omitted from the response — the API does not serialize null. Your client must not assume that a field is always present:
tableNumber— May be absent for takeout orders. Treat as "no table assigned."userNumber— May be absent if the order was created without a user session.state— Always present. Commands always reportPLACED.rows— Only present on orders. Commands have no rows.guestCount— Present on orders (currently always1).readTableState— Present (true) only on read-table-state commands.paymentNumber— Present only on request-bill commands (always1).exportMainData— Present (true) only on export-data / fetch-remote-menu commands.
The DTO also defines togo, moveTableTo, receipt, paymentDiscount, and customer, but the current backend never populates them, so they never appear in responses.
Safe access pattern:
// Use optional chaining and nullish coalescing
const table = order.tableNumber ?? "unknown";
const state = order.state ?? "PLACED";
const rows = order.rows ?? [];
const paymentDiscounts = order.paymentDiscount ?? [];Order State Machine
+---> COMPLETED
|
SELECTING --> NEW --> PLACED ----> ABORTED
|
+---> FAILED ---> PLACED (retry)- SELECTING: Customer is adding items. Not visible to service clients via the location endpoint.
- NEW: Order created by a client app. Not yet placed.
- PLACED: Customer submitted the order. This is the state your service client sees when polling.
- COMPLETED: Order fulfilled. Set by your service client via status update.
- ABORTED: Order cancelled. Set by your client or when a session closes.
- FAILED: Processing error. Below the configured failure threshold, reporting failure returns the order to
PLACED; at the threshold, operator recovery is needed. See failure handling.
When you update an order's status, the server records timestamps:
orderTime— Set when the order transitions toPLACEDcompleteTime— Set when the order transitions toCOMPLETEDorABORTEDfailedTime— Set when the order transitions toFAILED
Concurrency — coordinate integration workers
Multiple polling clients can receive the same orders. Coordinate workers and persist processing identifiers before external submission. A second status update is not guaranteed to fail because a first client changed the state. Do not rely on this endpoint to reserve work or transparently retry conflicts. Reconcile unknown outcomes before repeating external processing.
Handling Timeouts
Distinguish connection establishment, ordinary request deadlines, and SSE idle/read monitoring. For example, use a 30-second deadline for an ordinary request and investigate a 60-second gap without stream events. These are client configuration suggestions, not server response-time guarantees.
After a timed-out write, reconcile the outcome before repeating it. See Security & Best Practices for retry guidance.
Curl: Simulating Error Scenarios
Invalid API Key
curl -i -H "api_key: invalid-key" "https://{host}/orders/location"Non-existent Order
curl -i -X PUT \
-H "api_key: abc123-def456-ghi789" \
-H "Content-Type: application/json" \
-d '{"newState": "COMPLETED"}' \
"https://{host}/orders/99999/statusupdate"Invalid JSON Payload
curl -i -X PUT \
-H "api_key: abc123-def456-ghi789" \
-H "Content-Type: application/json" \
-d 'not-json' \
"https://{host}/orders/12345/statusupdate"Next Steps
- Security & Best Practices — Secure your integration and follow best practices