# Tables

Restaurant table management

## GET /api/v1/locations/{locationId}/staff

List staff for a location

List retailer_users with access to a location. Powers the POS table-map Staff sidebar. Returns a minimal projection (id, name, color).

**Permissions:** 🔵 `tables.read`

Path parameters:
- `locationId` (string, required) — Location ID

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    staff: Array<{
        id: number
        name?: string | null
        color?: string | null
      }>
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## PUT /api/v1/retailer-users/{retailerUserId}/color

Update a staff member's color

Set or clear the hex color used on the POS table-map staff badge. The target retailer_user must have access to the caller's active location.

**Permissions:** 🟠 `tables.update`

Path parameters:
- `retailerUserId` (string, required) — Retailer user ID

Request body:

```ts
{
  color: string | null
}
```

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    id: number
    name?: string | null
    color?: string | null
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## GET /api/v1/tables

List tables

List tables with optional filtering

**Permissions:** 🔵 `tables.read`

Query parameters:
- `locationId` (number, optional) — Filter by location ID
- `status` ("live" | "paused" | "deleted", optional) — Filter by status
- `tableGroupId` (number, optional) — Filter by table group ID
- `search` (string, optional) — Search by name or description
- `page` (number, optional) — Page number
- `limit` (number, optional) — Items per page

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    tables: Array<{
        customFields?: unknown[]
        metadata?: { … } | null
        id: string // uuid
        name: string
        description?: string | null
        contextParams: { … }
        locationId: number
        storefrontUrl?: string | null // uri
        tableGroupId?: number | null
        costCenterId?: number | null
        status: "live" | "paused" | "deleted"
        qrCodeImageUrl?: string | null // uri
        activeTabs: unknown[]
        totalOrderAmountOnActiveTables?: number
        totalPaidAmountOnActiveTables?: number
        isBookable: boolean
        minCapacity?: number | null
        maxCapacity?: number | null
        seatingDurationMinutes?: number | null
        serviceFeeOverride?: { … } | null
        newTabPerGuest?: boolean
        requireGuestCount?: boolean
        assignedRetailerUser?: { … } | null
        externalId?: string | null
        createdAt: string // date-time
        updatedAt?: string | null // date-time
      }>
    pagination: {
      page: number
      limit: number
      total: number
      totalPages: number
    }
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## POST /api/v1/tables

Create table

Create a new table

**Permissions:** 🟢 `tables.create`

Custom fields: send customFields: [{ "key": "your_field_key", "value": "your_value", "expectedVersion": 0 }]. Use either fieldId or a stable key. expectedVersion is optional; supply the last-read version to reject stale edits. Omitted fields are unchanged; null clears an optional value. See [Custom field inputs](/docs/custom-fields#values-on-normal-create-and-update-calls).

Request body:

```ts
{
  name: string
  description?: string
  locationId: number
  tableGroupId?: number
  costCenterId?: number | null
  contextParams?: Record<string, unknown>
  customFields?: unknown | unknown[]
}
```

Response 201 — Default Response

```ts
{
  status: "success"
  data: {
    customFields?: Array<{
        fieldId?: string
        key?: string
        label?: string
        group?: string
        type?: string
        displayValue?: string
        value?: unknown // The stored value; its JSON type follows the field type
        version?: integer
        status?: string
      }>
    metadata?: {
      customFields?: Record<string, unknown>
    } | null
    id: string // uuid
    name: string
    description?: string | null
    contextParams: Record<string, unknown>
    locationId: number
    storefrontUrl?: string | null // uri
    tableGroupId?: number | null
    costCenterId?: number | null
    status: "live" | "paused" | "deleted"
    qrCodeImageUrl?: string | null // uri
    activeTabs: Array<{
        customFields?: unknown[]
        metadata?: { … } | null
        id: string
        state: string
        orderTotal?: number
        paidTotal?: number
        tipsTotal?: number
        selectedPaymentFlow?: string | null
        coursingOrder?: number | null
        memberCount?: number
        totalAmountCents?: number
        paidAmountCents?: number
        remainingBalanceCents?: number
        paymentMode?: "ITEM" | "AMOUNT"
        storefrontQrCodeId?: string | null // uuid
        createdAt?: string // date-time
        updatedAt?: string | null // date-time
        extras?: unknown[]
        kitchenDispatches?: unknown[]
        notes?: unknown[]
        showNotesOnMap?: boolean
        tableDisplayName?: string | null
        preCheckPrintedAt?: string | null // date-time
      }>
    totalOrderAmountOnActiveTables?: number
    totalPaidAmountOnActiveTables?: number
    isBookable: boolean
    minCapacity?: number | null
    maxCapacity?: number | null
    seatingDurationMinutes?: number | null
    serviceFeeOverride?: {
      enabled?: boolean
      title?: string | null
      description?: string | null
      type?: "fixed" | "percentage"
      fixedCents?: number | null
      percentageBps?: number | null
      vatId?: number | null
    } | null
    newTabPerGuest?: boolean
    requireGuestCount?: boolean
    assignedRetailerUser?: {
      id: number
      name?: string | null
      color?: string | null
    } | null
    externalId?: string | null
    createdAt: string // date-time
    updatedAt?: string | null // date-time
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## GET /api/v1/tables/{id}

Get table details

Get a single table by ID with active tabs

**Permissions:** 🔵 `tables.read`

Path parameters:
- `id` (string, required) — Table UUID

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    customFields?: Array<{
        fieldId?: string
        key?: string
        label?: string
        group?: string
        type?: string
        displayValue?: string
        value?: unknown // The stored value; its JSON type follows the field type
        version?: integer
        status?: string
      }>
    metadata?: {
      customFields?: Record<string, unknown>
    } | null
    id: string // uuid
    name: string
    description?: string | null
    contextParams: Record<string, unknown>
    locationId: number
    storefrontUrl?: string | null // uri
    tableGroupId?: number | null
    costCenterId?: number | null
    status: "live" | "paused" | "deleted"
    qrCodeImageUrl?: string | null // uri
    activeTabs: Array<{
        customFields?: unknown[]
        metadata?: { … } | null
        id: string
        state: string
        orderTotal?: number
        paidTotal?: number
        tipsTotal?: number
        selectedPaymentFlow?: string | null
        coursingOrder?: number | null
        memberCount?: number
        totalAmountCents?: number
        paidAmountCents?: number
        remainingBalanceCents?: number
        paymentMode?: "ITEM" | "AMOUNT"
        storefrontQrCodeId?: string | null // uuid
        createdAt?: string // date-time
        updatedAt?: string | null // date-time
        extras?: unknown[]
        kitchenDispatches?: unknown[]
        notes?: unknown[]
        showNotesOnMap?: boolean
        tableDisplayName?: string | null
        preCheckPrintedAt?: string | null // date-time
      }>
    totalOrderAmountOnActiveTables?: number
    totalPaidAmountOnActiveTables?: number
    isBookable: boolean
    minCapacity?: number | null
    maxCapacity?: number | null
    seatingDurationMinutes?: number | null
    serviceFeeOverride?: {
      enabled?: boolean
      title?: string | null
      description?: string | null
      type?: "fixed" | "percentage"
      fixedCents?: number | null
      percentageBps?: number | null
      vatId?: number | null
    } | null
    newTabPerGuest?: boolean
    requireGuestCount?: boolean
    assignedRetailerUser?: {
      id: number
      name?: string | null
      color?: string | null
    } | null
    externalId?: string | null
    createdAt: string // date-time
    updatedAt?: string | null // date-time
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## PUT /api/v1/tables/{id}

Update table

Update an existing table

**Permissions:** 🟠 `tables.update`

Custom fields: send customFields: [{ "key": "your_field_key", "value": "your_value", "expectedVersion": 0 }]. Use either fieldId or a stable key. expectedVersion is optional; supply the last-read version to reject stale edits. Omitted fields are unchanged; null clears an optional value. See [Custom field inputs](/docs/custom-fields#values-on-normal-create-and-update-calls).

Path parameters:
- `id` (string, required) — Table UUID

Request body:

```ts
{
  name?: string
  description?: string | null
  tableGroupId?: number | null
  costCenterId?: number | null
  status?: "live" | "paused" | "deleted"
  contextParams?: Record<string, unknown>
  isBookable?: boolean
  minCapacity?: number | null
  maxCapacity?: number | null
  seatingDurationMinutes?: number | null
  serviceFeeOverride?: {
    enabled?: boolean
    title?: string | null
    description?: string | null
    type?: "fixed" | "percentage"
    fixedCents?: number | null
    percentageBps?: number | null
    vatId?: number | null
  } | null
  newTabPerGuest?: boolean
  requireGuestCount?: boolean
  assignedRetailerUserId?: number | null
  externalId?: string | null
  customFields?: unknown | unknown[]
}
```

Response 200 — Default Response

```ts
{
  status: "success"
  data: {
    customFields?: Array<{
        fieldId?: string
        key?: string
        label?: string
        group?: string
        type?: string
        displayValue?: string
        value?: unknown // The stored value; its JSON type follows the field type
        version?: integer
        status?: string
      }>
    metadata?: {
      customFields?: Record<string, unknown>
    } | null
    id: string // uuid
    name: string
    description?: string | null
    contextParams: Record<string, unknown>
    locationId: number
    storefrontUrl?: string | null // uri
    tableGroupId?: number | null
    costCenterId?: number | null
    status: "live" | "paused" | "deleted"
    qrCodeImageUrl?: string | null // uri
    activeTabs: Array<{
        customFields?: unknown[]
        metadata?: { … } | null
        id: string
        state: string
        orderTotal?: number
        paidTotal?: number
        tipsTotal?: number
        selectedPaymentFlow?: string | null
        coursingOrder?: number | null
        memberCount?: number
        totalAmountCents?: number
        paidAmountCents?: number
        remainingBalanceCents?: number
        paymentMode?: "ITEM" | "AMOUNT"
        storefrontQrCodeId?: string | null // uuid
        createdAt?: string // date-time
        updatedAt?: string | null // date-time
        extras?: unknown[]
        kitchenDispatches?: unknown[]
        notes?: unknown[]
        showNotesOnMap?: boolean
        tableDisplayName?: string | null
        preCheckPrintedAt?: string | null // date-time
      }>
    totalOrderAmountOnActiveTables?: number
    totalPaidAmountOnActiveTables?: number
    isBookable: boolean
    minCapacity?: number | null
    maxCapacity?: number | null
    seatingDurationMinutes?: number | null
    serviceFeeOverride?: {
      enabled?: boolean
      title?: string | null
      description?: string | null
      type?: "fixed" | "percentage"
      fixedCents?: number | null
      percentageBps?: number | null
      vatId?: number | null
    } | null
    newTabPerGuest?: boolean
    requireGuestCount?: boolean
    assignedRetailerUser?: {
      id: number
      name?: string | null
      color?: string | null
    } | null
    externalId?: string | null
    createdAt: string // date-time
    updatedAt?: string | null // date-time
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## DELETE /api/v1/tables/{id}

Delete table

Delete a table (soft delete - sets status to deleted)

**Permissions:** 🔴 `tables.delete`

Path parameters:
- `id` (string, required) — Table UUID

Response 200 — Table successfully deleted

```ts
{
  status?: "success"
  data?: {
    deleted?: boolean
  }
  meta?: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## GET /api/v1/tables/{id}/tab-history

Get table tab history

Lists tabs currently or previously associated with a table, including move/merge lineage and the staff actor.

**Permissions:** 🔵 `tables.read`

Query parameters:
- `page` (integer, optional)
- `limit` (integer, optional)

Path parameters:
- `id` (string, required)

Response 200 — Table tab history

```ts
{
  status: "success"
  data: {
    tabs: Array<{
        uuid: string // uuid
        locationId: number
        latestState: string
        selectedPaymentFlow: string | null
        paymentMode: string
        totalAmountCents: number
        paidAmountCents: number
        remainingBalanceCents: number
        storefrontQrCodeId: string | null
        createdAt: string // date-time
        updatedAt: string // date-time
        association: { … }
      }>
    pagination: {
      page: number
      limit: number
      total: number
      totalPages: number
    }
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## PUT /api/v1/tables/bulk

Bulk update tables

Update multiple tables in a single request. Apply the same changes to all specified tables.

**Permissions:** 🟠 `tables.update`

Custom fields: send customFields: [{ "key": "your_field_key", "value": "your_value", "expectedVersion": 0 }]. Use either fieldId or a stable key. expectedVersion is optional; supply the last-read version to reject stale edits. Omitted fields are unchanged; null clears an optional value. See [Custom field inputs](/docs/custom-fields#values-on-normal-create-and-update-calls).

Request body:

```ts
{
  tableIds: string[]
  tableGroupId?: number | null
  name?: string
  description?: string
  costCenterId?: number | null
  contextParamsToMerge?: Record<string, unknown>
  contextParamsToDelete?: string[]
  status?: "live" | "paused" | "deleted"
  isBookable?: boolean
  minCapacity?: number | null
  maxCapacity?: number | null
  seatingDurationMinutes?: number | null
  serviceFeeOverride?: {
    enabled?: boolean
    title?: string | null
    description?: string | null
    type?: "fixed" | "percentage"
    fixedCents?: number | null
    percentageBps?: number | null
    vatId?: number | null
  } | null
  newTabPerGuest?: boolean
  requireGuestCount?: boolean
  assignedRetailerUserId?: number | null
  externalId?: string | null
  customFields?: unknown | unknown[]
}
```

Response 200 — Tables updated successfully

```ts
{
  status?: "success"
  data?: Array<{
      customFields?: { … }[]
      metadata?: {
        customFields?: { … }
      } | null
      id: string // uuid
      name: string
      description?: string | null
      contextParams: Record<string, unknown>
      locationId: number
      storefrontUrl?: string | null // uri
      tableGroupId?: number | null
      costCenterId?: number | null
      status: "live" | "paused" | "deleted"
      qrCodeImageUrl?: string | null // uri
      activeTabs: { … }[]
      totalOrderAmountOnActiveTables?: number
      totalPaidAmountOnActiveTables?: number
      isBookable: boolean
      minCapacity?: number | null
      maxCapacity?: number | null
      seatingDurationMinutes?: number | null
      serviceFeeOverride?: {
        enabled?: boolean
        title?: string | null
        description?: string | null
        type?: "fixed" | "percentage"
        fixedCents?: number | null
        percentageBps?: number | null
        vatId?: number | null
      } | null
      newTabPerGuest?: boolean
      requireGuestCount?: boolean
      assignedRetailerUser?: {
        id: number
        name?: string | null
        color?: string | null
      } | null
      externalId?: string | null
      createdAt: string // date-time
      updatedAt?: string | null // date-time
    }>
  meta?: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```

## DELETE /api/v1/tables/bulk

Bulk delete tables

Soft delete multiple tables from one location in a single request. Retrying the same request is safe.

**Permissions:** 🔴 `tables.delete`

Request body:

```ts
{
  tableIds: string[]
}
```

Response 200 — Tables successfully deleted

```ts
{
  status: "success"
  data: {
    ids: string[]
    count: number
    deleted: boolean
  }
  meta: {
    requestId: string
    timestamp: string // date-time
    executionTimeMs: number
  }
}
```
