Skip to content

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.

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.

  1. You register your redirect URIs on the app in the developer console.
  2. The merchant clicks Connect LithosPOS in your merchant panel. You redirect the browser to https://my.lithospos.com/oauth/authorize.
  3. 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.
  4. 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.

ScopeGrants
stores.readList the merchant’s stores, so you can match them to your outlets
menus.readMenus and their item selection, for the guest’s menu screen
items.readItem names, prices, variants and descriptions
categories.readCategory structure for the menu’s navigation
modifiers.readModifier groups and options
tables.readList a store’s tables and read a table’s open bill
online_orders.readRead table orders and their status
online_orders.writePlace table orders, and cancel them before the store accepts
payments.writeRecord the payments guests make at the table
customers.readLook up an existing guest
customers.writeCreate 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.

The table routes are specific to this product. Menus, stores and customers are the shared reads every ordering product uses.

MethodPathScopes (all required)
GET/v1/tables?storeId=tables.read
GET/v1/tables/:tableId/billtables.read
POST/v1/table-ordersonline_orders.write, tables.read
GET/v1/table-orders/:orderIdonline_orders.read, tables.read
GET/v1/table-orders/:orderId/statusonline_orders.read, tables.read
POST/v1/table-orders/:orderId/paymentspayments.write
POST/v1/table-orders/:orderId/cancelonline_orders.write, tables.read
GET/v1/storesstores.read
GET/v1/menus, /v1/menus/:id/fullmenus.read
GET/v1/customerscustomers.read
POST/v1/customerscustomers.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.

  1. Connect the merchant: the OAuth consent above, once per merchant.
  2. Map outlets and tables: GET /v1/stores, then GET /v1/tables?storeId=…. Print one QR code per tableId.
  3. Show the menu: GET /v1/menus?storeId=…, then /v1/menus/:id/full.
  4. Place the order: POST /v1/table-orders with your own externalOrderId. The store’s POS and kitchen see it on that table straight away.
  5. Follow it: GET /v1/table-orders/:orderId/status as the store accepts it and the kitchen marks it ready.
  6. Take the payment: GET /v1/tables/:tableId/bill for what is outstanding, then POST /v1/table-orders/:orderId/payments when the guest pays.
Terminal window
# 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 41
curl -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 app
curl -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"
}'
201 Created (step 2)
{
"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
}
}
  • 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, taxAmount and totalAmount. Show the guest those numbers, not your own sum.
  • Creates and payments are retry-safe. externalOrderId is your idempotency key for orders and paymentRef is the key for payments. Repeating either returns 200 with 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. status moves when the store works the order. A paid order stays preparing or ready until the till settles the bill, and then becomes completed.
  • The bill is your bill. GET /v1/tables/:tableId/bill covers 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). currency is 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.

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 tableId stays 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 return 404 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_configured on every order until it is switched back on.

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 / ownerPassword from provisioning and draw a floor plan with a few tables. Until you do, GET /v1/tables returns "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 externalOrderId and a paymentRef, pay more than is outstanding, send the wrong currency, and pay a cancelled order. Each has its own error code.

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.

  1. 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.

  2. When the app is APPROVED, LithosPOS issues its production credential: a lp_app_pk… client id and a secret shown once. Under Apps → your app → OAuth, register up to five exact-match https:// redirect URIs for your Connect LithosPOS button.

    • Your Connect LithosPOS button redirects the merchant to https://my.lithospos.com/oauth/authorize with your client_id, redirect_uri and a state value.
    • The merchant’s account owner signs in and approves your app on the consent screen.
    • LithosPOS redirects back to your URI with code, state and merchant_id.
    • Your backend exchanges the code at POST /v1/oauth/token within 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.

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.