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.

POSLink does not own the bill
POSLink keeps a minimal tab record (an ID, the linked payments, and the last known total) purely to orchestrate the flow. Your ePOS remains the source of truth for the bill - its items, totals, and order lifecycle. This keeps the integration light and avoids duplicating your business logic.

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:

  1. Open tab - when a table is opened on the ePOS, it registers the tab with POSLink.
  2. Find a table - any card machine in the store lists the open tabs and a staff member selects one.
  3. 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).
  4. 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.
  5. Complete - once the remaining balance reaches zero, POSLink marks the tab COMPLETED.
  6. 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.
Bill detail is optional
If your ePOS sends item data or a receipt template, the card machine can display and print an itemised bill. If not, the card machine shows only the total.

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 Bearer token), and creating tabs and payments requires the payment_requests scope.
  • 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.

Tab identifiers
When you open a tab you supply a merchant_reference (your own ID for the order, unique per store) and POSLink returns a tab_id. Both are included on every event, so you can correlate either way. Endpoints that act on a specific tab take the tab_id in the path.

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.

Keep the stream healthy
The server sends a ":keepalive" comment about every 20 seconds - reconnect if you don't receive one within 60 seconds. It also closes idle connections after ~10 minutes so load rebalances across replicas, so always auto-reconnect. On reconnect, send the Last-Event-ID header; POSLink replays missed events from a 5-minute buffer. If the first event id you receive is not Last-Event-ID + 1, you've missed events outside the buffer - call GET /poslink/v1/tabs/{tab_id} to resync any affected tabs.

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 unique Idempotency-Key header 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.

Total can't drop below what's already paid
On a resumed tab, the total you send must be greater than or equal to total_paid. A lower total is rejected with 400 - resolve the discrepancy on your side (e.g. refund the difference) first.

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_reference here is the reference for this payment, not the tab - give each payment (each split share) its own unique value.
  • payment_type is required whenever tab_id is present.
  • The card machine handles split arithmetic and tips locally; you simply create a payment for the amount in 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.
Pausing and resuming a split
Staff can leave a split mid-flow. You'll see a TAB_PAUSED event, and a TAB_RESUMED event when they return and start the next payment. You don't track remaining balances yourself - POSLink derives them from the payments taken. Just send the current total whenever a bill is requested.

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}

Stale tabs are cleaned up automatically
If a tab is never closed, POSLink auto-closes tabs with no activity after 24 hours. Closed and completed tabs are excluded from the default tab listing, so they disappear from the card machine's list either way - but you should still close explicitly after payment.

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.