# Assigned seating

Tickets for specific seats: a tier with seats, the seat map as a CSV file, and ordering a seat by its label or your own ID.

A seat always belongs to one tier, that is, one price category. One event can mix tiers with seats and tiers with a quota: for example, stalls by seat and a dance floor without seats. A seat sells once, even when two people buy it in the same second.

## Set up an event with seats {#setup}

1. [Create an event](https://budu.events/en/docs/events/#create) with a `"seated": true` tier. Don't pass a quota for such a tier: it equals the number of its seats.
2. Upload the seats as a CSV file: [`POST /v1/events/{id}/seats`](#import). You can check the file first with `?dry_run=1`.
3. [Publish the event](https://budu.events/en/docs/events/#publish). A tier with seats can't be published without its seats.

Seats change only on a draft. An upload replaces every seat of the event.

## CSV format {#csv}

```csv
sector,row,number,label,external_id,tariff
Партер,1,1,П-1-1,hall-0001,parter
Партер,1,2,,hall-0002,parter
"Балкон, левая сторона",1,1,Б-1-1,hall-0101,balcony
```

| Column | Required | Description |
|---|---|---|
| `sector` | yes | Sector, up to 50 characters |
| `row` | yes | Row, up to 20 characters |
| `number` | yes | Seat number, up to 20 characters |
| `label` | no | Seat label, unique within the event, up to 100 characters. Empty means `sector-row-number`, in the example's second line `Партер-1-2` |
| `external_id` | no | Your own seat ID, for example from the hall plan. Unique within the event and never equal to any label |
| `tariff` | if there are several tiers with seats | Code or name of the tier with seats |

- The first line is the header. Case and spaces around names don't matter, columns go in any order. Any other column is an error.
- The separator is `,` or `;` (Excel with Russian settings saves with `;`). RFC 4180 quotes, a BOM and Windows line breaks are fine.
- UTF-8 only. In Excel, save as "CSV UTF-8".
- Empty lines are skipped. Control characters inside values are not allowed.
- Line order is the order of seats in the hall: lists and availability return seats in it.
- Up to 50,000 seats and 5 MB per file.

## Upload seats {#import}

`POST /v1/events/{id}/seats`

The request body is the file itself, with `Content-Type: text/csv`. The file is accepted whole or not at all. `?dry_run=1` checks and counts the seats without writing anything.

```bash Request
curl "https://api.budu.events/v1/events/0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60/seats?dry_run=1" \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @hall.csv
```

```json Response 200
{
  "total": 3,
  "per_tariff": [
    {"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c73", "count": 2},
    {"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c74", "count": 1}
  ],
  "errors": [],
  "sample": [
    {
      "id": "0193e0b3-0a11-7f00-8d2b-41c6e9a7b001",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c73",
      "sector": "Партер",
      "row": "1",
      "number": "1",
      "label": "П-1-1",
      "external_id": "hall-0001",
      "position": 1
    }
  ]
}
```

`sample` holds the first 20 seats for a preview. If the file has errors, the response is `400 invalid_request`, nothing is written, and the errors are in `details.errors`:

```json Response 400
{
  "error": {
    "code": "invalid_request",
    "message": "В файле мест есть ошибки — места не загружены",
    "details": {
      "errors": [
        {"line": 3, "message": "Место «П-1-1» повторяется: строки 2 и 3"}
      ]
    }
  }
}
```

`line` is the file's line number, with the header as line 1. `line: 0` refers to the file as a whole. The response holds up to 100 errors.

You don't need an `Idempotency-Key` here: repeating the same file gives the same result. After publishing, an upload answers `400` «Места можно загрузить только до публикации» (seats can be uploaded only before publishing).

## List seats {#list}

`GET /v1/events/{id}/seats`

| Parameter | Description |
|---|---|
| `limit` | Seats per page, 1 to 5,000, 1,000 by default |
| `after` | `id` of the last seat on the previous page |
| `sector` | Only this sector |

```bash Request
curl "https://api.budu.events/v1/events/0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60/seats?limit=2" \
  -H "Authorization: Bearer $BUDU_KEY"
```

```json Response 200
{
  "seats": [
    {
      "id": "0193e0b3-0a11-7f00-8d2b-41c6e9a7b001",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c73",
      "sector": "Партер",
      "row": "1",
      "number": "1",
      "label": "П-1-1",
      "external_id": "hall-0001",
      "position": 1
    },
    {
      "id": "0193e0b3-0a11-7f00-8d2b-41c6e9a7b002",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c73",
      "sector": "Партер",
      "row": "1",
      "number": "2",
      "label": "Партер-1-2",
      "external_id": "hall-0002",
      "position": 2
    }
  ],
  "last": false,
  "sectors": ["Партер", "Балкон, левая сторона"]
}
```

Seats come in file order. For the next page pass `after=<id of the last seat>` until `last` turns `true`. `sectors` comes on the first page only.

## Seat states {#states}

[`GET /v1/events/{id}/availability?seats=1`](https://budu.events/en/docs/events/#availability) adds a page of seat states to availability: `seats: [{seat_id, tariff_id, sector, label, state}]` and `seats_last`. You page through them the same way, with the `seats_after` and `seats_limit` parameters.

| `state` | Meaning |
|---|---|
| `free` | Free |
| `held` | Held by an order's reservation. Frees up if the reservation is cancelled or expires |
| `sold` | Sold. Turns `free` again if the ticket is cancelled and returned to sale |

## Order a seat {#order}

In [`POST /v1/orders`](https://budu.events/en/docs/orders/#create) and [`POST /v1/reservations`](https://budu.events/en/docs/orders/#reserve) an item with a seat looks like `{"seat": "<reference>"}` instead of `{"tariff_id", "qty"}`:

```json
{
  "event_id": "0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60",
  "items": [
    {"seat": "П-1-1"},
    {"seat": "hall-0002"},
    {"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71", "qty": 2}
  ],
  "buyer": {
    "email": "anna@example.ru",
    "first_name": "Анна",
    "last_name": "Петрова"
  }
}
```

- A seat reference is its `id`, `external_id` or `label`. We check them in that order.
- A seat's `qty` is `0` or `1`. You can leave out `tariff_id`, and if you give it, it must match the seat's tier.
- A tier with seats can't be ordered by quantity: `400` «Для этого тарифа выберите места» (choose seats for this tier).
- A seat counts as one ticket toward the limit of 20 per order and the tier's `per_order_limit`.
- In orders and tickets a seat comes as a `seat: {id, sector, row, number, label}` object. The guest sees "Sector · Row · Seat" on the ticket, in the email and in the calendar entry.

Errors: `409 seat_taken` if another order holds the seat or it's sold (`details.seat_id`, `details.seat`). `404 not_found` if the event has no such seat (`details.seat`). `409 event_not_on_sale` with «Продажа мест ещё не открыта» (seat sales aren't open yet) if only a few seconds have passed since publishing and the seats are still being loaded into sales.
