Submit payment
Notify Mozrest that a guest paid. For type: "full", Mozrest closes the order once the payment is confirmed; for type: "partial", the payment is recorded against the order which stays open.
POS-side forwarding (Mozrest pushes the payment to the underlying POS so the waiter UI / kitchen reflect it) is wired through a per-POS adapter. Whether it happens for a given venue is exposed via features.payAtTable.paymentForwarding. When false, Mozrest still records the payment internally - only the push to the POS is absent.
How the POS confirms is also per-venue, exposed via features.payAtTable.paymentConfirmation:
"immediate"- the POS applies the payment synchronously and the POST answers200 OKwithstatus: "confirmed". Nothing to poll. This is today's behaviour, plus the new lifecycle fields in the response."deferred"- the POS confirms out-of-band. Mozrest holds the POST for a few seconds while the confirmation arrives, so most payments still answer a synchronous200 OKwith a terminalstatus("confirmed"or"failed"). When the POS takes longer, the POST answers202 Acceptedwithstatus: "pending"and you poll the payment status endpoint to a terminal state. Build the polling loop either way -202is the slower tail, not the common case.
All currently provisioned venues run on immediate-confirmation POSs and keep receiving 200 OK + status: "confirmed" - existing integrations are unaffected. A 202 can only appear on venues explicitly provisioned on a deferred-confirmation POS, and /features tells you in advance which venues those are.
POST /v1/pat/venue/{venueId}/order/{orderId}/payment
Authorization: Bearer <token>
Content-Type: application/json
Path parameters
| Name | Type | Notes |
|---|---|---|
venueId | string | Mozrest venue identifier (must be in token scope) |
orderId | UUID | Order identifier returned by the bill endpoint |
Request body
{
"externalReference": "blitz-txn-9f2b1a",
"amount": 92.40,
"currency": "EUR",
"paymentMethod": "card",
"tip": 5.00,
"type": "full"
}
| Field | Type | Required | Notes |
|---|---|---|---|
externalReference | string (≤ 128 chars) | Yes | Idempotency key. Unique per partner. Re-submitting the same value against the same orderId returns the payment's current state (see Idempotent replay). Also the lookup key on the payment status endpoint. |
amount | decimal (> 0) | Yes | Total charged. Tip-inclusive - amount is the full sum the guest paid (items + tax + tip - discount). Trusted; not validated against the bill total in Phase 1. |
type | enum | Yes | full or partial. full closes the order (at confirmation); partial records and leaves the order open. |
currency | string (ISO 4217) | No | Trusted; not validated against venue currency. |
paymentMethod | string | No | Free-form (card, cash, wallet, …). |
tip | decimal (≥ 0) | No | The portion of amount that's a tip. Used on the POS UI / receipts. On type=full, Mozrest forwards this as the final authoritative tip on the order. |
Response - 200 OK (full, confirmed)
{
"paymentId": "bd108973-9b22-4f8c-b7e2-ef36c65c8fa9",
"orderId": "00ceefd4-8cc8-428b-985a-207b97cc3cba",
"status": "confirmed",
"failureCode": null,
"type": "full",
"amount": 92.40,
"currency": "EUR",
"paymentMethod": "card",
"tip": 5.00,
"externalReference": "blitz-txn-9f2b1a",
"orderStatus": "closed",
"recordedAt": "2026-05-11T06:27:07+00:00",
"confirmedAt": "2026-05-11T06:27:07+00:00"
}
Response - 200 OK (partial, confirmed)
{
"paymentId": "f795d2d5-d8ba-455b-a00c-e63154a6bee5",
"orderId": "00ceefd4-8cc8-428b-985a-207b97cc3cba",
"status": "confirmed",
"failureCode": null,
"type": "partial",
"amount": 20.00,
"currency": "EUR",
"paymentMethod": "card",
"tip": 0.00,
"externalReference": "blitz-txn-9f2b1a-partial",
"orderStatus": "open",
"recordedAt": "2026-05-11T06:31:32+00:00",
"confirmedAt": "2026-05-11T06:31:32+00:00"
}
Response - 202 Accepted (pending, deferred confirmation)
Only returned on venues where features.payAtTable.paymentConfirmation = "deferred", and only when the POS confirmation did not arrive within the short in-request wait. The payment is recorded in Mozrest and dispatched to the POS; confirmation is in flight.
{
"paymentId": "3c2a5d1e-77f0-4b2a-9c41-5b8d2e0f6a17",
"orderId": "00ceefd4-8cc8-428b-985a-207b97cc3cba",
"status": "pending",
"failureCode": null,
"type": "full",
"amount": 92.40,
"currency": "EUR",
"paymentMethod": "card",
"tip": 5.00,
"externalReference": "blitz-txn-9f2b1a",
"orderStatus": "open",
"recordedAt": "2026-05-11T06:27:07+00:00",
"confirmedAt": null
}
orderStatus stays "open" on a 202 - even for type: "full"The order closes only when the POS confirms the payment. Poll the payment status endpoint: once status flips to "confirmed", a type: "full" payment carries orderStatus: "closed".
Payment lifecycle
Every payment object - from this endpoint and from the payment status endpoint - carries three lifecycle fields:
| Field | Type | Notes |
|---|---|---|
status | enum | pending, confirmed, or failed. See the table below. |
failureCode | string or null | Stable machine-readable slug explaining a failure. Set only when status is "failed"; null otherwise. Full vocabulary in Failure codes. |
confirmedAt | ISO 8601 string or null | When the payment reached "confirmed". null while pending and on failed payments. |
status | Terminal? | Meaning |
|---|---|---|
confirmed | Yes | Success. The POS applied the payment - or the venue has no payment forwarding, in which case Mozrest's own record is final. |
pending | No | Recorded in Mozrest and dispatched to the POS; confirmation in flight. Only occurs on venues where paymentConfirmation = "deferred". |
failed | Yes | The POS rejected the payment after dispatch, or confirmation timed out. failureCode says why. The payment does not count toward the bill. |
Every payment reaches a terminal state (confirmed or failed) within 120 seconds of submission. If the POS never confirms, Mozrest fails the payment with failureCode: "pos_confirmation_timeout". You will never poll forever.
The integration loop
Write this once and it works on every venue and every POS, current and future - no branching on posCode or paymentConfirmation required:
POSTthe payment.- If the response
statusis"confirmed"or"failed"→ done (show success, or handle the failure). - Otherwise (
202/"pending") → pollGET …/payment/{externalReference}every ~1 second untilstatusis terminal.
On immediate-confirmation venues step 3 simply never triggers.
Closing semantics
type: full→ order is closed in Mozrest's DB when the payment is confirmed - immediately on a200+confirmedresponse, or at POS confirmation after a202. POS-side close happens whenfeatures.paymentForwarding = true. Subsequent payments on a closed order are rejected with422.type: partial→ order staysopenwhile there's still a balance. Multiple partials accumulate against the same order. Mozrest auto-closes the order when cumulative payments cover the bill - i.e. oncetotal - prepayment - sum(amount - tip)reaches0, the response of that final partial carriesorderStatus: "closed". Tip is excluded from the running total (it's on top of the bill, not paying it down). The canonical "settle the rest" flow is therefore: fetch/bill, submittype: partialwithamount = totalDue- no need to flip the last call totype: full.
Only confirmed payments settle the bill. Pending and failed payments never count toward the bill's totals.paid and never contribute to auto-close.
currency, paymentMethod, and tip echo the request and are null in the response when you did not send them.
Idempotent replay
Re-submitting the same externalReference with the same payload against the same order returns the payment's current state - not a snapshot of the original response - with header:
X-Idempotent-Replay: true
The HTTP status code matches the current state: 202 Accepted while the payment is still pending, 200 OK once it is terminal (confirmed or failed).
orderStatus on a replay likewise reflects the order's current status - e.g. replaying an early partial after the order was later closed returns orderStatus: "closed".
Errors
| Status | When |
|---|---|
400 Bad Request | Missing/invalid field. error.details maps each field to its problems, e.g. {"error": {"code": 400, "message": "Validation failed", "details": {"amount": ["This value should be greater than 0."]}}} |
401 Unauthorized | Token invalid or revoked. OAuth-style body, not the standard envelope - see Errors |
403 Forbidden | Missing Authorization header, token does not have partner access, or venue not in scope |
404 Not Found | Order not found in venue (message: Order "{orderId}" not found for venue "{venueId}".) |
409 Conflict | Two distinct causes, distinguishable by message: (a) same externalReference reused on the same order with a different payload - message lists the mismatching fields; use a fresh reference for a genuinely new payment. (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. Poll the in-flight payment via the payment status endpoint until it is terminal, then resubmit. |
422 Unprocessable Entity | Order is already closed or voided (message: Order "{orderId}" is not open (status: closed).) |
502 Bad Gateway | Immediate-confirmation venues only: Mozrest forwarded the payment to the underlying POS synchronously and the POS refused, errored, or was unreachable. The payment is not recorded and the order is not closed in Mozrest. See Errors → 502 POS forwarding errors. Safe to retry with the same externalReference - per-POS server-side idempotency deduplicates. |
When 502 is returned, the partner-facing error.message is human-readable and suitable for surfacing in your UI. error.details is null on 502 - there is no machine-readable failure slug today.
On deferred-confirmation venues a POS rejection after the 202 never surfaces as an HTTP error: the payment flips to status: "failed" with a failureCode on the payment object. See Errors → Asynchronous failures.