Errors
Error responses from the Pay-at-Table API use a nested error envelope. The only exception is an invalid token, which returns an OAuth-style body (see below).
Envelope
{
"error": {
"code": 400,
"message": "Validation failed",
"details": {
"amount": ["This value should be greater than 0."]
}
}
}
| Field | Notes |
|---|---|
error.code | The HTTP status code, repeated in the body. |
error.message | Human-readable explanation suitable for surfacing in your UI. |
error.details | Per-field validation errors on 400 (map of field name to an array of messages). On 502, an object whose code is a stable failure slug (see 502 POS forwarding errors). On the 409 pending-payment case, {"code": "pos_payment_pending"}. null on every other error. |
Invalid token - 401
A request with an Authorization header whose token is invalid or revoked does not use the envelope above. It returns an OAuth-style body:
{
"error": "access_denied",
"error_description": "The access token provided is invalid."
}
A request with no Authorization header (or a malformed one) returns 403 with the standard envelope, not 401:
{
"error": {
"code": 403,
"message": "Access Denied. You do not have permission to perform this action.",
"details": null
}
}
Status codes
| HTTP | When |
|---|---|
400 | Request body fails validation (missing/invalid field). error.details maps each field to its problems; error.message is "Validation failed". |
401 | Authorization header present but the token is invalid or revoked. OAuth-style body (see above). |
403 | Missing/malformed Authorization header, token lacks partner access, or venue is not in the token's scope. |
404 | Bill endpoint: table unknown for the venue, or no open order on the table. Payment endpoint: order not found in venue. Payment status endpoint: order not found in venue, or no payment with that externalReference on the order. Features endpoint: venue has no active POS connection. The message states which case applies. |
409 | Payment endpoint, two distinct causes distinguishable by message: (a) same externalReference reused on the same order with a different payload - message lists the mismatching fields; (b) a new externalReference submitted while another payment on the same order is still pending - message: Another payment ("<externalReference>") on order "<orderId>" is awaiting POS confirmation. Poll it to a terminal state before submitting a new payment. Resolve (b) by polling the in-flight payment via the payment status endpoint until terminal, then resubmitting. |
422 | Order is closed or voided when a payment is submitted. |
502 | POS forwarding failed (payment and bill endpoints; on the bill endpoint only for pos_rate_limited). See below. |
5xx | Unexpected server error. message is "Internal Server Error". |
502 POS forwarding errors
When the payment endpoint returns 502, Mozrest forwarded the payment to the underlying POS and the POS refused, errored, or was unreachable. The bill endpoint returns 502 only with pos_rate_limited, when the POS answers HTTP 429 on the bill read; every other bill-read failure keeps its existing status. The response body is:
{
"error": {
"code": 502,
"message": "The bill is currently open on the POS interface. Please ask the waiter to close any open ticket on this table and retry.",
"details": {
"code": "pos_check_locked"
}
}
}
error.message is controlled by Mozrest (no raw POS body leaks) and is safe to surface to the guest or operator. error.details.code carries a stable, machine-readable failure slug (table below) that you can branch on. Use message for display. When a POS failure has no specific mapping, the slug is pos_provider_rejected.
The cases behind a 502, and what to do:
error.details.code | Meaning | Retry with the same externalReference? | Recommended partner action |
|---|---|---|---|
pos_check_locked | The bill is currently open in the POS (a waiter or terminal has it on screen). The POS rejects payment writes while the check is "in use". | Yes | Surface message to the guest. Retry after the waiter releases the check. |
pos_check_already_closed | The bill is already closed in the POS. No further payments can be applied. | No, terminal | Stop retrying. The bill is paid (or voided) on the POS side. |
pos_check_not_found | The bill could not be found in the POS (deleted, moved, or not yet synced). | No, terminal | Re-fetch via /bill. If the order disappears, the table is no longer open. |
pos_idempotency_conflict | A different payment payload was already recorded against the POS with this externalReference. | No, terminal | Use a fresh externalReference. Do not retry the same one with a different body. |
pos_auth_failed | Mozrest could not authenticate with the POS. | No, terminal | Do not retry - contact Mozrest support. |
pos_config_missing | Pay-at-Table is not configured for this venue's POS. | No, terminal | Do not retry - contact Mozrest support. |
pos_order_reference_missing | The order is missing its POS-side reference (for example, the order is not yet placed on the POS). | No, terminal | Do not retry - contact Mozrest support. |
pos_transport_error | Mozrest could not reach the POS (network / timeout). | Yes | Retry shortly with the same externalReference. |
pos_provider_unavailable | The POS returned a 5xx. | Yes | Retry shortly with the same externalReference. |
pos_rate_limited | The POS answered HTTP 429: it is rate limiting requests. message: The POS is rate limiting requests. Please retry shortly. | Yes | Retry shortly with the same externalReference. |
pos_provider_rejected | The POS refused for a reason Mozrest does not yet map. Also the default slug for any unmapped forwarding failure. | No, terminal | Surface message. Contact Mozrest support if it recurs. |
Per-POS adapters use server-side idempotency keyed on externalReference (e.g. Simphony's header.idempotencyId = md5(externalReference)), so retrying the same request after a 502 is safe - the POS deduplicates.
Whatever the cause, a 502 on the payment endpoint means the payment was not recorded in Mozrest and the order is not closed.
On the payment endpoint, a 502 can only occur on immediate-confirmation venues (features.payAtTable.paymentConfirmation = "immediate"), where forwarding happens synchronously inside the POST. On deferred-confirmation venues, POS failures surface asynchronously - see the next section.
Asynchronous failures
On venues where features.payAtTable.paymentConfirmation = "deferred", the payment POST answers 202 Accepted with status: "pending" and the POS confirms afterwards. If the POS then rejects the payment - or never confirms within the 120-second ceiling - the failure is data, not an HTTP error: the payment flips to status: "failed" with a machine-readable failureCode, and the payment status endpoint keeps answering 200 OK with that state in the body. Do not expect a 4xx/5xx for a payment that failed after the 202.
failureCode slugs are stable and safe to branch on. Where applicable they use the same vocabulary as the 502 cases above:
failureCode | Meaning | Recommended partner action |
|---|---|---|
pos_confirmation_timeout | The POS did not confirm the payment within 120 seconds of submission. | Refund/void the charge on your side and inform the guest. Report recurring cases to Mozrest. |
pos_check_not_found | The check was closed or moved by staff while the payment was in flight. | Re-fetch /bill and restart the flow. |
pos_overpaid | The payment would exceed the outstanding balance - e.g. staff took a payment on the till concurrently. | Re-fetch /bill and submit a corrected amount with a fresh externalReference. |
pos_config_missing | Pay-at-Table is not correctly configured for this venue's POS. | Do not retry - contact Mozrest support. |
A failed payment never counts toward the bill's totals.paid and never closes the order. Full lifecycle, polling guidance, and per-state JSON examples: Payment status.