Pay at Table
Pay at Table (PAT) lets hospitality staff pull a live bill onto a Teya card machine, take full or split payments (with optional tips), and sync the result back to the ePOS - all at the table, without walking to a fixed till.
This guide introduces the feature and the flow your ePOS integrates with. For the step-by-step integration, see Integrate Pay at Table below; for the full endpoint reference, see the POSLink API reference.
How it works
POSLink sits in the middle between your ePOS and the card machine - the two never talk to each other directly. From your ePOS's perspective, the integration has two parts:
- Receive requests - open a Server-Sent Events (SSE) stream to POSLink to be notified when a staff member acts at a card machine (e.g. asks for a bill or starts a payment).
- Respond over REST - call POSLink to open and close tabs, send the bill, and take payments.
POSLink relays these requests to and from the card machine and keeps a lightweight record of each tab. You only integrate the ePOS side; the card machine side is handled by Teya.
The tab lifecycle
A table can have one or multiple tabs, which correspond to an order / bill.
The flow follows a predictable shape, with the ePOS and card machine each playing their part:
- Open tab - when a table is opened on the ePOS, it registers the tab with POSLink.
- Find a table - any card machine in the store lists the open tabs and a staff member selects one.
- Show bill - the card machine requests the bill; POSLink asks the ePOS, which responds with the current total (and, optionally, itemised data or a printable receipt).
- Pay - the staff member takes payment in full or as a split. Each payment intent reaches the ePOS, which will respond by calling the same payment request APIs, just with some tab related identifiers.
- Complete - once the remaining balance reaches zero, POSLink marks the tab
COMPLETED. - Close tab - the ePOS closes the tab to remove it from the open-tables list. This is mandatory after payment.
A tab moves through these states: OPEN → PAYING → COMPLETED → CLOSED, with PAUSED available when a split is left mid-flow.
What staff can do
- Pay in full by card, with an optional tip prompt before the card screen.
- Split the bill - equally (the card machine divides the total by the number of payers) or by custom amounts, with a tip allowed on each payment.
- Take cash as part of a split, recorded for reconciliation.
- Pause and resume a split - staff can step away and return; POSLink recalculates the remaining balance from the payments already taken.
- Print or view an itemised bill before taking payment, when the ePOS provides bill data.
Before you start
- PAT is disabled on all card machines by default. ePOS app must call a configuration endpoint to enable it for all card machines in the store (can be opted out on each card machine's settings if needed).
- There is no card machine-to-table pairing - any card machine in the store can manage any open table.
- Your ePOS must already be integrated with POSLink for standard Pay at Counter payments. See Connect a card machine and Send a payment request if you haven't set that up yet.
Next steps
Ready to integrate? Integrate Pay at Table below maps each step of the flow above to the exact endpoints and events your ePOS will use, linking out to the POSLink API reference for full request and response schemas.
Integrate Pay at Table
This guide walks through Pay at Table (PAT) from the ePOS side: how to enable the feature, register tables, react to what staff do on the card machine, and reconcile payments back into your system.
If you haven't already, read the introduction above for the concepts and the high-level flow. For the full request and response schemas of every endpoint mentioned here, see the POSLink API reference.
Before you start
- Your ePOS must already be integrated with POSLink for standard Pay at Counter payments - see Connect a card machine and Send a payment request.
- All calls are authenticated the same way as the rest of POSLink (a
Bearertoken), and creating tabs and payments requires thepayment_requestsscope. - Amounts are always integers in the smallest currency unit (e.g.
5000= £50.00) paired with an ISO 4217 currency code.
The integration in one picture
PAT is event-driven. Your ePOS holds one long-lived connection open to receive what happens at the card machine, and makes ordinary REST calls to respond:
- Inbound - a single Server-Sent Events (SSE) stream per store delivers every card machine-initiated request and tab update (a staff member asking for a bill, starting a payment, pausing a split…).
- Outbound - you call POSLink over REST to open and close tabs, send the bill, take payments, and reject requests you can't honour.
You never call the card machine, and the card machine never calls you - POSLink sits in the middle and keeps a lightweight record of each tab.
Step 1 - Enable Pay at Table for the store (once)
PAT is disabled by default. Enable it once per store by setting the PAT_ENABLED configuration key. This makes the "Find a Table" entry point appear on every card machine in the store (each card machine keeps a local opt-out toggle that defaults to on).
PUT /poslink/v1/stores/{store_id}/configs/PAT_ENABLED HTTP/1.1
Authorization: bearer {{token}}
Content-Type: application/json
Host: api.teya.com
{
"value": "true"
}
**API reference - **
PUT /poslink/v1/stores/{store_id}/configs/{config_key}
Calling it again with the same value has no additional effect, so it's safe to set on startup.
Step 2 - Open the tab event stream
Open the SSE stream for the store and keep it open. Everything the card machine does arrives here.
GET /poslink/v1/tabs/events?store_id={store_id} HTTP/1.1
Authorization: bearer {{token}}
Accept: text/event-stream
Host: api.teya.com
**API reference - **
GET /poslink/v1/tabs/events
Each event frame carries an event: line (the type), an id: line (used for reconnection), and a data: line with a flat JSON object containing event_type, tab_id, merchant_reference, and event-specific fields.
See Tab events reference below for the full list of events.
Step 3 - Open a tab
When a table or order should become payable, register it with POSLink. The total isn't known yet - you provide it later when the card machine asks for the bill.
POST /poslink/v1/tabs HTTP/1.1
Authorization: bearer {{token}}
Content-Type: application/json
Idempotency-Key: {{$uuid}}
Host: api.teya.com
{
"store_id": "eac4d470-4731-45b2-af6b-c828f03c1f9d",
"tab_name": "Table 5",
"merchant_reference": "ORDER-2026-001",
"currency_code": "GBP",
"epos_instance_id": "epos-register-1"
}
{
"tab_id": "3f2b6a2e-9d3e-4a1f-8f1e-6b2e5a8c9d01",
"tab_name": "Table 5",
"status": "OPEN",
"total_amount": { "amount": 0, "currency": "GBP" },
"total_paid": 0,
"remaining": 0,
"merchant_reference": "ORDER-2026-001",
"payment_requests": [],
"created_at": "2026-04-21T10:00:00Z",
"updated_at": "2026-04-21T10:00:00Z"
}
**API reference - **
POST /poslink/v1/tabs
tab_name is what staff see in the card machine's table list. merchant_reference must be unique per store; opening a second tab with the same reference returns 409 Conflict.
Idempotency-Key
As with payment requests, send a uniqueIdempotency-Keyheader so a retried call after a network error doesn't create a duplicate tab. See Send a payment request for the full idempotency behaviour.
Step 4 - Respond to a bill request
When a staff member selects the table on a card machine, you receive a SHOW_BILL_REQUEST event:
event: SHOW_BILL_REQUEST
data: {"event_type":"SHOW_BILL_REQUEST","tab_id":"3f2b6a2e-...","merchant_reference":"ORDER-2026-001","terminal_id":"term-001"}
Respond by sending the current bill. The total amount is required; everything else is optional. Provide a print_model (a structured receipt template) if you want the card machine to display and print an itemised bill - otherwise only the total is shown.
POST /poslink/v1/tabs/{tab_id}/bill HTTP/1.1
Authorization: bearer {{token}}
Content-Type: application/json
Host: api.teya.com
{
"terminal_id": "term-001",
"total_amount": { "amount": 5000, "currency": "GBP" },
"print_model": {
"rows": [
{ "type": "Items", "items": [ { "type": "Text", "text": "Teya Cafe", "bold": true, "align": "CENTER" } ] },
{ "type": "Divider" },
{ "type": "Items", "items": [ { "type": "Text", "text": "Total: 50.00", "bold": true, "align": "RIGHT" } ] }
]
}
}
**API reference - **
POST /poslink/v1/tabs/{tab_id}/bill
To send a pre-rendered receipt image instead of a print_model, post the same endpoint as multipart/form-data with terminal_id, total_amount, currency, and an image file.
The image must be 200 KB or smaller, and the whole request 220 KB or smaller. Exceeding either limit is rejected with 413 PAYLOAD_TOO_LARGE and the code BILL_IMAGE_TOO_LARGE.
If you can't fulfil the request (table not found, bill already settled elsewhere, etc.), reject it instead - see Rejecting a request.
Step 5 - Respond to a payment request
When staff start a payment - full, or one share of a split - you receive a PAY_REQUEST event carrying the amount, type (FULL / SPLIT), and method (CARD / CASH):
event: PAY_REQUEST
data: {"event_type":"PAY_REQUEST","tab_id":"3f2b6a2e-...","merchant_reference":"ORDER-2026-001","terminal_id":"term-001","type":"SPLIT","method":"CARD","amount":{"amount":2500,"currency":"GBP"}}
Materialise the payment by calling the standard payment-requests endpoint with the tab_id and payment_type from the event. This is the same call as a Pay at Counter payment - the two extra fields are what link it to the tab.
POST /poslink/v3/payment-requests HTTP/1.1
Authorization: bearer {{token}}
Content-Type: application/json
Idempotency-Key: {{$uuid}}
Host: api.teya.com
{
"store_id": "eac4d470-4731-45b2-af6b-c828f03c1f9d",
"terminal_id": "term-001",
"requested_amount": { "amount": 2500, "currency": "GBP" },
"transaction_type": "SALE",
"merchant_reference": "ORDER-2026-001-PAY-1",
"tab_id": "3f2b6a2e-9d3e-4a1f-8f1e-6b2e5a8c9d01",
"payment_type": "SPLIT",
"payment_method": "CARD"
}
**API reference - **
POST /poslink/v3/payment-requests
A few things to note:
merchant_referencehere is the reference for this payment, not the tab - give each payment (each split share) its own unique value.payment_typeis required whenevertab_idis present.- The card machine handles split arithmetic and tips locally; you simply create a payment for the
amountin the event. Tips, when added, are reflected in the final payment result. - For a cash split (
payment_method: CASH), record the payment the same way - there's no card flow on the card machine for cash.
Step 6 - Track payments
As each payment progresses you receive updates on the same stream:
TAB_PAYMENT_STATUS_PROGRESS- interim status of a payment (e.g.IN_PROGRESS).TAB_PAYMENT_COMPLETED- a single payment reached a final state (SUCCESSFUL,FAILED,CANCELLED).
Both carry a payment_request_detail object. Use these to update your own order/UI in real time. For richer per-payment data (card metadata, gateway IDs), retrieve the linked payment with GET /poslink/v3/payment-requests/{payment-request-id}, the SSE-based real-time status endpoint (see Send a payment request).
event: TAB_PAYMENT_COMPLETED
data: {"event_type":"TAB_PAYMENT_COMPLETED","tab_id":"3f2b6a2e-...","merchant_reference":"ORDER-2026-001","payment_request_detail":{"payment_request_id":"9c1a7c2c-...","status":"SUCCESSFUL","amount":{"amount":2500,"currency":"GBP"},"tip":200,"method":"CARD"}}
When the balance reaches zero, POSLink emits TAB_COMPLETED with the totals:
event: TAB_COMPLETED
data: {"event_type":"TAB_COMPLETED","tab_id":"3f2b6a2e-...","merchant_reference":"ORDER-2026-001","total_paid":5000,"payment_count":3}
Step 7 - Close the tab
Closing a tab is mandatory once it's settled - it removes the table from the card machine's open-tables list. Close it when you receive TAB_COMPLETED.
DELETE /poslink/v1/tabs/{tab_id} HTTP/1.1
Authorization: bearer {{token}}
Host: api.teya.com
**API reference - **
DELETE /poslink/v1/tabs/{tab_id}
Other operations
You won't need these on the happy path, but they're available:
| Operation | Endpoint | When to use |
|---|---|---|
| List tabs | GET /poslink/v1/tabs |
Reconcile open tabs, or recover after downtime. Defaults to active tabs (OPEN, PAYING, PAUSED); cursor-paginated. Filter with merchant_reference to find one tab. |
| Get a tab | GET /poslink/v1/tabs/{tab_id} |
Fetch full state and payment history - use this to resync after a missed-events gap on the stream. |
| Update a tab | PATCH /poslink/v1/tabs/{tab_id} |
Push a changed total_amount and/or tab_item_data when the order is amended. |
Rejecting a request
When you can't fulfil a SHOW_BILL_REQUEST or PAY_REQUEST, report an error instead of responding. POSLink clears the bill from the card machine and shows your message.
POST /poslink/v1/tabs/{tab_id}/errors HTTP/1.1
Authorization: bearer {{token}}
Content-Type: application/json
Host: api.teya.com
{
"terminal_id": "term-001",
"error_code": "TABLE_NOT_FOUND",
"message": "Table 5 was not found in the ePOS system"
}
**API reference - **
POST /poslink/v1/tabs/{tab_id}/errors
| Error code | When |
|---|---|
TABLE_NOT_FOUND |
The table/order no longer exists on the ePOS. |
BILL_ALREADY_SETTLED |
The bill was already paid outside PAT. |
EPOS_ERROR |
A generic ePOS-side failure. |
REQUEST_REJECTED |
The ePOS explicitly rejects the request (e.g. permission denied). |
Tab events reference
Every event on the stream carries event_type, tab_id, and merchant_reference, plus the fields below.
| Event | Extra fields | What it means |
|---|---|---|
SHOW_BILL_REQUEST |
terminal_id |
A card machine selected the table - respond with the bill (Step 4). |
PAY_REQUEST |
terminal_id, type, method, amount |
A payment is starting - create the payment request (Step 5). |
TAB_PAYMENT_STATUS_PROGRESS |
payment_request_detail |
Interim status of a payment in progress. |
TAB_PAYMENT_COMPLETED |
payment_request_detail |
A single payment reached a final state. |
TAB_PAUSED |
- | A split was paused mid-flow. |
TAB_RESUMED |
- | A paused split resumed. |
TAB_COMPLETED |
total_paid, payment_count |
The tab is fully paid - close it (Step 7). |
TAB_BILL_HIDDEN |
- | The card machine stopped showing the bill without paying (staff backed out, or the display timed out). |
Tab status
A tab moves through these statuses, reported on tab responses:
| Status | Meaning |
|---|---|
OPEN |
Registered and payable; no payment in progress. |
PAYING |
A payment (full or split) is in progress. |
PAUSED |
A split was paused; resumes on the next payment. |
COMPLETED |
Fully paid (remaining is 0). |
CLOSED |
Closed by the ePOS or by automatic cleanup. |