# 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 **Android** 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 Android. The walkthrough below assumes the PosLink SDK has already been integrated and [Setup](/epos-sdk/poslink/android/getting-started#5-set-up-the-sdk) is complete - see [Getting Started](/epos-sdk/poslink/android/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. `ConnectionState.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/android/getting-started) and [Make Payment](/epos-sdk/poslink/android/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.
- **iOS / Windows / direct REST integration** - separate guides will ship alongside the iOS and Windows SDK v1.6.0 releases.

---

## Enable Pay at Table on the store

PAT is opt-in at the store level. `setPayAtTableEnabledOnStore(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**.

```tabs
    tab:
      title: Kotlin (callbacks)
      content:
        ```kotlin
        teyaPosLinkSDK.tabsApi.setPayAtTableEnabledOnStore(
            enable = true,
            onSuccess = { /* Persist that PAT is enabled for this store */ },
            onFailure = { failure -> Log.w("PAT", "Failed to enable PAT: ${failure.reason}") },
        )
        ```
    tab:
      title: Kotlin (coroutines)
      content:
        ```kotlin
        teyaPosLinkSDK.tabs.setPayAtTableEnabledOnStore(enable = true)
            .onOk { /* Persist that PAT is enabled for this store */ }
            .onErr { failure -> Log.w("PAT", "Failed to enable PAT: $failure") }
        ```
    tab:
      title: Java
      content:
        ```java
        teyaPosLinkSDK.getTabsApi().setPayAtTableEnabledOnStore(
                true,
                () -> Unit.INSTANCE,
                failure -> {
                    Log.w("PAT", "Failed to enable PAT: " + failure.getReason());
                    return Unit.INSTANCE;
                }
        );
        ```
```

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

```tabs
    tab:
      title: Kotlin (callbacks)
      content:
        ```kotlin
        val tabId = TabId("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 ->
                // tab.status == TabStatus.OPEN
                // tab.paymentRequests is null at creation (no payments yet)
            },
            onFailure = { failure -> Log.w("PAT", "openTab failed: ${failure.reason}") },
        )
        ```
    tab:
      title: Kotlin (coroutines)
      content:
        ```kotlin
        val tabId = TabId("ORDER-2026-001")

        teyaPosLinkSDK.tabs.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
        ).onOk { tab ->
            // tab.status == TabStatus.OPEN
            // tab.paymentRequests is null at creation (no payments yet)
        }.onErr { failure ->
            Log.w("PAT", "openTab failed: $failure")
        }
        ```
    tab:
      title: Java
      content:
        ```java
        TabId tabId = new TabId("ORDER-2026-001");

        teyaPosLinkSDK.getTabsApi().openTab(
                tabId,
                "Table 5",
                "GBP",
                tab -> Unit.INSTANCE,
                failure -> {
                    Log.w("PAT", "openTab failed: " + failure.getReason());
                    return Unit.INSTANCE;
                }
        );
        ```
```

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

---

## Subscribe to tab events

PAT is event-driven. `TabEventListener` is the listener interface; 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**.

In the coroutines API `tabEvents` is a `Flow<TabEvent>` you collect instead, where `TabEvent` is a sealed interface covering the same events. Collection stops when the collecting coroutine is cancelled, so there is no listener to unregister.

```tabs
    tab:
      title: Kotlin (callbacks)
      content:
        ```kotlin
        private val tabEventsListener = object : TabEventListener {

            override fun onShowBillRequested(request: TabBillRequest) {
                // See "Respond to bill requests"
            }

            override fun onPayRequested(request: TabPayRequest) {
                // See "Take tab payments"
            }

            override fun onPaymentCompleted(detail: TabPaymentDetail) {
                Log.d("PAT", "Payment ${detail.eposTransactionId} reached ${detail.status}")
            }

            override fun onTabCompleted(completion: TabCompletion) {
                // See "Close the tab"
            }

            override fun onConnectionStateChange(state: ConnectionState) {
                Log.d("PAT", "Tab event stream is now $state")
            }
        }

        teyaPosLinkSDK.tabsApi.tabEvents.subscribe(tabEventsListener)
        ```
    tab:
      title: Kotlin (coroutines)
      content:
        ```kotlin
        teyaPosLinkSDK.tabs.tabEvents.collect { event ->
            when (event) {
                is TabEvent.ShowBillRequested -> {
                    // See "Respond to bill requests" - event.request is a TabBillRequest
                }
                is TabEvent.PayRequested -> {
                    // See "Take tab payments" - event.request is a TabPayRequest
                }
                is TabEvent.PaymentCompleted -> {
                    Log.d("PAT", "Payment ${event.detail.eposTransactionId} reached ${event.detail.status}")
                }
                is TabEvent.TabCompleted -> {
                    // See "Close the tab" - event.completion is a TabCompletion
                }
                is TabEvent.ConnectionStateChanged -> {
                    Log.d("PAT", "Tab event stream is now ${event.state}")
                }
                else -> Unit
            }
        }
        ```
    tab:
      title: Java
      content:
        ```java
        TabEventListener tabEventsListener = new TabEventListener() {
            @Override
            public void onShowBillRequested(@NonNull TabBillRequest request) {
                // See "Respond to bill requests"
            }

            @Override
            public void onPayRequested(@NonNull TabPayRequest request) {
                // See "Take tab payments"
            }

            @Override
            public void onPaymentCompleted(@NonNull TabPaymentDetail detail) {
                Log.d("PAT", "Payment " + detail.getEposTransactionId() + " reached " + detail.getStatus());
            }

            @Override
            public void onTabCompleted(@NonNull TabCompletion completion) {
                // See "Close the tab"
            }

            @Override
            public void onConnectionStateChange(@NonNull ConnectionState state) {
                Log.d("PAT", "Tab event stream is now " + state);
            }
        };

        teyaPosLinkSDK.getTabsApi().getTabEvents().subscribe(tabEventsListener);
        ```
```

Subscribing for the first time opens the underlying SSE stream and emits `ConnectionState.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(listener)` and `unsubscribeAll()` detach them.

**All callbacks at a glance:**

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

---

## Respond to bill requests

The samples below run inside the `tabEvents` collector from the previous section: the callback tabs show the `TabEventListener` override, and the coroutines tabs show the body of the matching `TabEvent` branch, where `event` is the value being matched.

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.

```tabs
    tab:
      title: Kotlin (callbacks)
      content:
        ```kotlin
        override fun onShowBillRequested(request: TabBillRequest) {
            val bill = lookUpBillInEpos(request.tabId)
            if (bill == null) {
                teyaPosLinkSDK.tabsApi.respondWithError(
                    tabId = request.tabId,
                    terminalId = request.terminalId,
                    errorCode = TabErrorCode.TableNotFound,
                    message = "Tab no longer exists",
                    onSuccess = {}, onFailure = {},
                )
                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 = null,
                onSuccess = { /* Terminal will display (and optionally print) the bill */ },
                onFailure = { failure -> Log.w("PAT", "respondToBillRequest failed: ${failure.reason}") },
            )
        }
        ```
    tab:
      title: Kotlin (coroutines)
      content:
        ```kotlin
        val request = event.request
        val bill = lookUpBillInEpos(request.tabId)
        if (bill == null) {
            teyaPosLinkSDK.tabs.respondWithError(
                tabId = request.tabId,
                terminalId = request.terminalId,
                errorCode = TabErrorCode.TableNotFound,
                message = "Tab no longer exists",
            )
            return@collect
        }
        teyaPosLinkSDK.tabs.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 = null,
        ).onOk {
            // Terminal will display (and optionally print) the bill
        }.onErr { failure ->
            Log.w("PAT", "respondToBillRequest failed: $failure")
        }
        ```
    tab:
      title: Java
      content:
        ```java
        @Override
        public void onShowBillRequested(@NonNull TabBillRequest request) {
            Bill bill = lookUpBillInEpos(request.getTabId());
            if (bill == null) {
                teyaPosLinkSDK.getTabsApi().respondWithError(
                        request.getTabId(),
                        request.getTerminalId(),
                        TabErrorCode.TableNotFound,
                        "Tab no longer exists",
                        () -> Unit.INSTANCE,
                        failure -> Unit.INSTANCE
                );
                return;
            }
            teyaPosLinkSDK.getTabsApi().respondToBillRequest(
                    request.getTabId(),
                    request.getTerminalId(),               // echo from the event
                    bill.getTotalMinorUnits(),             // e.g. 5000 for £50.00
                    bill.getCurrency(),
                    bill.toReceiptTemplate(),              // OR pass billImage for a pre-rendered bill
                    null,                                  // billImage
                    () -> Unit.INSTANCE,                   // onSuccess
                    failure -> {
                        Log.w("PAT", "respondToBillRequest failed: " + failure.getReason());
                        return Unit.INSTANCE;
                    }
            );
        }
        ```
```

**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 platform bitmap. When `billImage` is supplied, `printModel` is ignored.

```tabs
    tab:
      title: Kotlin (callbacks)
      content:
        ```kotlin
        teyaPosLinkSDK.tabsApi.respondToBillRequest(
            tabId = request.tabId,
            terminalId = request.terminalId,
            totalAmount = bill.totalMinorUnits,
            currency = bill.currency,
            printModel = null,
            billImage = Image(BitmapFactory.decodeResource(resources, R.drawable.bill_image)),
            onSuccess = { },
            onFailure = { },
        )
        ```
    tab:
      title: Kotlin (coroutines)
      content:
        ```kotlin
        teyaPosLinkSDK.tabs.respondToBillRequest(
            tabId = request.tabId,
            terminalId = request.terminalId,
            totalAmount = bill.totalMinorUnits,
            currency = bill.currency,
            printModel = null,
            billImage = Image(BitmapFactory.decodeResource(resources, R.drawable.bill_image)),
        )
        ```
    tab:
      title: Java
      content:
        ```java
        teyaPosLinkSDK.getTabsApi().respondToBillRequest(
                request.getTabId(),
                request.getTerminalId(),
                bill.getTotalMinorUnits(),
                bill.getCurrency(),
                null,
                AndroidImageFactoryKt.Image(BitmapFactory.decodeResource(getResources(), R.drawable.bill_image)),
                () -> Unit.INSTANCE,
                failure -> Unit.INSTANCE
        );
        ```
```

**Bill limits**

Bills are rendered by the same pipeline as [custom printing](/epos-sdk/poslink/android/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 size for the bitmap is **20 MB**.
- The maximum height for the bitmap is **10000 px**.
- The recommended width is **384 px**. Wider bitmaps are scaled down to fit the printer width.
- The bitmap 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 `Image`. Limits detected by the backend are reported through `respondToBillRequest`'s `onFailure` as `UnexpectedError`, with details in `debugErrorMessage`.

**Error handling**

**`TabErrorCode` 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/android/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 `TabPaymentContext` 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`).

```tabs
    tab:
      title: Kotlin (callbacks)
      content:
        ```kotlin
        override fun onPayRequested(request: TabPayRequest) {
            val paymentSubscription = teyaPosLinkSDK.transactionsApi.makePayment(
                transactionId = UUID.randomUUID().toString(),
                amount = request.amount,
                currency = request.currency,
                tip = null,
                tabContext = request.tabContext,    // echo from the event
            )

            paymentSubscription.subscribe(
                TeyaPosLinkPaymentInProgressUi(autoDismissOnFinalStateAfterMs = 2000)
            )
        }
        ```
    tab:
      title: Kotlin (coroutines)
      content:
        ```kotlin
        val request = event.request
        val paymentHandle = teyaPosLinkSDK.transactions.makePayment(
            transactionId = UUID.randomUUID().toString(),
            amount = request.amount,
            currency = request.currency,
            tip = null,
            tabContext = request.tabContext,    // echo from the event
        )

        paymentHandle.showTeyaPaymentInProgressUi(autoDismissOnFinalStateAfter = 2.seconds)
        ```
    tab:
      title: Java
      content:
        ```java
        @Override
        public void onPayRequested(@NonNull TabPayRequest request) {
            PaymentStateSubscription paymentSubscription = teyaPosLinkSDK.getTransactionsApi().makePayment(
                    UUID.randomUUID().toString(), // transactionId
                    request.getAmount(),
                    request.getCurrency(),
                    null,                         // tip
                    request.getTabContext()       // echo from the event
            );

            paymentSubscription.subscribe(
                    TeyaPosLinkPaymentInProgressUi.create(2000L) // autoDismissOnFinalStateAfterMs
            );
        }
        ```
```

The returned `PaymentStateSubscription` (or `PaymentHandle` in the coroutines API) exposes the same lifecycle as regular payments (`Pending` → `New` → `In_Progress` → `Successful` / `Processing_Failed` / `Canceled` / `Communication_Failed`). For tab payments, `onPaymentProgress` and `onPaymentCompleted` on `TabEventListener` 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.

```tabs
    tab:
      title: Kotlin (callbacks)
      content:
        ```kotlin
        teyaPosLinkSDK.tabsApi.closeTab(
            tabId = TabId("ORDER-2026-001"),
            onSuccess = { /* archived */ },
            onFailure = { failure -> Log.w("PAT", "closeTab failed: ${failure.reason}") },
        )
        ```
    tab:
      title: Kotlin (coroutines)
      content:
        ```kotlin
        teyaPosLinkSDK.tabs.closeTab(tabId = TabId("ORDER-2026-001"))
            .onOk { /* archived */ }
            .onErr { failure -> Log.w("PAT", "closeTab failed: $failure") }
        ```
    tab:
      title: Java
      content:
        ```java
        teyaPosLinkSDK.getTabsApi().closeTab(
                new TabId("ORDER-2026-001"),
                () -> Unit.INSTANCE,    // onSuccess: archived
                failure -> {
                    Log.w("PAT", "closeTab failed: " + failure.getReason());
                    return Unit.INSTANCE;
                }
        );
        ```
```

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

---

## List and get tabs

```tabs
    tab:
      title: Kotlin (callbacks)
      content:
        ```kotlin
        // listTabs (cursor-paginated)
        teyaPosLinkSDK.tabsApi.listTabs(
            statuses = listOf(TabStatus.OPEN, TabStatus.PAYING, TabStatus.PAUSED), // null = active default
            after = null,
            before = null,
            limit = 50,
            onSuccess = { page ->
                page.items.forEach { /* TabSummary */ }
                val next = page.nextCursor // pass into `after` on the next call
            },
            onFailure = { /* ... */ },
        )

        // getTab (full detail including payment history)
        teyaPosLinkSDK.tabsApi.getTab(
            tabId = TabId("ORDER-2026-001"),
            onSuccess = { tab -> /* tab.paymentRequests lists every payment recorded against the tab */ },
            onFailure = { /* ... */ },
        )
        ```
    tab:
      title: Kotlin (coroutines)
      content:
        ```kotlin
        // listTabs (cursor-paginated)
        teyaPosLinkSDK.tabs.listTabs(
            statuses = listOf(TabStatus.OPEN, TabStatus.PAYING, TabStatus.PAUSED), // null = active default
            after = null,
            before = null,
            limit = 50,
        ).onOk { page ->
            page.items.forEach { /* TabSummary */ }
            val next = page.nextCursor // pass into `after` on the next call
        }.onErr { /* ... */ }

        // getTab (full detail including payment history)
        teyaPosLinkSDK.tabs.getTab(tabId = TabId("ORDER-2026-001"))
            .onOk { tab -> /* tab.paymentRequests lists every payment recorded against the tab */ }
            .onErr { /* ... */ }
        ```
    tab:
      title: Java
      content:
        ```java
        // listTabs (cursor-paginated)
        teyaPosLinkSDK.getTabsApi().listTabs(
                Arrays.asList(TabStatus.OPEN, TabStatus.PAYING, TabStatus.PAUSED), // statuses (null = active default)
                null, // after
                null, // before
                50,    // limit
                page -> {
                    for (TabSummary summary : page.getItems()) { /* ... */ }
                    String next = page.getNextCursor(); // pass into `after` on the next call
                    return Unit.INSTANCE;
                },
                failure -> Unit.INSTANCE
        );

        // getTab (full detail including payment history)
        teyaPosLinkSDK.getTabsApi().getTab(
                new TabId("ORDER-2026-001"),
                tab -> Unit.INSTANCE, // tab.getPaymentRequests() lists every payment recorded against the tab
                failure -> Unit.INSTANCE
        );
        ```
```

Field-level documentation for each type returned by `listTabs` and `getTab` lives in the auto-generated [API Reference](/kotlin-docs).
