---
title: Add a seat to a seated booking
description: Adds one attendee seat to an existing seated booking.
api_method: POST
api_path: "/v1/bookings/{uid}/seats"
canonical_url: https://wiblo.app/docs/developers/api/seats/create-booking-seat
last_updated: 2026-07-28T18:08:25+02:00
md_url: https://wiblo.app/docs/developers/api/seats/create-booking-seat.md
---

# Add a seat to a seated booking

`POST /v1/bookings/{uid}/seats`

Adds one attendee seat to an existing seated booking. The SECURITY DEFINER transaction locks the booking row, counts seats fresh inside the lock (no over-capacity race), and inserts the attendee + booking_seats pair together. An email already seated on the booking answers 200 with `duplicate: true` and the existing seat echoed — nothing is written (the create-surface idempotency doctrine). At capacity the answer is 409 `BOOKING_SEATS_FULL`. Owners and admins only (seats sell through the create surface). Per-user rate-limited at 30/60s.

## Path parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `uid` | `string` | Yes |  |

## Headers

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `X-Wiblo-Workspace` | `string` | Yes | Workspace id the call is scoped to. The actor must hold an active membership in it; a foreign or unknown id collapses onto 404 WORKSPACE_NOT_FOUND. |

## Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `attendee` | `object` | Yes | The person taking the seat. |
| `attendee.name` | `string` | Yes | Attendee's display name (2–255 characters). |
| `attendee.email` | `string` | Yes | Attendee's email address. Also the attendee's idempotency identity — the same email retrying the same slot or seat dedupes. |
| `attendee.timezone` | `string` | Yes | Attendee's IANA time zone, e.g. `Europe/London`; unknown zones are rejected. |
| `attendee.phone_number` | `string` | No | Attendee's phone number (max 50 characters). |

## Request

**curl**

```bash
curl https://api.wiblo.app/v1/bookings/f2b8e5c1-9d4a-4c7f-b6e3-1a8d5f2c9b7e/seats \
  -X POST \
  -H "Authorization: Bearer $WIBLO_TOKEN" \
  -H "X-Wiblo-Workspace: $WIBLO_WORKSPACE_ID" \
  -H "Content-Type: application/json" \
  -d '{
  "attendee": {
    "name": "Maya Lindqvist",
    "email": "maya@example.com",
    "timezone": "Europe/London",
    "phone_number": "+44 7700 900123"
  }
}'
```

**TypeScript**

```ts
import { createSdk, createBookingSeat } from "@workspace/sdk"

const sdk = createSdk({ baseUrl: "https://api.wiblo.app" })

const { data, error } = await createBookingSeat({
  client: sdk,
  path: { uid: "f2b8e5c1-9d4a-4c7f-b6e3-1a8d5f2c9b7e" },
  headers: { "X-Wiblo-Workspace": "..." },
  body: {
    "attendee": {
      "name": "Maya Lindqvist",
      "email": "maya@example.com",
      "timezone": "Europe/London",
      "phone_number": "+44 7700 900123"
    }
  },
})
```

## Example response

```json
{
  "seat": {
    "seat_id": "a3f6d9c2-7e4b-4a1d-8c5f-2b9e6a3d7f4c",
    "reference_uid": "c8e2b5f9-3d7a-4f6c-9b1e-4a7d2c8f5b3e",
    "booking_id": "e4a7c2d9-6b1f-4d8e-9a3c-5f2b8d7e4c1a",
    "booking_uid": "f2b8e5c1-9d4a-4c7f-b6e3-1a8d5f2c9b7e",
    "attendee_id": "d1c4f7a3-8b2e-4d5a-a9c6-3e8f1b4d7a2c",
    "seats_taken": 6,
    "seats_total": 8,
    "duplicate": false,
    "message": "Seat added"
  }
}
```

## Responses

**`200`** — Idempotent replay — this attendee already holds a seat on the booking (`duplicate: true`); nothing was written. Returns `AddSeatResponse`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `seat` | `object` | Yes | The created (or echoed) seat. |
| `seat.seat_id` | `string` | Yes | Unique id of the seat row. |
| `seat.reference_uid` | `string` | Yes | The seat's own public handle — per-seat cancel keys off it. |
| `seat.booking_id` | `string` | Yes | Id of the booking the seat is on. |
| `seat.booking_uid` | `string` | Yes | Public uid of the booking the seat is on. |
| `seat.attendee_id` | `string` | Yes | Id of the attendee holding the seat. |
| `seat.seats_taken` | `integer` | Yes | Seats taken on the booking after this call, including this seat. |
| `seat.seats_total` | `integer` | Yes | Total seat capacity — the service's `seats_per_time_slot`. |
| `seat.duplicate` | `boolean` | Yes | `true` when this attendee already held a seat on the booking — the existing seat is echoed and nothing was written. |
| `seat.message` | `string` | Yes | Human-readable outcome message. |

**`201`** — The seat was added: its booking_seats row and attendee landed in one transaction. Returns `AddSeatResponse`.

**`400`** — Body failed Zod validation or the `X-Wiblo-Workspace` header was missing (`VALIDATION_FAILED`), the booking's service is not seated, or the booking is not in a bookable state (`BAD_REQUEST`). Returns `ApiErrorEnvelope`.

**`401`** — No valid session cookie or `wbl_*` bearer was present. Returns `ApiErrorEnvelope`.

**`403`** — The actor is a plain member (`ADMIN_REQUIRED` — owners and admins only). Returns `ApiErrorEnvelope`.

**`404`** — No booking with this uid in this workspace (`BOOKING_NOT_FOUND` — cross-workspace uids collapse here) or the workspace collapsed (`WORKSPACE_NOT_FOUND`). Returns `ApiErrorEnvelope`.

**`409`** — Every seat on the slot is taken (`BOOKING_SEATS_FULL`). Returns `ApiErrorEnvelope`.

**`429`** — Rate limit exceeded. The body's `error.code` is `RATE_LIMITED` and `error.details.retry_after` is the same number of seconds as the `Retry-After` header. Returns `ApiErrorEnvelope`.
