# 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](#integrate-pay-at-table) below; for the full endpoint reference, see the [POSLink API reference](/apis/poslink/apis).

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

```admonition
icon: fas fa-circle-info
title: POSLink does not own the bill
content: 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.

```admonition
icon: fas fa-circle-info
title: Bill detail is optional
content: 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](/poslink/develop/connect-terminal) and [Send a payment request](/poslink/develop/send-payment-request) if you haven't set that up yet.

## Next steps

Ready to integrate? [Integrate Pay at Table](#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](/apis/poslink/apis) 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](/apis/poslink/apis).

### Before you start

- Your ePOS must already be integrated with POSLink for standard Pay at Counter payments - see [Connect a card machine](/poslink/develop/connect-terminal) and [Send a payment request](/poslink/develop/send-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.

```admonition
icon: fas fa-circle-info
title: Tab identifiers
content: 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).

```json title="Example request"
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}`](/apis/poslink/apis#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.

```json title="Example request"
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`](/apis/poslink/apis#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.

```admonition
icon: fas fa-exclamation-triangle
title: Keep the stream healthy
content: 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](#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.

```json title="Example request"
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"
}
```

```json title="Example response"
{
  "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`](/apis/poslink/apis#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**<br>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](/poslink/develop/send-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:

```text title="SSE 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.

```json title="Example request"
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`](/apis/poslink/apis#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`.

```admonition
icon: fas fa-circle-info
title: Total can't drop below what's already paid
content: 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](#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`):

```text title="SSE event"
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.

```json title="Example request"
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`](/apis/poslink/apis#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.

```admonition
icon: fas fa-circle-info
title: Pausing and resuming a split
content: 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}`](/apis/poslink/apis#get--poslink-v3-payment-requests--payment-request-id-), the SSE-based real-time status endpoint (see [Send a payment request](/poslink/develop/send-payment-request)).

```text title="SSE event"
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:

```text title="SSE event"
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`.

```json title="Example request"
DELETE /poslink/v1/tabs/{tab_id} HTTP/1.1
Authorization: bearer {{token}}
Host: api.teya.com
```

> **API reference - ** [`DELETE /poslink/v1/tabs/{tab_id}`](/apis/poslink/apis#delete--poslink-v1-tabs--tab-id-)

```admonition
icon: fas fa-circle-info
title: Stale tabs are cleaned up automatically
content: 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`](/apis/poslink/apis#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}`](/apis/poslink/apis#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}`](/apis/poslink/apis#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.

```json title="Example request"
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`](/apis/poslink/apis#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. |
