Table QR Payment overview
Table QR Payment is for software that puts a QR code on a restaurant table and lets the guest order and pay from their own phone: pay-at-table apps such as Noon, QR ordering and self-checkout apps, hotel poolside and in-room ordering.
The guest scans the code, picks from the store’s menu and sends the order. It lands on the store’s point of sale on that table, priced by the store, and the kitchen works it like any other dine-in order. When the guest pays in your app, you record the payment against the order. The POS then sees the order as prepaid and closes it when the bill is settled.
A Table QR Payment app cannot touch the catalogue, stock, purchasing or reports. It sees the stores, their tables and menus, the orders it placed and the payments it recorded, which is why merchants agree to connect one quickly.
How merchants connect
Section titled “How merchants connect”Linking a merchant uses the standard OAuth 2.0 redirect flow, the same one the Online Order and Reservation APIs use. There is no API key and no access request.
- You register your redirect URIs on the app in the developer console.
- The merchant clicks Connect LithosPOS in your merchant panel. You redirect the browser
to
https://my.lithospos.com/oauth/authorize. - The merchant’s account owner signs in to their LithosPOS back office and approves your app on the consent screen, which lists the scopes below in plain language.
- LithosPOS redirects back to your URI with a code. Your backend exchanges it for an access token and a refresh token, and keeps refreshing server to server from then on.
One link covers the whole merchant company. A multi-store merchant links once, and you send
storeId on every call. Match your outlets to stores with GET /v1/stores. The merchant can
disconnect you at any time from Settings → Connected apps. After that, your next refresh
fails with invalid_grant.
Scope bundle
Section titled “Scope bundle”| Scope | Grants |
|---|---|
stores.read | List the merchant’s stores, so you can match them to your outlets |
menus.read | Menus and their item selection, for the guest’s menu screen |
items.read | Item names, prices, variants and descriptions |
categories.read | Category structure for the menu’s navigation |
modifiers.read | Modifier groups and options |
tables.read | List a store’s tables and read a table’s open bill |
online_orders.read | Read table orders and their status |
online_orders.write | Place table orders, and cancel them before the store accepts |
payments.write | Record the payments guests make at the table |
customers.read | Look up an existing guest |
customers.write | Create or update the guest’s customer record |
Anything outside this list returns 403 partner.scope_denied. There is no way to widen a
bundle. An integration that also needs stock or reports needs a Full API app, whose token
carries every scope above as well.
Endpoints
Section titled “Endpoints”The table routes are specific to this product. Menus, stores and customers are the shared reads every ordering product uses.
| Method | Path | Scopes (all required) |
|---|---|---|
GET | /v1/tables?storeId= | tables.read |
GET | /v1/tables/:tableId/bill | tables.read |
POST | /v1/table-orders | online_orders.write, tables.read |
GET | /v1/table-orders/:orderId | online_orders.read, tables.read |
GET | /v1/table-orders/:orderId/status | online_orders.read, tables.read |
POST | /v1/table-orders/:orderId/payments | payments.write |
POST | /v1/table-orders/:orderId/cancel | online_orders.write, tables.read |
GET | /v1/stores | stores.read |
GET | /v1/menus, /v1/menus/:id/full | menus.read |
GET | /v1/customers | customers.read |
POST | /v1/customers | customers.write |
Every table route checks the stores on your grant. A storeId outside it returns
403 partner.store_not_granted, and an order or table at a store outside it reads as 404.
The field-level schemas are in the API reference.
The shape of an integration
Section titled “The shape of an integration”- Connect the merchant: the OAuth consent above, once per merchant.
- Map outlets and tables:
GET /v1/stores, thenGET /v1/tables?storeId=…. Print one QR code pertableId. - Show the menu:
GET /v1/menus?storeId=…, then/v1/menus/:id/full. - Place the order:
POST /v1/table-orderswith your ownexternalOrderId. The store’s POS and kitchen see it on that table straight away. - Follow it:
GET /v1/table-orders/:orderId/statusas the store accepts it and the kitchen marks it ready. - Take the payment:
GET /v1/tables/:tableId/billfor what is outstanding, thenPOST /v1/table-orders/:orderId/paymentswhen the guest pays.
A complete first call
Section titled “A complete first call”# 1. Which tables does store 3 have?curl -s "$API_BASE_URL/tables?storeId=3" \ -H "Authorization: Bearer $ACCESS_TOKEN"
# 2. Put an order on table 41curl -s "$API_BASE_URL/table-orders" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "storeId": 3, "tableId": 41, "externalOrderId": "NOON-8F2K1", "items": [ { "itemId": 120, "variantId": 0, "qty": 2, "note": "no onion", "modifiers": [{ "modifierId": 55, "qty": 1 }] } ], "guests": 4 }'
# 3. The guest paid 182.50 plus a 10.00 tip in your appcurl -s "$API_BASE_URL/table-orders/51231/payments" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "paymentRef": "noon-txn-77412", "amount": 182.50, "tipAmount": 10.00, "currency": "AED", "paidAt": "2026-10-16T13:04:11Z", "method": "card" }'{ "data": { "orderId": 51231, "externalOrderId": "NOON-8F2K1", "storeId": 3, "tableId": 41, "tableName": "T12", "status": "new", "paymentStatus": "unpaid", "subtotal": 173.81, "taxAmount": 8.69, "totalAmount": 182.50, "currency": "AED", "duplicate": false }}Design rules worth knowing early
Section titled “Design rules worth knowing early”- The store sets the prices. You send items, variants, modifiers and quantities, never
prices. LithosPOS prices the order from the store’s menu and returns
subtotal,taxAmountandtotalAmount. Show the guest those numbers, not your own sum. - Creates and payments are retry-safe.
externalOrderIdis your idempotency key for orders andpaymentRefis the key for payments. Repeating either returns200with the original result and"duplicate": true, so a timed-out request can simply be sent again. - Payment and lifecycle are separate.
paymentStatus(unpaid,partially_paid,paid) moves when you record payments.statusmoves when the store works the order. A paid order stayspreparingorreadyuntil the till settles the bill, and then becomescompleted. - The bill is your bill.
GET /v1/tables/:tableId/billcovers the open orders your app placed on that table. Items a waiter rings up on the till are not included in this version. - Amounts are in the store’s currency. Send major units (
182.50).currencyis optional, but if you send it, it must match the store’s currency. - The store moves orders too. Staff accept, prepare and complete orders at the POS. You can cancel an order only before the store accepts it and before any payment is recorded.
- Single items only, for now. Meals and combos return
422 table_orders.combo_unsupported. Hide them from the guest’s menu. - No webhooks yet. Follow orders by polling their status. See the polling cadence.
Merchant setup
Section titled “Merchant setup”One thing in the merchant’s back office must be in place before the first order, and one is handled for them:
- Tables. Tables come from Settings → Floor plans. A table’s
tableIdstays the same when the merchant renames it, so key your QR codes on the id. Deleting a table breaks its QR code: orders for it return404 table_orders.table_not_found. - The settlement account. Payments recorded through this API settle to the merchant’s
Noon Food aggregator account (Settings → Aggregators). It is created automatically on
the first order. A merchant who later switches it off gets
409 table_orders.not_configuredon every order until it is switched back on.
Sandbox
Section titled “Sandbox”Sandbox works the same way it does for every product, with client credentials or the full consent flow against your own sandbox merchant. A few things are specific to table ordering:
- Seed something to order. Tick Items, Modifiers and Menus when you provision the merchant.
- Add the tables yourself. Sample data has no tables. Sign in to the sandbox back office
with the
ownerEmail/ownerPasswordfrom provisioning and draw a floor plan with a few tables. Until you do,GET /v1/tablesreturns"data": []. The Noon Food aggregator account is created on your first order. - No money moves. A payment you record is booked on the order exactly as it would be in production, but nothing is charged. Card processing stays on your side.
- Test the failure paths. Repeat an
externalOrderIdand apaymentRef, pay more than is outstanding, send the wrongcurrency, and pay a cancelled order. Each has its own error code.
Going to production
Section titled “Going to production”Production starts with the same app review as every product. After approval, merchants connect your app themselves through OAuth. There is no access request and no API key.
-
Submit the app for review
Section titled “Submit the app for review”In the console, open Apps → your app → Review and fill in the integration description, URLs, support email, walkthrough video and privacy policy. Select Submit for review, then contact the LithosPOS team to tell them the app is waiting. See the go-live checklist for what reviewers check.
-
Register your redirect URIs
Section titled “Register your redirect URIs”When the app is
APPROVED, LithosPOS issues its production credential: alp_app_pk…client id and a secret shown once. Under Apps → your app → OAuth, register up to five exact-matchhttps://redirect URIs for your Connect LithosPOS button. -
Connect each merchant with OAuth
Section titled “Connect each merchant with OAuth”- Your Connect LithosPOS button redirects the merchant to
https://my.lithospos.com/oauth/authorizewith yourclient_id,redirect_uriand astatevalue. - The merchant’s account owner signs in and approves your app on the consent screen.
- LithosPOS redirects back to your URI with
code,stateandmerchant_id. - Your backend exchanges the code at
POST /v1/oauth/tokenwithin five minutes. You get a 900-second access token and a refresh token. - Call the API with
Authorization: Bearer <access_token>. Refresh before it expires, and save the new refresh token each time, because it rotates on every use.
- Your Connect LithosPOS button redirects the merchant to
Adding another merchant later is another consent, not another review. The full flow, PKCE, refresh rules and error codes are in Connect merchants with OAuth.