# Заказы

Заказ выпускает билеты. Отправьте его одним запросом после оплаты или придержите билеты резервом на время оплаты.

Есть два способа продать билет.

- **Один запрос после оплаты.** Покупатель оплатил у вас, сайт отправляет [`POST /v1/orders`](#create) и получает билеты. Подходит, если билетов с запасом и вы готовы вернуть деньги, когда билеты закончатся за время оплаты.
- **Резерв на время оплаты.** Сайт резервирует билеты до оплаты ([`POST /v1/reservations`](#reserve)), покупатель платит, сайт подтверждает резерв ([`POST /v1/reservations/{id}/confirm`](#confirm)). Если оплата не прошла, резерв отменяют ([`DELETE /v1/reservations/{id}`](#cancel)) или он истекает сам. Так продают места в зале и тарифы, которые могут закончиться.

В обоих случаях билеты выпускаются, а письмо гостю уходит сразу после подтверждения.

## Объект заказа {#order-object}

| Поле | Тип | Описание |
|---|---|---|
| `id` | string | UUID заказа |
| `org_id` | string | UUID организации |
| `livemode` | boolean | Боевой заказ или тестовый |
| `event_id` | string | UUID события |
| `status` | string | Статус, см. [ниже](#statuses) |
| `buyer` | object | Покупатель: `email`, `first_name`, `last_name`, `phone`. Он же владелец билетов |
| `items` | array | Состав: `{tariff_id, qty, seat}`. Билеты без мест сгруппированы по тарифу, каждое место отдельной позицией с `qty: 1` и `seat` (id места) |
| `hold_expires_at` | string | До какого момента держится резерв, RFC 3339 |
| `payment_ref` | string | Ваш номер платежа, если передали |
| `tickets` | array | Билеты: `{id, url, tariff_id, holder_first_name, holder_last_name, status, seat}`. Пусто, пока билеты не выпущены |
| `seats` | array | Места заказа `{id, sector, row, number, label}` в порядке позиций |
| `reason` | string | Почему заказ нельзя подтвердить. `seat_taken`: место ушло другому заказу, деньги нужно вернуть |
| `source` | string | Откуда заказ: `api`, `manual` (выдан вручную) или `import` (из списка гостей) |

`tickets[].url` — ссылка на страницу билета: `https://budu.events/t/…`. Её можно сразу показать покупателю. Подробнее в разделе [билеты](https://budu.events/docs/tickets/#links).

## Статусы {#statuses}

| Статус | Что значит |
|---|---|
| `reserved` | Билеты в резерве до `hold_expires_at`, оплаты ещё нет |
| `confirming` | Подтверждение идёт. Отменить заказ уже нельзя |
| `paid` | Продажа зафиксирована, билеты выпускаются. Обычно это доли секунды |
| `issued` | Билеты выпущены, письмо отправлено |
| `expired` | Резерв истёк. Подтверждение ещё возможно, если билеты остались |
| `cancelled` | Резерв отменён |
| `failed` | Подтвердить не удалось: состав не совпал с резервом. Создайте новый заказ |

Заказ в `paid` или `issued` больше не становится `expired` или `cancelled`. Отменить проданное можно только по билету: [`POST /v1/tickets/{id}/cancel`](https://budu.events/docs/tickets/#cancel).

## Создать заказ {#create}

`POST /v1/orders`

Резерв и подтверждение одним запросом. Отправляйте после оплаты.

| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
| `event_id` | string | да | UUID опубликованного события |
| `items` | array | да | Позиции: `{"tariff_id", "qty"}` или `{"seat"}` для [места](https://budu.events/docs/seats/#order). Всего до 20 билетов |
| `buyer.email` | string | да | Почта: на неё уйдёт письмо с билетами |
| `buyer.first_name`, `buyer.last_name` | string | да | Имя и фамилия: они напечатаны на билете |
| `buyer.phone` | string | нет | Телефон, хранится в заказе |
| `payment_ref` | string | нет | Ваш номер платежа, вернётся в заказе |

```bash Запрос
curl https://api.budu.events/v1/orders \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Idempotency-Key: order-1042" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60",
    "items": [{"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71", "qty": 2}],
    "buyer": {
      "email": "maria@example.ru",
      "first_name": "Мария",
      "last_name": "Соколова"
    },
    "payment_ref": "pay-77812"
  }'
```

```json Ответ 201
{
  "id": "0193e0c5-1f20-7c3e-b6a1-9d4e2f7a8b10",
  "org_id": "0193d9a1-2b3c-7d4e-8f50-61728394a5b6",
  "livemode": false,
  "event_id": "0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60",
  "status": "issued",
  "buyer": {
    "email": "maria@example.ru",
    "first_name": "Мария",
    "last_name": "Соколова",
    "phone": ""
  },
  "items": [
    {"tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71", "qty": 2, "seat": ""}
  ],
  "hold_expires_at": "2026-10-22T12:15:04Z",
  "payment_ref": "pay-77812",
  "tickets": [
    {
      "id": "0193e0c5-1f6b-7e02-a3c9-5b8d1e4f9f0a",
      "url": "https://budu.events/t/Vw3kqP7nX2bLmR9tYc4sHa",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71",
      "holder_first_name": "Мария",
      "holder_last_name": "Соколова",
      "status": "valid",
      "seat": null
    },
    {
      "id": "0193e0c5-1f6b-7e02-a3c9-5b8d1e4fa21c",
      "url": "https://budu.events/t/9pLmZ2xQw8RkT4vNb6CyJe",
      "tariff_id": "0193e0b2-4a7d-7a21-8c44-6e2f9a0b3c71",
      "holder_first_name": "Мария",
      "holder_last_name": "Соколова",
      "status": "valid",
      "seat": null
    }
  ],
  "seats": [],
  "reason": "",
  "source": "api"
}
```

Если билеты не успели выпуститься за пару секунд, ответ будет `202`:

```json Ответ 202
{"status": "processing", "order_id": "0193e0c5-1f20-7c3e-b6a1-9d4e2f7a8b10"}
```

Продажа уже зафиксирована, билеты выпустятся сами, письмо уйдёт как обычно. Ссылки на билеты появятся в [`GET /v1/orders/{id}`](#get): читайте заказ раз в секунду, пока статус не станет `issued`.

| Ошибка | Когда |
|---|---|
| `400 invalid_request` | Нет покупателя, неверная почта, нет имени или фамилии, пустой состав, нет такого тарифа (`details.tariff_id`) |
| `404 not_found` | Нет события или места |
| `409 event_not_on_sale` | Событие не опубликовано, или продажа ещё открывается после публикации |
| `409 sold_out` | Билеты тарифа закончились (`details.tariff_id`) |
| `409 seat_taken` | Место уже занято (`details.seat_id`, `details.seat`) |
| `422 limit_exceeded` | Больше 20 билетов или больше `per_order_limit` тарифа (`details.tariff_id`) |
| `500 internal` с `details.order_id` | Связь с хранилищем остатков оборвалась посреди продажи. Заказ мы завершим сами: проверьте его через `GET /v1/orders/{id}` |

## Зарезервировать {#reserve}

`POST /v1/reservations`

Те же поля, что у [заказа](#create), и ещё одно:

| Поле | Тип | Обязательно | Описание |
|---|---|---|---|
| `hold_ttl_seconds` | integer | нет | Сколько держать резерв, от 60 до 1 800 секунд. По умолчанию 900, то есть 15 минут |

`payment_ref` в резерве не нужен: его передают при подтверждении.

```bash Запрос
curl https://api.budu.events/v1/reservations \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Idempotency-Key: cart-5531" \
  -H "Content-Type: application/json" \
  -d '{
    "event_id": "0193e0b2-4a7c-7d10-9e3f-2c5a8b1d4e60",
    "items": [{"seat": "П-1-1"}, {"seat": "П-1-2"}],
    "buyer": {
      "email": "anna@example.ru",
      "first_name": "Анна",
      "last_name": "Петрова"
    },
    "hold_ttl_seconds": 600
  }'
```

Ответ `201` с [заказом](#order-object) в статусе `reserved` и `hold_expires_at`. Пока резерв жив, эти билеты и места никто другой не купит. Ошибки те же, что у заказа.

## Подтвердить резерв {#confirm}

`POST /v1/reservations/{id}/confirm`

Вызывайте после оплаты. Тело необязательно: `{"payment_ref": "pay-77813"}`.

```bash Запрос
curl https://api.budu.events/v1/reservations/0193e0c5-2a40-7b11-9c3d-4e5f6a7b8c90/confirm \
  -H "Authorization: Bearer $BUDU_KEY" \
  -H "Idempotency-Key: pay-77813" \
  -H "Content-Type: application/json" \
  -d '{"payment_ref": "pay-77813"}'
```

| Ситуация | Ответ |
|---|---|
| Резерв жив, билеты выпущены | `200`, заказ в статусе `issued` с билетами |
| Продажа зафиксирована, билеты ещё выпускаются | `202 {"status": "processing", "order_id"}`. Билеты появятся сами |
| Резерв истёк, но билеты ещё есть | резерв берётся заново, дальше как в первых двух строках |
| Резерв истёк, и билеты разобрали | `409 reservation_expired`: верните деньги покупателю |
| Место из заказа ушло другому заказу | `409 seat_taken` с `details.order_id`, `details.seat_id`, `details.seat`. Это окончательно: заказ становится `expired` с `reason: "seat_taken"`, верните деньги |
| Резерв отменён | `400 invalid_request` «Резерв отменён — создайте новый» |
| Заказ уже подтверждён с другим `payment_ref` | `409 idempotency_conflict`, в `details.payment_ref` прежний платёж |

Подтверждение безопасно повторять: повтор вернёт тот же заказ и те же билеты.

> **Успешное подтверждение побеждает.** Если два подтверждения или подтверждение и истечение резерва пересеклись, а продажа успела зафиксироваться, заказ станет `paid`, и билеты выпустятся. Проданное мы не откатываем. Поэтому не отменяйте платёж из-за таймаута своего HTTP-клиента: повторите подтверждение с тем же `Idempotency-Key` или прочитайте заказ. `409 reservation_expired` и `409 seat_taken` приходят, только если продажа не состоялась.

## Отменить резерв {#cancel}

`DELETE /v1/reservations/{id}`

Освобождает билеты и места резерва, если оплата не прошла. Подтверждённый заказ так не отменить.

```bash Запрос
curl -X DELETE https://api.budu.events/v1/reservations/0193e0c5-2a40-7b11-9c3d-4e5f6a7b8c90 \
  -H "Authorization: Bearer $BUDU_KEY"
```

Ответ `200` с заказом в статусе `cancelled`. Повторная отмена отвечает так же. Ошибки: `400 invalid_request` «Заказ уже оплачивается — отменить резерв нельзя» (статус `confirming`) и «Заказ уже оплачен — отменяйте билеты по отдельности» (`paid`, `issued`).

## Получить заказ {#get}

`GET /v1/orders/{id}`

```bash Запрос
curl https://api.budu.events/v1/orders/0193e0c5-1f20-7c3e-b6a1-9d4e2f7a8b10 \
  -H "Authorization: Bearer $BUDU_KEY"
```

Ответ `200` с [заказом](#order-object). Здесь всегда `200`, даже пока билеты выпускаются: тогда статус `paid`, и в `tickets` ещё не все билеты.
