# Pay at Table

Pay at Table (PAT) lets a Teya payment card machine initiate payments against an open table (or any open order) that lives in your ePOS application. A staff member taps the table on the card machine; the card machine asks your ePOS for the bill, displays it to the customer, takes payment (in full or split), and notifies your ePOS when the payment lands.

This guide walks through the complete PAT integration on **Windows** with the Teya Unified ePOS SDK.

```admonition
icon: fas fa-info-circle
content: |
    Pay at Table is available from **Unified ePOS SDK v1.6.0** on Windows. The walkthrough below assumes the PosLink SDK has already been integrated and [Setup](/epos-sdk/poslink/windows/getting-started#3-initialize-and-set-up-the-sdk) is complete - see [Getting Started](/epos-sdk/poslink/windows/getting-started).
```

## When to use Pay at Table

The integration has two halves:

1. **The ePOS registers open tables with Teya** as **tabs**. The card machine lists those tabs to the staff member.
2. **The ePOS subscribes to card machine-initiated events** (bill requested, payment requested, payment completed, tab paused/resumed). It can answer bill requests with the bill, start the payment via the SDK when a payment request arrives, and close the tab once it is fully paid.

> Pay at Counter (the regular `makePaymentAndSubscribe` flow) covers the simpler case: the customer comes to a counter to pay and the ePOS initiates the payment. Pay at Table fits restaurants, bars, hotels, and any setting where the card machine is mobile and the staff member, rather than the ePOS user, drives payment.

## Vocabulary

| Term | Meaning |
|---|---|
| **Tab** | An open table, ticket, or order in your ePOS. Each tab has an ePOS-chosen `tabId` and a display `tabName`. |
| **Bill** | The current breakdown of items, total, and amount remaining for a tab. Your ePOS owns the bill data; Teya only relays it to the card machine on request. |
| **Payment request** | A card machine-initiated request to pay against a tab. Carries an `amount`, a `paymentType` (`FULL` or `SPLIT`), and the originating `terminalId`. |
| **Tab status** | `OPEN`, `PAYING`, `PAUSED`, `COMPLETED`, or `CLOSED`. Active tabs are `OPEN`, `PAYING`, `PAUSED`. |

## Lifecycle

```text
        ┌───── ePOS ─────┐                  ┌─────── Teya ───────┐                ┌── Terminal ──┐
        │                │                  │                    │                │              │
        │ openTab(id,    │ ───────────────▶ │   tab registered   │                │              │
        │   name, "GBP") │                  │                    │                │              │
        │                │                  │                    │                │              │
        │                │                  │                    │ ◀───────────── │ staff taps │
        │                │                  │ SHOW_BILL_REQUEST │                │ tab on UI   │
        │   onShowBill   │◀── RPC stream ───│                    │                │              │
        │   Requested    │                  │                    │                │              │
        │                │                  │                    │                │              │
        │ respondToBill │ ───────────────▶ │     bill sent      │ ─────────────▶ │ bill shown │
        │ Request(total) │                  │                    │                │              │
        │                │                  │                    │                │              │
        │                │                  │                    │ ◀───────────── │ staff taps │
        │                │                  │    PAY_REQUEST     │                │ Pay         │
        │   onPay        │◀── RPC stream ───│                    │                │              │
        │   Requested    │                  │                    │                │              │
        │                │                  │                    │                │              │
        │ makePayment(   │ ───────────────▶ │ payment created   │ ─────────────▶ │ takes card │
        │   tabContext) │                  │                    │                │              │
        │                │                  │                    │                │              │
        │                │                  │                    │ ◀───────────── │ payment     │
        │                │                  │   TAB_COMPLETED    │                │ successful │
        │   onTab        │◀── RPC stream ───│                    │                │              │
        │   Completed    │                  │                    │                │              │
        │                │                  │                    │                │              │
        │ closeTab(id)   │ ───────────────▶ │    tab archived    │                │              │
        └────────────────┘                  └────────────────────┘                └──────────────┘
```

Key points about the lifecycle:

- **Tabs are opened by the ePOS.** Teya does not create tabs on its own; the card machine can only show tabs the ePOS has registered.
- **The card machine drives the conversation.** All bill, pay, pause, and resume events originate from the card machine; the ePOS reacts to them rather than initiating them.
- **`SHOW_BILL_REQUEST` does not commit the customer to paying.** The staff member can show the bill, review it, and close it. `onBillHidden` fires when the bill comes off screen.
- **Payment requests move the tab to `PAYING`.** Split payments produce multiple `PAY_REQUEST` events against the same tab, each with `paymentType = SPLIT`. The tab transitions to `COMPLETED` once the outstanding balance reaches zero.
- **Tabs can pause.** If the customer steps away mid-payment or the staff member explicitly pauses, `onTabPaused` fires. `TAB_RESUMED` follows when the staff member returns. Tabs idle for 24 hours are auto-closed by Teya.
- **`closeTab` archives a tab.** It works on `OPEN`, `PAUSED`, and `COMPLETED` tabs; a `PAYING` tab must settle (or cancel) its in-flight payment before it can be closed. Until called, the tab keeps surfacing in `listTabs`.

## Connection model

Tab events stream over a single JSON-RPC call. `subscribeTabEvents` opens the stream and the SDK emits each tab event as a JSON-RPC notification (a message with no `id`), each one carrying a `result.type` such as `subscribeTabEvents->onShowBillRequested`. `unsubscribeTabEvents` ends the stream, which then completes with a final response carrying the original request `id` and a `subscribeTabEvents->onConnectionStateChange` result whose `state` is `DISCONNECTED`. Transient disconnects (network drops, brief backend restarts) are retried automatically with exponential backoff, so no explicit handling is needed. `DISCONNECTED` is also delivered when the SDK drops the stream because the linked store was cleared; in that case the setup flow needs to run again before resubscribing.

## What is NOT covered here

- **PosLink SDK basics** (initialization, store linking, regular payments) covered in [POSLink Getting Started](/epos-sdk/poslink/windows/getting-started) and [Make Payment](/epos-sdk/poslink/windows/make-payment).
- **Designing the merchant-side UI of the ePOS.** The SDK is headless for PAT; table state, bill rendering, and payment summaries live on the ePOS side.
- **Android / iOS / direct REST integration.** Separate guides cover those platforms.

---

## Enable Pay at Table on the store

PAT is opt-in at the store level. `setPayAtTableEnabledOnStore` with `enable: true` enables it for the linked store; persisting a flag client-side avoids repeating the call on every launch.

**Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "setPayAtTableEnabledOnStore",
  "params": {
    "enable": true
  },
  "id": "f3c0a1d4-7e91-4f3a-9e1b-7c2d8a6f4e10"
}
```

**Response:**

```tabs
    tab:
      title: Success
      content:
        ```json
        {
          "jsonrpc": "2.0",
          "result": {
            "type": "setPayAtTableEnabledOnStore->onResult",
            "failureReason": null,
            "debugErrorMessage": null
          },
          "id": "f3c0a1d4-7e91-4f3a-9e1b-7c2d8a6f4e10"
        }
        ```
        `failureReason` is `null` and POSLink has accepted the new setting. Persist locally that PAT is enabled for this store.
    tab:
      title: Failure
      content:
        ```json
        {
          "jsonrpc": "2.0",
          "result": {
            "type": "setPayAtTableEnabledOnStore->onResult",
            "failureReason": "NETWORK_ERROR",
            "debugErrorMessage": "Socket timeout has expired"
          },
          "id": "f3c0a1d4-7e91-4f3a-9e1b-7c2d8a6f4e10"
        }
        ```
        `failureReason` is one of `NO_LINKED_STORE`, `NETWORK_ERROR`, or `UNEXPECTED_ERROR`.
```

`setPayAtTableEnabledOnStore` is a **store-wide capability flag**. Each individual card machine additionally keeps a **local opt-out toggle**, defaulting to ON. Disabling the store flag does not flip the local toggle, and enabling it does not override a card machine that the merchant opted out individually. Disabling is an entry-point gate only; any PAT session already in progress runs to completion.

---

## Open a tab

`openTab` registers a table or open order so the card machine can list it and act on it. The ePOS picks the `tabId`; Teya stores the metadata and routes card machine-initiated events back to the ePOS.

**Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "openTab",
  "params": {
    "tabId": "ORDER-2026-001",
    "tabName": "Table 5",
    "currency": "GBP"
  },
  "id": "9b7f1d22-4c54-4e71-8e2a-86a4f3e2b9c1"
}
```

- `tabName` is the label shown on the card machine's tab list. POSLink does not enforce uniqueness.
- `currency` is an ISO 4217 code; all payments on this tab must use it.

**Response:**

```tabs
    tab:
      title: Success
      content:
        ```json
        {
          "jsonrpc": "2.0",
          "result": {
            "type": "openTab->onResult",
            "tab": {
              "tabId": "ORDER-2026-001",
              "tabName": "Table 5",
              "status": "OPEN",
              "totalAmount": null,
              "totalPaid": null,
              "remaining": null,
              "currency": "GBP",
              "showingBillTerminalId": null,
              "billFileId": null,
              "paymentRequests": null,
              "createdAtEpochMillis": 1733241600000,
              "updatedAtEpochMillis": 1733241600000
            },
            "failureReason": null,
            "debugErrorMessage": null
          },
          "id": "9b7f1d22-4c54-4e71-8e2a-86a4f3e2b9c1"
        }
        ```
        The returned `tab` carries the freshly opened tab. Detail-only fields (`paymentRequests`, `billFileId`, totals) are `null` at creation; they populate as the tab is used.
    tab:
      title: Failure
      content:
        ```json
        {
          "jsonrpc": "2.0",
          "result": {
            "type": "openTab->onResult",
            "tab": null,
            "failureReason": "NETWORK_ERROR",
            "debugErrorMessage": "Socket timeout has expired"
          },
          "id": "9b7f1d22-4c54-4e71-8e2a-86a4f3e2b9c1"
        }
        ```
        `failureReason` is one of `NO_LINKED_STORE`, `NETWORK_ERROR`, or `UNEXPECTED_ERROR`.
```

---

## Subscribe to tab events

PAT is event-driven. `subscribeTabEvents` is a single _streaming_ JSON-RPC call: send it once, and the SDK emits tab events back as JSON-RPC notifications (messages with no `id`). The ePOS branches on each notification's `result.type` to decide how to react. As described in [Getting Started](/epos-sdk/poslink/windows/getting-started#read-responses-asynchronously), notifications can be detected in the read loop by checking for a missing `id` field.

**Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "subscribeTabEvents",
  "id": "0b3a7c8d-2a4f-4d6c-9b3a-6c1e8f5d4a20"
}
```

While the stream is active the SDK emits notifications, each one with a different `result.type` value. A representative bill-requested notification looks like:

```json
{
  "jsonrpc": "2.0",
  "result": {
    "type": "subscribeTabEvents->onShowBillRequested",
    "tabId": "ORDER-2026-001",
    "terminalId": "term-001"
  }
}
```

Connection-state notifications use the same envelope:

```json
{
  "jsonrpc": "2.0",
  "result": {
    "type": "subscribeTabEvents->onConnectionStateChange",
    "state": "CONNECTED"
  }
}
```

When the underlying stream actually drops (because `unsubscribeTabEvents` was called or the linked store was cleared), the SDK emits one final response carrying the **original request `id`** and a `DISCONNECTED` connection state, then stops emitting:

```json
{
  "jsonrpc": "2.0",
  "result": {
    "type": "subscribeTabEvents->onConnectionStateChange",
    "state": "DISCONNECTED"
  },
  "id": "0b3a7c8d-2a4f-4d6c-9b3a-6c1e8f5d4a20"
}
```

`CONNECTING` only appears on transient drops while the SDK reconnects; `CONNECTED` is emitted once the stream is established. Only one subscription may be active at a time; calling `subscribeTabEvents` again while one is live is rejected with an invalid-state error (see [Getting Started](/epos-sdk/poslink/windows/getting-started#error-responses) for the error envelope).

**All sub-event types at a glance:**

| `result.type` | Fires when |
|---|---|
| `subscribeTabEvents->onShowBillRequested` | Card Machine asks for the bill so it can be shown to the customer. |
| `subscribeTabEvents->onPayRequested` | Card Machine wants to take a payment (full or split) against the tab. |
| `subscribeTabEvents->onPaymentProgress` | Best-effort intermediate update for an in-flight payment. |
| `subscribeTabEvents->onPaymentCompleted` | One payment request reached a final state. Split payments fire this once per payment. |
| `subscribeTabEvents->onTabPaused` | Card Machine paused a split mid-flow (e.g. customer stepped away). |
| `subscribeTabEvents->onTabResumed` | A paused tab resumed because a card machine initiated a new payment against it. |
| `subscribeTabEvents->onTabCompleted` | The tab's outstanding balance reached zero. You still need to call `closeTab`. |
| `subscribeTabEvents->onBillHidden` | Bill came off screen without payment (card machine exited or timed out). |
| `subscribeTabEvents->onConnectionStateChange` | `CONNECTED` ↔ `CONNECTING` ↔ `DISCONNECTED` lifecycle of the underlying stream. |

To stop the stream, send:

```json
{
  "jsonrpc": "2.0",
  "method": "unsubscribeTabEvents",
  "id": "47ae2f3c-9b1d-4c8a-95e2-2a3b7c5d8f01"
}
```

The SDK responds with an acknowledgement (`unsubscribeTabEvents->ack`) and the streamed `subscribeTabEvents` call then completes with its final `DISCONNECTED` response described above. The call is a no-op if no subscription is active.

---

## Respond to bill requests

When a `subscribeTabEvents->onShowBillRequested` notification arrives, `respondToBillRequest` sends the bill back so the card machine can render it. It accepts either a `printModel` (structured receipt rows, using the same row schema as `printCustomTemplate`) or a `billImagePath` (absolute path to a pre-rendered PNG/JPEG). When the request cannot be honoured (order missing, bill already settled, internal error), `respondWithError` rejects it with an error category and a message that the card machine surfaces to the staff member.

**Incoming event:**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "type": "subscribeTabEvents->onShowBillRequested",
    "tabId": "ORDER-2026-001",
    "terminalId": "term-001"
  }
}
```

**Respond with the bill**

**Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "respondToBillRequest",
  "params": {
    "tabId": "ORDER-2026-001",
    "terminalId": "term-001",
    "totalAmount": 5000,
    "currency": "GBP",
    "printModel": [
      { "rowType": "item", "item": { "elementType": "text", "text": "Sample bill", "bold": true, "align": "CENTER" } },
      { "rowType": "item", "item": { "elementType": "text", "text": "Tab: ORDER-2026-001", "align": "CENTER" } },
      { "rowType": "spacer" },
      {
        "rowType": "items",
        "items": [
          { "elementType": "text", "text": "TOTAL", "bold": true },
          { "elementType": "text", "text": "GBP 50.00", "bold": true, "align": "RIGHT" }
        ]
      }
    ]
  },
  "id": "f1a2b3c4-d5e6-47f8-89ab-1c2d3e4f5061"
}
```

- `tabId` and `terminalId` echo the values from the originating event.
- `totalAmount` is in the smallest currency unit (e.g. `5000` for £50.00).
- Pass `printModel` for a structured itemised bill, or `billImagePath` (absolute path) for a pre-rendered image. When `billImagePath` is set the SDK uploads the image and `printModel` is not sent. The same payload drives both the on-screen view and the printed copy, so it should render legibly in both.

**Response:**

```tabs
    tab:
      title: Success
      content:
        ```json
        {
          "jsonrpc": "2.0",
          "result": {
            "type": "respondToBillRequest->onResult",
            "failureReason": null,
            "debugErrorMessage": null
          },
          "id": "f1a2b3c4-d5e6-47f8-89ab-1c2d3e4f5061"
        }
        ```
        POSLink accepted the bill. The terminal will display (and optionally print) it.
    tab:
      title: Failure
      content:
        ```json
        {
          "jsonrpc": "2.0",
          "result": {
            "type": "respondToBillRequest->onResult",
            "failureReason": "TOTAL_LESS_THAN_PAID",
            "debugErrorMessage": "Total 5000 is less than already paid 6000"
          },
          "id": "f1a2b3c4-d5e6-47f8-89ab-1c2d3e4f5061"
        }
        ```
        `failureReason` is one of the values listed in the table below.
```

**Alternative: pre-rendered bill image**

For ePOSes that already render the bill as an image (PDFs converted to bitmaps, generated receipts, etc.), pass `billImagePath` instead of `printModel`. The value must be an absolute path to a PNG or JPEG on disk. The SDK uploads the file and `printModel` is ignored when `billImagePath` is supplied.

```json
{
  "jsonrpc": "2.0",
  "method": "respondToBillRequest",
  "params": {
    "tabId": "ORDER-2026-001",
    "terminalId": "term-001",
    "totalAmount": 5000,
    "currency": "GBP",
    "billImagePath": "C:\\Bills\\ORDER-2026-001.png"
  },
  "id": "9b8a7c6d-5e4f-43a2-b1c0-d9e8f7a6b5c4"
}
```

**Bill limits**

Bills are rendered by the same pipeline as [custom receipt printing](/epos-sdk/poslink/windows/print-receipts), so the same limits apply.

**Template limits** - apply when you send `printModel`:

- A template can have a maximum of **150** rows.
- Before being sent to the card machine the template is converted to another JSON in which images are encoded as Base64 strings. This conversion increases the overall size, so the final JSON (including images) must not exceed **250 KB**.

**Image limits** - apply when you send `billImagePath`:

- The maximum height for the image is **10000 px**.
- The recommended width is **384 px**. Wider images are scaled down to fit the printer width.
- The image is converted to image data before being sent, and the total size of that data must not exceed **200 KB**.

Limits that can be detected immediately return a JSON-RPC error with `code` `-3` (`ILLEGAL_ARGUMENT_EXCEPTION`) and the reason in `message`. Limits detected by the backend are reported in `respondToBillRequest->onResult` with `failureReason` set to `UNEXPECTED_ERROR` and details in `debugErrorMessage`.

**Reject the request**

**Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "respondWithError",
  "params": {
    "tabId": "ORDER-2026-001",
    "terminalId": "term-001",
    "errorCode": "TABLE_NOT_FOUND",
    "message": "Tab no longer exists"
  },
  "id": "21fa9bd0-b8c7-4aa3-8c4d-94c0e2f6d7b3"
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "type": "respondWithError->onResult",
    "failureReason": null,
    "debugErrorMessage": null
  },
  "id": "21fa9bd0-b8c7-4aa3-8c4d-94c0e2f6d7b3"
}
```

A null `failureReason` means POSLink accepted the rejection and the card machine will display `message` alongside the error code. On failure `failureReason` is one of `NO_LINKED_STORE`, `NETWORK_ERROR`, or `UNEXPECTED_ERROR`.

**Error handling**

**`errorCode` values** carried by `respondWithError`:

| Code | Meaning |
|---|---|
| `TABLE_NOT_FOUND` | The tab/order no longer exists in the ePOS. |
| `BILL_ALREADY_SETTLED` | The bill was already settled outside Pay at Table (cash at counter, refunded earlier, etc.). |
| `EPOS_ERROR` | Generic ePOS-side failure: DB down, internal exception, anything not covered above. |
| `REQUEST_REJECTED` | The ePOS explicitly rejects the request as a policy decision. |

**`respondToBillRequest->onResult.failureReason` values:**

| Reason | Meaning |
|---|---|
| `NO_LINKED_STORE` | The SDK has no linked store; re-running [Setup](/epos-sdk/poslink/windows/getting-started#3-initialize-and-set-up-the-sdk) restores it. |
| `NETWORK_ERROR` | Could not reach Teya; backoff retry is a safe response. |
| `TOTAL_LESS_THAN_PAID` | The supplied `totalAmount` is lower than the amount already paid on the tab; the bill needs to be recomputed. |
| `TAB_IN_WRONG_STATE` | Show-bill TTL expired, tab not found, tab is `COMPLETED`/`CLOSED`, or `terminalId` mismatch; the request can be treated as cancelled. |
| `UNEXPECTED_ERROR` | Anything else; `debugErrorMessage` carries details. |

---

## Take tab payments

When a `subscribeTabEvents->onPayRequested` notification arrives, `makePaymentAndSubscribe` (the same method covered in [Make Payment](/epos-sdk/poslink/windows/make-payment)) starts the payment. The values carried by the event become a `tabContext` object on the request, so the SDK can route the payment back to the originating card machine and tab. The context carries the `tabId`, the originating `terminalId`, the payment `type` (`FULL` or `SPLIT`), and the payment `method` (`CARD` or `CASH`).

**Incoming event:**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "type": "subscribeTabEvents->onPayRequested",
    "tabId": "ORDER-2026-001",
    "terminalId": "term-001",
    "paymentType": "SPLIT",
    "paymentMethod": "CARD",
    "amount": 2500,
    "currency": "GBP"
  }
}
```

**Start the payment, request:**

```json
{
  "jsonrpc": "2.0",
  "method": "makePaymentAndSubscribe",
  "params": {
    "transactionId": "tx-019851ef-29f2-70e8-800d-fdd875bfa0a4",
    "amount": 2500,
    "currency": "GBP",
    "useTeyaDefaultUi": true,
    "tip": null,
    "tabContext": {
      "tabId": "ORDER-2026-001",
      "terminalId": "term-001",
      "type": "SPLIT",
      "method": "CARD"
    }
  },
  "id": "8c2c3a3f-6f1c-4ed0-9f8b-2b4d6e8a0c12"
}
```

- Build `tabContext` by echoing the event's fields. The event names them `paymentType` / `paymentMethod`; the request names them `type` / `method` under `tabContext`.
- `amount` and `currency` on the request match the event's `amount` and `currency`.

Responses follow the standard `makePaymentAndSubscribe->onPaymentStateChanged` envelope documented in [Make Payment](/epos-sdk/poslink/windows/make-payment). There is no separate payment-state subscription for tab payments; the existing intermediate-state notifications (messages with no `id`) and the final response (carrying the original request `id`) are the same surface, and tab payments succeed or fail through that same channel. In addition, the active `subscribeTabEvents` stream emits `subscribeTabEvents->onPaymentProgress` and `subscribeTabEvents->onPaymentCompleted` notifications for the same payment, so either surface can drive the UI.

```admonition
icon: fas fa-info-circle
content: |
    The SDK uses `tabContext.terminalId` to route the payment, so tab payments succeed even when the ePOS is linked at the **store** level only (no specific terminal linked). The terminal that initiated the request is always the one that completes the payment.
```

---

## Close the tab

`closeTab` archives a tab and removes it from the default `listTabs` results. It works on `OPEN`, `PAUSED`, and `COMPLETED` tabs; a `PAYING` tab must settle (or cancel) its in-flight payment before it can be closed. Teya also auto-closes tabs idle for 24 hours.

**Request:**

```json
{
  "jsonrpc": "2.0",
  "method": "closeTab",
  "params": {
    "tabId": "ORDER-2026-001"
  },
  "id": "5d4c3b2a-1e9f-48d7-b6c5-a4f3e2d1c0b9"
}
```

**Response:**

```tabs
    tab:
      title: Success
      content:
        ```json
        {
          "jsonrpc": "2.0",
          "result": {
            "type": "closeTab->onResult",
            "failureReason": null,
            "debugErrorMessage": null
          },
          "id": "5d4c3b2a-1e9f-48d7-b6c5-a4f3e2d1c0b9"
        }
        ```
        The tab transitioned to `CLOSED`.
    tab:
      title: Failure
      content:
        ```json
        {
          "jsonrpc": "2.0",
          "result": {
            "type": "closeTab->onResult",
            "failureReason": "NETWORK_ERROR",
            "debugErrorMessage": "Socket timeout has expired"
          },
          "id": "5d4c3b2a-1e9f-48d7-b6c5-a4f3e2d1c0b9"
        }
        ```
        `failureReason` is one of `NO_LINKED_STORE`, `NETWORK_ERROR`, or `UNEXPECTED_ERROR`.
```

A closed tab is **not** deleted. `getTab` still returns it and `listTabs` with `statuses: ["CLOSED"]` still surfaces it, so payment history and receipts remain available.

---

## List and get tabs

**`listTabs` (cursor-paginated) request:**

```json
{
  "jsonrpc": "2.0",
  "method": "listTabs",
  "params": {
    "statuses": ["OPEN", "PAYING", "PAUSED"],
    "after": null,
    "before": null,
    "limit": 50
  },
  "id": "a1b2c3d4-e5f6-4708-89a0-1c2d3e4f5061"
}
```

- `statuses` is optional. When omitted or `null` the backend default applies (active tabs only: `OPEN`, `PAYING`, `PAUSED`). Pass `"COMPLETED"` or `"CLOSED"` to include archived tabs.
- `after` and `before` are opaque cursors from a previous page's `nextCursor` / `prevCursor`; they are mutually exclusive.
- `limit` is optional; the backend default applies when `null`.

**`listTabs` response:**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "type": "listTabs->onResult",
    "page": {
      "items": [
        {
          "tabId": "ORDER-2026-001",
          "tabName": "Table 5",
          "status": "OPEN",
          "totalAmount": 5000,
          "totalPaid": 0,
          "remaining": 5000,
          "currency": "GBP",
          "showingBillTerminalId": null
        }
      ],
      "nextCursor": "cursor-next",
      "prevCursor": null
    },
    "failureReason": null,
    "debugErrorMessage": null
  },
  "id": "a1b2c3d4-e5f6-4708-89a0-1c2d3e4f5061"
}
```

Pass the `nextCursor` value into `after` on the next call to fetch the following page.

**`getTab` (full detail including payment history) request:**

```json
{
  "jsonrpc": "2.0",
  "method": "getTab",
  "params": {
    "tabId": "ORDER-2026-001"
  },
  "id": "b2c3d4e5-f607-4819-9ab0-2d3e4f506172"
}
```

**`getTab` response:**

```json
{
  "jsonrpc": "2.0",
  "result": {
    "type": "getTab->onResult",
    "tab": {
      "tabId": "ORDER-2026-001",
      "tabName": "Table 5",
      "status": "PAYING",
      "totalAmount": 5000,
      "totalPaid": 2500,
      "remaining": 2500,
      "currency": "GBP",
      "showingBillTerminalId": "term-001",
      "billFileId": null,
      "paymentRequests": [
        {
          "eposTransactionId": "tx-123",
          "status": "SUCCESSFUL",
          "amount": 2500,
          "currency": "GBP",
          "tip": null,
          "method": "CARD",
          "type": "SPLIT",
          "createdAtEpochMillis": 1733241600000,
          "updatedAtEpochMillis": 1733241700000,
          "transactionTimestampEpochMillis": 1733241650000,
          "metadata": null
        }
      ],
      "createdAtEpochMillis": 1733241500000,
      "updatedAtEpochMillis": 1733241700000
    },
    "failureReason": null,
    "debugErrorMessage": null
  },
  "id": "b2c3d4e5-f607-4819-9ab0-2d3e4f506172"
}
```

`tab.paymentRequests` lists every payment recorded against the tab. Field-level documentation for each model surfaced here lives in the auto-generated API Reference shipped alongside the Windows SDK.
