# Бронь и оплата по шагам

Главный сценарий от начала до конца: родитель выбрал смену → ваша CRM оформляет бронь → лагерь
отправляет родителю счёт Kaspi → родитель платит → место закреплено → остаток оплачивается к сроку.

Все данные в примерах вымышленные. Ключ — в переменной `ZB_KEY` (см.
[Первый запрос](/partners/quickstart)).

## Что нужно знать до начала

:::info{title="Пять правил, на которых держится весь сценарий"}

1. **Бронь сразу занимает место в квоте вашего отдела, но держит его только 24 часа.** За сутки
   плательщик должен внести минимальный платёж. Не внёс — бронь отменяется сама.
2. **Деньги принимает только лагерь — счётом Kaspi.** Счёт уходит с Kaspi лагеря на телефон
   плательщика. Ваш отдел денег не принимает и отметить бронь оплаченной не может.
3. **Дата рождения ребёнка обязательна.** Без неё бронь не оформится.
4. **У брони одновременно только один неоплаченный счёт.**
5. **Каждый `POST` — с заголовком `Idempotency-Key`**, кроме расчёта стоимости и проверки
   «можно ли оформить». Это защищает от двойной брони при обрыве связи
   (см. [Повтор запроса](/partners/idempotency)).

:::

Что пригодится для оформления:

| Что                      | Откуда взять                                                                              |
| ------------------------ | ----------------------------------------------------------------------------------------- |
| Номер смены (`shift_id`) | `GET /shifts?on_sale=true`                                                                |
| ФИО ребёнка              | От родителя. Полностью: фамилия, имя, отчество (если есть)                                |
| Дата рождения ребёнка    | От родителя, формат `ГГГГ-ММ-ДД`                                                          |
| Город ребёнка (`city_id`) | Номер из справочника городов лагеря. Список номеров городов лагерь передаёт вместе с ключом; в ответах API город приходит как `{"id": "1", "name": "Астана"}` |
| ФИО и телефон родителя   | От родителя. Телефон — любой записью: `8 701 000 00 01`, `+7 (701) 000-00-01`             |
| Телефон плательщика      | Только если платит **не** родитель — на него уйдёт счёт Kaspi                             |
| Точка посадки            | Если нужен трансфер: `transfer_point_id` из `transfer_prices` смены                       |

## Как проходит бронь — схема

```
 оформление          ┌─────────── 24 часа ───────────┐
 POST /bookings ──►  awaiting (место придержано)      │
                     │  счёт Kaspi на минимум ушёл     │
                     │                                │
       плательщик оплатил минимум          не оплатил до hold_until
                     │                                │
                     ▼                                ▼
                secured (место закреплено)      unpaid: бронь отменена,
                     │                          место вернулось в квоту
       день full_payment_due: счёт на остаток
                     │
                     ▼
            остаток оплачен: debt = 0.00
```

<Stepper>

1. **Найдите смену и проверьте места**

   ```sh
   curl "https://partners.zvezdnyy-burabai.edu.kz/ext/v1/shifts?on_sale=true" \
     -H "Authorization: Bearer $ZB_KEY"
   ```

   Подходит смена с `on_sale: true` и `seats.free` больше нуля. Пример ответа и что значит каждое
   поле — в [Первом запросе](/partners/quickstart#шаг-2-какие-смены-продаются). Одну смену можно
   перечитать по номеру: `GET /shifts/12`.

   Места меняются каждую минуту — их занимают и другие менеджеры вашего отдела. Не храните
   `seats.free` у себя надолго: перечитывайте смену перед оформлением или следите за событием
   `shift.seats_changed`.

2. **Посчитайте стоимость (по желанию)**

   [`POST /quote`](/api/брони) — сколько будет стоить бронь. Считает **тем же расчётом**, что и
   оформление, поэтому цифры совпадут. Ничего не создаёт, место не занимает, `Idempotency-Key` не
   нужен. Нужно право `read`.

   ```sh
   curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/quote \
     -H "Authorization: Bearer $ZB_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "shift_id": "12",
       "needs_transfer": true,
       "transfer_point_id": "4",
       "discount": { "amount": "10000", "note": "Второй ребёнок в семье" }
     }'
   ```

   Ответ `200`:

   ```json
   {
     "price": { "amount": "250000.00", "currency": "KZT" },
     "transfer_fee": { "amount": "15000.00", "currency": "KZT" },
     "discount": { "amount": "10000.00", "currency": "KZT" },
     "total": { "amount": "255000.00", "currency": "KZT" },
     "min_payment_now": { "amount": "50000.00", "currency": "KZT" },
     "hold_hours": 24,
     "full_payment_due": "2027-06-24"
   }
   ```

   - `total` — к оплате всего: 250 000 + 15 000 − 10 000 = 255 000 ₸;
   - `min_payment_now` — минимальный платёж: на эту сумму уйдёт счёт при оформлении. `0.00` —
     предоплата не нужна, бронь сразу закреплена;
   - `hold_hours` — сколько часов держится место без минимального платежа. `null` — предоплата не
     нужна.

   Скидка больше `max_discount` смены — отказ `discount_over_max` (большую скидку даёт только
   администратор лагеря).

3. **Проверьте, пройдёт ли оформление (по желанию)**

   [`POST /bookings/check`](/api/брони) — те же проверки, что при оформлении, но без брони: продажи
   открыты, у отдела есть место, ребёнок ещё не записан на эту смену, нет запрета. Удобно, чтобы
   показать менеджеру все препятствия сразу, ещё до ввода остальных данных. Нужно право `bookings`;
   `Idempotency-Key` не нужен; не больше **300 проверок в час** на отдел.

   ```sh
   curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/check \
     -H "Authorization: Bearer $ZB_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "shift_id": "12",
       "child": { "full_name": "Тестов Тимур Аскарович", "birth_date": "2016-04-12" },
       "parent": { "phone": "8 701 000 00 01" }
     }'
   ```

   Ответ `200`, если оформить можно:

   ```json
   { "can_book": true, "reasons": [] }
   ```

   Если нельзя — все причины сразу:

   ```json
   {
     "can_book": false,
     "reasons": [
       {
         "code": "duplicate_child",
         "message": "Этот ребёнок уже записан на эту смену. Если это ошибка — обратитесь в лагерь"
       }
     ]
   }
   ```

   Возможные `code`: `sales_closed`, `shift_not_open`, `quota_exhausted`, `no_quota`,
   `duplicate_child`, `blacklisted`, `birth_date_implausible` (что каждый значит — в
   [Ошибках](/partners/errors#правила-лагеря)). `can_book: true` не гарантирует места: между проверкой и
   оформлением его может занять другой менеджер.

4. **Оформите бронь**

   [`POST /bookings`](/api/брони) с заголовком `Idempotency-Key`. Нужно право `bookings`.

   <CodeTabs>

   ```sh title="cURL"
   curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings \
     -H "Authorization: Bearer $ZB_KEY" \
     -H "Idempotency-Key: 6f1c2a9e-3b7d-4e0a-9c51-2d8e4f7a1b03" \
     -H "Content-Type: application/json" \
     -d '{
       "shift_id": "12",
       "child": {
         "full_name": "Тестов Тимур Аскарович",
         "birth_date": "2016-04-12",
         "city_id": "1"
       },
       "parent": {
         "full_name": "Тестова Айгерим Болатовна",
         "phone": "8 701 000 00 01"
       },
       "needs_transfer": true,
       "transfer_point_id": "4",
       "discount": { "amount": "10000", "note": "Второй ребёнок в семье" },
       "invoice": "min",
       "external_id": "deal-10452",
       "metadata": { "crm_deal": "10452", "source": "instagram" },
       "partner_manager": "Пример Менеджер"
     }'
   ```

   ```js title="JavaScript"
   import { randomUUID } from "node:crypto";

   // Ключ повтора создаём ОДИН раз на действие и сохраняем рядом с заявкой в своей базе:
   // при обрыве связи повторяем запрос с тем же ключом.
   const idempotencyKey = deal.bookingRequestKey ??= randomUUID();

   const res = await fetch("https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings", {
     method: "POST",
     headers: {
       Authorization: `Bearer ${process.env.ZB_KEY}`,
       "Idempotency-Key": idempotencyKey,
       "Content-Type": "application/json",
     },
     body: JSON.stringify({
       shift_id: "12",
       child: { full_name: "Тестов Тимур Аскарович", birth_date: "2016-04-12", city_id: "1" },
       parent: { full_name: "Тестова Айгерим Болатовна", phone: "8 701 000 00 01" },
       needs_transfer: true,
       transfer_point_id: "4",
       discount: { amount: "10000", note: "Второй ребёнок в семье" },
       invoice: "min",
       external_id: "deal-10452",
     }),
   });
   const booking = await res.json();
   if (!res.ok) throw new Error(`${res.status} ${booking.code}: ${booking.message}`);
   console.log(booking.id, booking.payment_state, booking.hold_until);
   ```

   ```python title="Python"
   import os
   import uuid
   import requests

   # Ключ повтора создаём один раз на действие и храним рядом с заявкой.
   idempotency_key = deal.booking_request_key or str(uuid.uuid4())

   res = requests.post(
       "https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings",
       headers={
           "Authorization": f"Bearer {os.environ['ZB_KEY']}",
           "Idempotency-Key": idempotency_key,
       },
       json={
           "shift_id": "12",
           "child": {"full_name": "Тестов Тимур Аскарович", "birth_date": "2016-04-12", "city_id": "1"},
           "parent": {"full_name": "Тестова Айгерим Болатовна", "phone": "8 701 000 00 01"},
           "needs_transfer": True,
           "transfer_point_id": "4",
           "discount": {"amount": "10000", "note": "Второй ребёнок в семье"},
           "invoice": "min",
           "external_id": "deal-10452",
       },
       timeout=40,
   )
   booking = res.json()
   if not res.ok:
       raise RuntimeError(f"{res.status_code} {booking['code']}: {booking['message']}")
   print(booking["id"], booking["payment_state"], booking["hold_until"])
   ```

   ```php title="PHP"
   // Ключ повтора создаём один раз на действие и храним рядом с заявкой.
   $idempotencyKey = $deal['booking_request_key'] ?? bin2hex(random_bytes(16));

   $body = [
       'shift_id' => '12',
       'child' => ['full_name' => 'Тестов Тимур Аскарович', 'birth_date' => '2016-04-12', 'city_id' => '1'],
       'parent' => ['full_name' => 'Тестова Айгерим Болатовна', 'phone' => '8 701 000 00 01'],
       'needs_transfer' => true,
       'transfer_point_id' => '4',
       'discount' => ['amount' => '10000', 'note' => 'Второй ребёнок в семье'],
       'invoice' => 'min',
       'external_id' => 'deal-10452',
   ];
   $ch = curl_init('https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings');
   curl_setopt_array($ch, [
       CURLOPT_POST => true,
       CURLOPT_HTTPHEADER => [
           'Authorization: Bearer ' . getenv('ZB_KEY'),
           'Idempotency-Key: ' . $idempotencyKey,
           'Content-Type: application/json',
       ],
       CURLOPT_POSTFIELDS => json_encode($body, JSON_UNESCAPED_UNICODE),
       CURLOPT_RETURNTRANSFER => true,
       CURLOPT_TIMEOUT => 40,
   ]);
   $booking = json_decode(curl_exec($ch), true);
   $status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
   if ($status >= 400) {
       throw new RuntimeException("$status {$booking['code']}: {$booking['message']}");
   }
   echo $booking['id'], ' ', $booking['payment_state'], PHP_EOL;
   ```

   </CodeTabs>

   **Поля запроса:**

   | Поле                | Обязательно | Что это                                                                                   |
   | ------------------- | ----------- | ----------------------------------------------------------------------------------------- |
   | `shift_id`          | да          | Номер смены                                                                               |
   | `child.full_name`   | да          | ФИО ребёнка                                                                               |
   | `child.birth_date`  | **да**      | Дата рождения `ГГГГ-ММ-ДД`. Без неё — отказ `birth_date_required` (почему — ниже)         |
   | `child.city_id`     | да          | Город из справочника городов лагеря                                                       |
   | `parent.full_name`  | да          | ФИО родителя                                                                              |
   | `parent.phone`      | да          | Телефон родителя, любой записью                                                           |
   | `payer_phone`       | нет         | Телефон плательщика, если платит не родитель: счета Kaspi уйдут на него                   |
   | `needs_transfer`    | нет         | `true` — нужен трансфер. По умолчанию `false`                                             |
   | `transfer_point_id` | если трансфер | Точка посадки из `transfer_prices` смены                                                |
   | `discount`          | нет         | Скидка: `amount` — тенге строкой (`"10000"`), `note` — за что. Не больше `max_discount` смены |
   | `invoice`           | нет         | Какой счёт Kaspi отправить сразу: `min`, `full` или `none` (таблица ниже)                 |
   | `comment`           | нет         | Комментарий для лагеря                                                                    |
   | `together_with`     | нет         | Номера ваших броней той же смены, с кем поселить вместе (до 50)                           |
   | `external_id`       | нет         | Номер сделки или заявки в вашей CRM (до 255 знаков, уникален в вашем отделе)              |
   | `metadata`          | нет         | Ваши метки: до 20 пар «ключ — значение»                                                   |
   | `partner_manager`   | нет         | Кто продал в вашей CRM (до 200 знаков) — для ваших отчётов                                |

   Поле, которого нет в этой таблице, — отказ `422 invalid_input` (имя поля — в `message`). Так
   опечатка в имени поля не превращается в молча потерянные данные.

   **Какой счёт отправить сразу** (`invoice`):

   | Значение | Что произойдёт                                                                                                      |
   | -------- | ------------------------------------------------------------------------------------------------------------------- |
   | `min`    | Счёт на минимальный платёж смены. **Так и будет, если поле не передать**, когда у смены минимальный платёж больше нуля |
   | `full`   | Счёт сразу на всю сумму к оплате                                                                                    |
   | `none`   | Счёт не отправляется. Место всё равно держится только 24 часа — выставьте счёт сами до `hold_until`                 |

   Если у смены минимальный платёж `0.00` и `invoice` не передан, счёт не отправляется: бронь сразу
   закреплена, оплатить её можно позже (счётом `full` или автоматическим счётом на остаток).

   :::info{title="Почему дата рождения обязательна"}

   В базе лагеря ребёнок узнаётся по **ФИО и дате рождения вместе**. По ним лагерь понимает, что
   «Тестов Тимур, 12.04.2016» — тот же ребёнок, что ездил прошлым летом, а не новый; находит дубль,
   если ребёнка уже записали на эту смену; назначает отряд по возрасту.

   Без даты рождения двух детей с одним именем в одной семье не различить, а ребёнок другого отдела
   мог бы «перейти» к вам по одному совпадению имени и телефона. Поэтому без даты API не оформляет
   бронь вовсе (`400 birth_date_required`). Дата должна быть правдоподобной: в лагерь едут дети от
   3 до 19 лет на дату заезда, иначе — `birth_date_implausible`. Дата в будущем — `bad_birth_date`.

   :::

   **Ответ `201`** — бронь целиком:

   ```json
   {
     "id": "15230",
     "version": "2027-05-14T09:31:07.482913Z",
     "status": "active",
     "payment_state": "awaiting",
     "hold_until": "2027-05-15T09:31:07.482913Z",
     "shift": { "id": "12", "number": 3, "date_start": "2027-07-01", "date_end": "2027-07-14" },
     "child": {
       "id": "40871",
       "full_name": "Тестов Тимур Аскарович",
       "birth_date": "2016-04-12",
       "city": { "id": "1", "name": "Астана" }
     },
     "parent": { "id": "22014", "full_name": "Тестова Айгерим Болатовна", "phone": "+77010000001" },
     "payer_phone": null,
     "transfer": {
       "needed": true,
       "fee": { "amount": "15000.00", "currency": "KZT" },
       "point": { "id": "4", "name": "Астана, ж/д вокзал" }
     },
     "price": { "amount": "250000.00", "currency": "KZT" },
     "discount": { "amount": { "amount": "10000.00", "currency": "KZT" }, "note": "Второй ребёнок в семье" },
     "total": { "amount": "255000.00", "currency": "KZT" },
     "paid": { "amount": "0.00", "currency": "KZT" },
     "debt": { "amount": "255000.00", "currency": "KZT" },
     "full_payment_due": "2027-06-24",
     "squad_number": null,
     "checkin": { "checked_in_at": null, "checked_out_at": null },
     "voucher": { "status": "none" },
     "voucher_number": "03-154",
     "comment": "",
     "cancelled_reason": null,
     "contacts_hidden": false,
     "external_id": "deal-10452",
     "metadata": { "crm_deal": "10452", "source": "instagram" },
     "partner_manager": "Пример Менеджер",
     "created_at": "2027-05-14T09:31:07.482913Z",
     "updated_at": "2027-05-14T09:31:07.482913Z"
   }
   ```

   Сохраните у себя `id` брони — по нему все дальнейшие запросы. Телефон в ответе всегда в виде
   `+7XXXXXXXXXX`, даже если вы прислали `8 701 …`.

   Самые частые отказы при оформлении:

   | Ответ                        | Что значит и что делать                                                                  |
   | ---------------------------- | ---------------------------------------------------------------------------------------- |
   | `400 birth_date_required`    | Нет даты рождения. Спросите у родителя                                                    |
   | `409 quota_exhausted`        | Места вашего отдела на смене закончились. Предложите другую смену или попросите лагерь добавить мест |
   | `409 no_quota`               | Вашему отделу не выделены места на эту смену                                              |
   | `409 sales_closed`           | Продажи на смену закрыты                                                                  |
   | `409 duplicate_child`        | Ребёнок уже записан на эту смену (возможно, другим отделом). Если это ошибка — в лагерь   |
   | `403 blacklisted`            | Оформить этого ребёнка нельзя. Причину API не сообщает — обратитесь в лагерь              |
   | `400 discount_over_max`      | Скидка больше разрешённой                                                                 |
   | `409 external_id_taken`      | Этот `external_id` уже у другой вашей брони (в `message` — её номер). Скорее всего, бронь уже оформлена — прочитайте её: `GET /bookings/by-external-id/deal-10452` |

   Все коды — в [Ошибках](/partners/errors).

5. **Лагерь отправляет счёт Kaspi**

   При `invoice: min` или `full` счёт создаётся **вместе с бронью** и уходит плательщику очередью —
   обычно за несколько секунд. Вы узнаете об этом событиями:

   - `invoice.queued` — счёт создан и ждёт отправки в Kaspi;
   - `invoice.issued` — счёт у плательщика в приложении Kaspi.

   Счета брони — [`GET /bookings/15230/invoices`](/api/счета):

   ```json
   {
     "items": [
       {
         "id": "731",
         "booking_id": "15230",
         "amount": { "amount": "50000.00", "currency": "KZT" },
         "phone": "+77010000001",
         "state": "issued",
         "cancelling": false,
         "payment_type": "prepayment",
         "purpose": "Путёвка 03-154, смена 3, 01.07–14.07, Тестов Тимур Аскарович",
         "created_at": "2027-05-14T09:31:07.482913Z",
         "issued_at": "2027-05-14T09:31:12.104377Z",
         "paid_at": null,
         "payment_id": null,
         "error": null,
         "version": "2027-05-14T09:31:12.104377Z"
       }
     ],
     "next_cursor": null
   }
   ```

   - `purpose` — текст, который плательщик видит в Kaspi рядом с суммой. Подскажите родителю
     искать счёт с этим текстом;
   - `payment_type: prepayment` — счёт меньше полной суммы, `payment` — на весь остаток.

   Если счёт не выставился (`invoice.error`, у счёта заполнено `error`) — чаще всего телефон
   плательщика не зарегистрирован в Kaspi. Что делать — в
   [Счета Kaspi и оплата](/partners/kaspi-invoices#если-счёт-не-выставился).

6. **Плательщик оплачивает**

   Плательщик открывает Kaspi, видит счёт и оплачивает. Через несколько секунд лагерь узнаёт об
   оплате, и приходят события (обычно в таком порядке, но полагайтесь на номера событий, а не на
   порядок типов):

   - `invoice.paid` — счёт оплачен;
   - `payment.received` — платёж записан;
   - `booking.secured` — минимальный платёж набран, место закреплено.

   Платёж — [`GET /bookings/15230/payments`](/api/платежи) или по номеру из события:

   ```json
   {
     "id": "9120",
     "booking_id": "15230",
     "amount": { "amount": "50000.00", "currency": "KZT" },
     "type": "prepayment",
     "source": "kaspi_invoice",
     "method": "kaspi_invoice",
     "invoice_id": "731",
     "paid_on": "2027-05-14",
     "verified": false,
     "statement_line": null,
     "bank": "Kaspi",
     "legal_entity": {
       "id": "1",
       "name": "ТОО «Пример»",
       "legal_name": "Товарищество с ограниченной ответственностью «Пример»",
       "bin": "000000000000",
       "iik": "KZ000000000000000000",
       "onec_organization": null
     },
     "created_at": "2027-05-14T10:02:45.918201Z",
     "version": "2027-05-14T10:02:45.918201Z"
   }
   ```

   Бронь после этого (`GET /bookings/15230`, только изменившиеся поля):

   ```json
   {
     "id": "15230",
     "payment_state": "secured",
     "paid": { "amount": "50000.00", "currency": "KZT" },
     "debt": { "amount": "205000.00", "currency": "KZT" }
   }
   ```

   `verified: false` — бухгалтерия лагеря ещё не сверила платёж с банком. Это не мешает: остаток
   уменьшился и место закреплено сразу. Когда сверят — придёт событие `payment.verified`.

7. **Если плательщик не оплатил за сутки**

   Не пришёл минимальный платёж до `hold_until` — лагерь **сам** отменяет бронь (обычно в течение
   нескольких минут после срока):

   - событие `booking.hold_expired`;
   - у брони `status: cancelled`, `payment_state: unpaid`;
   - место вернулось в квоту (`shift.seats_changed`);
   - неоплаченный счёт гасится в Kaspi.

   Ребёнок при этом **остаётся в вашей базе** — чтобы менеджер мог перезвонить. Список таких броней
   на обзвон — [`GET /reports/unpaid`](/api/отчеты). Если семья всё-таки хочет ехать —
   [восстановите бронь](/partners/booking-changes#восстановить-отменённую-бронь), если места ещё есть.

8. **Остаток — к сроку полной оплаты**

   В день `full_payment_due` лагерь **сам** выставляет плательщику один счёт на весь остаток.
   Ничего делать не нужно. Родитель хочет заплатить раньше или частями — выставьте счёт сами
   (см. [Счета Kaspi и оплата](/partners/kaspi-invoices)).

   Когда остаток оплачен — `debt` становится `0.00`. Бронь оплачена полностью.

</Stepper>

## Как узнавать о переменах

Не опрашивайте каждую бронь по кругу — для этого есть **события**: лента изменений вашего отдела.
Самый удобный способ — [вебхуки](/partners/webhooks): лагерь сам присылает вам каждое событие. Если
своего публичного сервера нет — [опрос ленты](/partners/events#опрос) раз в 10–30 секунд.

Какие события важны в этом сценарии:

| Событие                | Что сделать у себя                                                  |
| ---------------------- | ------------------------------------------------------------------- |
| `booking.created`      | Бронь появилась (в том числе оформленная менеджером вашего отдела в CRM лагеря) |
| `invoice.issued`       | Сказать менеджеру: «счёт у родителя в Kaspi»                        |
| `invoice.error`        | Предупредить менеджера: счёт не ушёл, проверить телефон            |
| `payment.received`     | Обновить «оплачено» и «остаток» — перечитайте бронь                 |
| `booking.secured`      | Место закреплено — сделка выиграна                                   |
| `booking.hold_expired` | Бронь снята по неоплате — задача менеджеру на звонок                |
| `booking.cancelled`    | Бронь отменена (вами или лагерем)                                   |

## Несколько детей одной семьи

[`POST /bookings/batch`](/api/брони) оформляет от 1 до 10 детей одной семьи **за один раз: или все,
или никого**. Если не прошёл хотя бы один ребёнок (нет мест, дубль, запрет), не оформляется никто, а
в `message` сказано, какой ребёнок и почему: «Ребёнок 2: …».

```sh
curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/batch \
  -H "Authorization: Bearer $ZB_KEY" \
  -H "Idempotency-Key: 0b8e5d14-7c3a-4f29-a6e1-93d2c5b80f47" \
  -H "Content-Type: application/json" \
  -d '{
    "parent": { "full_name": "Тестова Айгерим Болатовна", "phone": "8 701 000 00 01" },
    "invoice": "min",
    "together": true,
    "children": [
      {
        "shift_id": "12",
        "child": { "full_name": "Тестов Тимур Аскарович", "birth_date": "2016-04-12", "city_id": "1" },
        "external_id": "deal-10452-1"
      },
      {
        "shift_id": "12",
        "child": { "full_name": "Тестова Алия Аскаровна", "birth_date": "2018-09-30", "city_id": "1" },
        "discount": { "amount": "10000", "note": "Второй ребёнок в семье" },
        "external_id": "deal-10452-2"
      }
    ]
  }'
```

Ответ `201` — `{"items": [бронь, бронь]}` в порядке детей запроса. У каждого ребёнка — своя бронь и
**свой счёт Kaspi**. `together: true` — поселить вместе детей семьи, попавших на одну смену. У
каждого ребёнка можно указать свой `invoice`; если не указан — берётся общий.

## Дальше

- [Счета Kaspi и оплата](/partners/kaspi-invoices) — частичная оплата, замена и отмена счёта,
  юрлица лагеря.
- [Изменения брони](/partners/booking-changes) — правка, отмена, восстановление, перенос.
