# 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 **iOS** 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 iOS. The walkthrough below assumes the PosLink SDK has already been integrated and [Setup](/epos-sdk/poslink/ios/getting-started#5-set-up-the-sdk) is complete - see [Getting Started](/epos-sdk/poslink/ios/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 `makePayment` 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 `type` (`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   │◀── SSE event ────│                    │                │              │
        │   Requested    │                  │                    │                │              │
        │                │                  │                    │                │              │
        │ respondToBill │ ───────────────▶ │     bill sent      │ ─────────────▶ │ bill shown │
        │ Request(total) │                  │                    │                │              │
        │                │                  │                    │                │              │
        │                │                  │                    │ ◀───────────── │ staff taps │
        │                │                  │    PAY_REQUEST     │                │ Pay         │
        │   onPay        │◀── SSE event ────│                    │                │              │
        │   Requested    │                  │                    │                │              │
        │                │                  │                    │                │              │
        │ makePayment(   │ ───────────────▶ │ payment created   │ ─────────────▶ │ takes card │
        │   tabContext) │                  │                    │                │              │
        │                │                  │                    │                │              │
        │                │                  │                    │ ◀───────────── │ payment     │
        │                │                  │   TAB_COMPLETED    │                │ successful │
        │   onTab        │◀── SSE event ────│                    │                │              │
        │   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 `type = 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 Server-Sent Events. The first `subscribe` opens the stream; the last `unsubscribe` closes it. Transient disconnects (network drops, brief backend restarts) are retried automatically with exponential backoff, so no explicit handling is needed. `TeyaTabConnectionState.disconnected` is emitted whenever the backing connection is actually dropped: when the last subscriber is removed (`unsubscribe` / `unsubscribeAll`), or when the SDK drops the stream because the linked store was cleared. In the latter 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/ios/getting-started) and [Make Payment](/epos-sdk/poslink/ios/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 / Windows / direct REST integration**. Separate guides cover those surfaces.

---

## Enable Pay at Table on the store

PAT is opt-in at the store level. `setPayAtTableEnabledOnStore(enable: true, ...)` enables it for the linked store; persisting a flag client-side avoids repeating the call on every launch. All callbacks are dispatched on the **main thread**.

```swift
teyaPosLinkSDK.tabsApi.setPayAtTableEnabledOnStore(
    enable: true,
    onSuccess: {
        // Persist that PAT is enabled for this store
    },
    onFailure: { failure in
        print("Failed to enable PAT: \(failure.reason)")
    }
)
```

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

```swift
let tabId = TeyaTabId(value: "ORDER-2026-001")

teyaPosLinkSDK.tabsApi.openTab(
    tabId: tabId,
    tabName: "Table 5",       // shown on the terminal's tab list
    currency: "GBP",          // ISO 4217; all payments on this tab must use it
    onSuccess: { tab in
        // tab.status == TeyaTabStatus.open
        // tab.paymentRequests is nil at creation (no payments yet)
    },
    onFailure: { failure in
        print("openTab failed: \(failure.reason)")
    }
)
```

The returned `TeyaTab` carries the freshly opened tab. Detail-only fields (line items, payment requests, bill file id) are `nil` at creation; they populate as the tab is used.

---

## Subscribe to tab events

PAT is event-driven. `TeyaTabEventListener` is the listener protocol; every callback has a no-op default, so only the events of interest need overriding. `tabEvents.subscribe` registers a listener. Callbacks are dispatched on the **main thread**.

```swift
private class TabEventsListener: TeyaTabEventListener {

    func onShowBillRequested(request: TeyaTabBillRequest) {
        // See "Respond to bill requests"
    }

    func onPayRequested(request: TeyaTabPayRequest) {
        // See "Take tab payments"
    }

    func onPaymentCompleted(detail: TeyaTabPaymentDetail) {
        print("Payment \(detail.eposTransactionId) reached \(detail.status)")
    }

    func onTabCompleted(completion: TeyaTabCompletion) {
        // See "Close the tab"
    }

    func onConnectionStateChange(state: TeyaTabConnectionState) {
        print("Tab event stream is now \(state)")
    }
}

let tabEventsListener = TabEventsListener()
teyaPosLinkSDK.tabsApi.tabEvents.subscribe(tabEventListener: tabEventsListener)
```

Subscribing for the first time opens the underlying SSE stream and emits `TeyaTabConnectionState.connected` once the connection is established. `connecting` only appears on transient drops while the SDK reconnects. Multiple listeners can be registered against the same stream and each receives the same events; `unsubscribe(tabEventListener:)` and `unsubscribeAll()` detach them.

**All callbacks at a glance:**

| Callback | Fires when |
|---|---|
| `onShowBillRequested(TeyaTabBillRequest)` | Card Machine asks for the bill so it can be shown to the customer. |
| `onPayRequested(TeyaTabPayRequest)` | Card Machine wants to take a payment (full or split) against the tab. |
| `onPaymentProgress(TeyaTabPaymentDetail)` | Best-effort intermediate update for an in-flight payment. |
| `onPaymentCompleted(TeyaTabPaymentDetail)` | One payment request reached a final state. Split payments fire this once per payment. |
| `onTabPaused(TeyaTabId)` | Card Machine paused a split mid-flow (e.g. customer stepped away). |
| `onTabResumed(TeyaTabId)` | A paused tab resumed because a card machine initiated a new payment against it. |
| `onTabCompleted(TeyaTabCompletion)` | The tab's outstanding balance reached zero. You still need to call `closeTab`. |
| `onBillHidden(TeyaTabId)` | Bill came off screen without payment (card machine exited or timed out). |
| `onConnectionStateChange(TeyaTabConnectionState)` | `connected` ↔ `connecting` ↔ `disconnected` lifecycle of the underlying stream. |
| `onUnsubscribed()` | SDK has finished tearing down this listener. |

---

## Respond to bill requests

When `onShowBillRequested` fires, `respondToBillRequest` sends the bill back so the card machine can render it. It accepts either a `printModel` (structured receipt rows) or a `billImage` (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.

```swift
func onShowBillRequested(request: TeyaTabBillRequest) {
    guard let bill = lookUpBillInEpos(tabId: request.tabId) else {
        teyaPosLinkSDK.tabsApi.respondWithError(
            tabId: request.tabId,
            terminalId: request.terminalId,
            errorCode: TeyaTabErrorCode.tableNotFound,
            message: "Tab no longer exists",
            onSuccess: {},
            onFailure: { _ in }
        )
        return
    }
    teyaPosLinkSDK.tabsApi.respondToBillRequest(
        tabId: request.tabId,
        terminalId: request.terminalId,           // echo from the event
        totalAmount: bill.totalMinorUnits,        // e.g. 5000 for £50.00
        currency: bill.currency,
        printModel: bill.toReceiptTemplate(),     // OR pass billImage for a pre-rendered bill
        billImage: nil,
        onSuccess: {
            // Terminal will display (and optionally print) the bill
        },
        onFailure: { failure in
            print("respondToBillRequest failed: \(failure.reason)")
        }
    )
}
```

**Alternative: pre-rendered bill image**

For ePOSes that already render the bill as an image (PDFs converted to bitmaps, generated receipts, etc.), `billImage` accepts a Teya `Image` wrapping a `UIImage`. When `billImage` is supplied, `printModel` is ignored.

```swift
teyaPosLinkSDK.tabsApi.respondToBillRequest(
    tabId: request.tabId,
    terminalId: request.terminalId,
    totalAmount: bill.totalMinorUnits,
    currency: bill.currency,
    printModel: nil,
    billImage: TeyaImageKt.create(uiImage: UIImage(named: "bill_image")!),
    onSuccess: { },
    onFailure: { _ in }
)
```

**Bill limits**

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

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

- A template can have a maximum of **150** rows.
- The template is converted to JSON before being sent to the card machine, and its total size must not exceed **250 KB**.
    - Images inside the template are converted to Base64 strings, which can significantly increase the size.

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

- 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 `UIImage` 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 throw an **exception** when you construct the `Template` or call `TeyaImageKt.create()`. Limits detected by the backend are reported through `respondToBillRequest`'s `onFailure` as `unexpectedError`, with details in `debugErrorMessage`.

**Error handling**

**`TeyaTabErrorCode` values** carried by `respondWithError`:

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

**`ShowBillFailure.Reason` values** returned via `respondToBillRequest`'s `onFailure`:

| Reason | Meaning |
|---|---|
| `noLinkedStore` | The SDK has no linked store; re-running [Setup](/epos-sdk/poslink/ios/getting-started#5-set-up-the-sdk) restores it. |
| `networkError` | Could not reach Teya; backoff retry is a safe response. |
| `totalLessThanPaid` | The supplied `totalAmount` is lower than the amount already paid on the tab; the bill needs to be recomputed. |
| `tabInWrongState` | Show-bill TTL expired, tab not found, tab is `completed`/`closed`, or `terminalId` mismatch; the request can be treated as cancelled. |
| `unexpectedError` | Anything else; `debugErrorMessage` carries details. |

---

## Take tab payments

When `onPayRequested` fires, `makePayment` on `transactionsApi` starts the payment. The `TeyaTabPaymentContext` from the event **is echoed back unchanged** so the SDK can route the payment. The context carries the `tabId`, the originating `terminalId`, the payment `type` (`FULL` or `SPLIT`), and the payment `method` (`CARD` or `CASH`).

```swift
func onPayRequested(request: TeyaTabPayRequest) {
    let paymentSubscription = teyaPosLinkSDK.transactionsApi.makePayment(
        transactionId: UUID().uuidString,
        amount: request.amount,
        currency: request.currency,
        tip: nil,
        tabContext: request.tabContext    // echo from the event
    )

    paymentSubscription.subscribe(
        listener: TeyaPosLinkInProgressUiKt.create(autoDismissOnFinalStateAfterMs: 2000)
    )
}
```

The returned `PaymentStateSubscription` exposes the same lifecycle as regular payments (`Pending` → `New` → `In_Progress` → `Successful` / `Processing_Failed` / `Canceled` / `Communication_Failed`). For tab payments, `onPaymentProgress` and `onPaymentCompleted` on `TeyaTabEventListener` also fire; 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.

```swift
teyaPosLinkSDK.tabsApi.closeTab(
    tabId: TeyaTabId(value: "ORDER-2026-001"),
    onSuccess: {
        // archived
    },
    onFailure: { failure in
        print("closeTab failed: \(failure.reason)")
    }
)
```

A closed tab transitions to `CLOSED`. It is **not** deleted. `getTab(tabId:)` still returns it and `listTabs(statuses: [TeyaTabStatus.closed])` still surfaces it, so payment history and receipts remain available.

---

## List and get tabs

```swift
// listTabs (cursor-paginated)
teyaPosLinkSDK.tabsApi.listTabs(
    statuses: [TeyaTabStatus.open, TeyaTabStatus.paying, TeyaTabStatus.paused], // nil = active default
    after: nil,
    before: nil,
    limit: 50,
    onSuccess: { page in
        for summary in page.items {
            // TeyaTabSummary
        }
        let next = page.nextCursor // pass into `after` on the next call
    },
    onFailure: { _ in /* ... */ }
)

// getTab (full detail including payment history)
teyaPosLinkSDK.tabsApi.getTab(
    tabId: TeyaTabId(value: "ORDER-2026-001"),
    onSuccess: { tab in
        // tab.paymentRequests lists every payment recorded against the tab
    },
    onFailure: { _ in /* ... */ }
)
```

Field-level documentation for each type returned by `listTabs` and `getTab` lives in the auto-generated API Reference (shipped alongside the SDK Swift package).
