Skip to main content

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 answers 200 OK with status: "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 synchronous 200 OK with a terminal status ("confirmed" or "failed"). When the POS takes longer, the POST answers 202 Accepted with status: "pending" and you poll the payment status endpoint to a terminal state. Build the polling loop either way - 202 is the slower tail, not the common case.
No breaking change for existing integrations

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

NameTypeNotes
venueIdstringMozrest venue identifier (must be in token scope)
orderIdUUIDOrder identifier returned by the bill endpoint

Request body

{
"externalReference": "blitz-txn-9f2b1a",
"amount": 92.40,
"currency": "EUR",
"paymentMethod": "card",
"tip": 5.00,
"type": "full"
}
FieldTypeRequiredNotes
externalReferencestring (≤ 128 chars)YesIdempotency 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.
amountdecimal (> 0)YesTotal 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.
typeenumYesfull or partial. full closes the order (at confirmation); partial records and leaves the order open.
currencystring (ISO 4217)NoTrusted; not validated against venue currency.
paymentMethodstringNoFree-form (card, cash, wallet, …).
tipdecimal (≥ 0)NoThe 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
}
note
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:

FieldTypeNotes
statusenumpending, confirmed, or failed. See the table below.
failureCodestring or nullStable machine-readable slug explaining a failure. Set only when status is "failed"; null otherwise. Full vocabulary in Failure codes.
confirmedAtISO 8601 string or nullWhen the payment reached "confirmed". null while pending and on failed payments.
statusTerminal?Meaning
confirmedYesSuccess. The POS applied the payment - or the venue has no payment forwarding, in which case Mozrest's own record is final.
pendingNoRecorded in Mozrest and dispatched to the POS; confirmation in flight. Only occurs on venues where paymentConfirmation = "deferred".
failedYesThe POS rejected the payment after dispatch, or confirmation timed out. failureCode says why. The payment does not count toward the bill.
Hard guarantee - terminal within 120 seconds

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:

  1. POST the payment.
  2. If the response status is "confirmed" or "failed"done (show success, or handle the failure).
  3. Otherwise (202 / "pending") → poll GET …/payment/{externalReference} every ~1 second until status is 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 a 200 + confirmed response, or at POS confirmation after a 202. POS-side close happens when features.paymentForwarding = true. Subsequent payments on a closed order are rejected with 422.
  • type: partial → order stays open while 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. once total - prepayment - sum(amount - tip) reaches 0, the response of that final partial carries orderStatus: "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, submit type: partial with amount = totalDue - no need to flip the last call to type: 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

StatusWhen
400 Bad RequestMissing/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 UnauthorizedToken invalid or revoked. OAuth-style body, not the standard envelope - see Errors
403 ForbiddenMissing Authorization header, token does not have partner access, or venue not in scope
404 Not FoundOrder not found in venue (message: Order "{orderId}" not found for venue "{venueId}".)
409 ConflictTwo 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 EntityOrder is already closed or voided (message: Order "{orderId}" is not open (status: closed).)
502 Bad GatewayImmediate-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.