События
Каждое изменение ваших броней, счетов, платежей и детей попадает в ленту событий вашего отдела. Так вы узнаёте, что родитель оплатил счёт, бронь снялась по неоплате, лагерь назначил отряд или ребёнок заехал, — без постоянного перечитывания всех броней.
В ленту попадает всё, что происходит с вашими записями, кто бы это ни сделал: ваша CRM через
API, менеджер вашего отдела в CRM лагеря, сотрудник лагеря, автоматика (снятие по неоплате, счёт на
остаток, ответ Kaspi). События хранятся 90 дней. Нужно право read.
Три способа получать события
| Способ | Как | Когда удобен |
|---|---|---|
| Вебхуки | Лагерь сам отправляет POST на ваш https-адрес, с подписью | Есть публичный сервер. События приходят сразу. Рекомендуем |
| Опрос | Ваша CRM спрашивает GET /events?after=N | Проще всего; публичного адреса не нужно. Задержка — как часто опрашиваете |
| Веб-сокет | Постоянное соединение wss://…/ext/v1/stream | События сразу без публичного адреса, но нужно держать соединение |
Все три отдают одни и те же события с одними и теми же номерами. Можно сочетать: вебхуки для скорости, опрос раз в час — на случай, если что-то пропустили.
О вебхуках — отдельная страница: Вебхуки.
Как выглядит событие
Code
| Поле | Что это |
|---|---|
id | Номер события — он же место в ленте. Только растёт, не повторяется. По нему узнаётся повтор |
type | Что случилось (таблица ниже) |
occurred_at | Когда случилось, UTC |
entity | Чего касается: type (booking, invoice, payment, child, parent, shift) и id |
object | Запись целиком — та же, что вернул бы GET по ней. null, если запись вам больше не видна (ребёнок ушёл в другой отдел) |
data | Подробности, которых нет в самой записи. Сейчас бывают у booking.moved ({"from_shift_id": "12"}), client.merged и child.merged ({"into_id": "812"}), ping ({"message": "…"}). У остальных — null |
object — текущий вид записи, а не снимок на момент события
object собирается в момент отправки события. Если бронь успели изменить ещё раз, в object
события booking.created уже будет новая версия. Поэтому:
- не делайте выводов о прошлом по
object— смотрите наtype; - обновляйте у себя запись по
object, только если егоversionновее той, что у вас (у брони, счёта, платежа есть полеversion).
Типы событий
Брони (entity.type = booking):
| Тип | Когда |
|---|---|
booking.created | Бронь оформлена (через API, в CRM лагеря, переносом на другую смену), или бронь передали вашему отделу |
booking.updated | Изменились данные брони: ФИО, телефоны, цена, скидка, трансфер, комментарий, отряд, срок удержания |
booking.cancelled | Бронь отменена вручную |
booking.hold_expired | Бронь снята автоматически: минимальный платёж не пришёл за сутки |
booking.restored | Отменённая бронь восстановлена |
booking.secured | Минимальный платёж набран — место закреплено |
booking.moved | Лагерь перевёл эту бронь на другую смену (data.from_shift_id — с какой) |
booking.removed | Лагерь передал бронь другому отделу — у вас её больше нет |
voucher.ready | Путёвка сформирована |
voucher.sent | Путёвка отправлена семье |
child.checked_in | Ребёнок заехал в лагерь (по брони) |
child.checked_out | Ребёнок уехал из лагеря |
Счета Kaspi (invoice): invoice.queued, invoice.issued, invoice.paid,
invoice.cancelled, invoice.expired, invoice.error, invoice.mismatch — имя совпадает с новым
состоянием счёта (см. Счета Kaspi).
Платежи (payment):
| Тип | Когда |
|---|---|
payment.received | Пришли деньги: оплачен счёт Kaspi или лагерь внёс платёж |
payment.refunded | Записан возврат (платёж с отрицательной суммой) |
payment.verified | Бухгалтерия лагеря сверила платёж с банком |
payment.updated | Лагерь поправил платёж |
Платёж не меняет версию брони и не порождает booking.updated. Получив payment.*,
перечитайте бронь (GET /bookings/{object.booking_id}), чтобы обновить «оплачено» и «остаток».
Дети и семьи (child, parent):
| Тип | Когда |
|---|---|
child.added | Ребёнок перешёл к вам (вы оформили ему бронь на самую позднюю смену) |
child.removed | Ребёнок ушёл в другой отдел. object — null |
child.merged | Две карточки ребёнка объединены, data.into_id — оставшаяся |
client.merged | Две карточки семьи объединены, data.into_id — оставшаяся |
Подробнее — в Ваша база.
Смены (shift): shift.seats_changed — изменилось число свободных мест вашего отдела (кто-то
оформил или отменил бронь, лагерь поменял квоту); shift.status_changed — смена открылась, закрылась,
началась, завершилась.
Возвраты (в подготовке, см. Возвраты): refund_request.created,
refund_request.approved, refund_request.rejected, refund_request.cancelled,
refund_request.done, refund_request.failed.
Проверка вебхука: ping — только в ответ на POST /webhooks/{id}/test.
Список открытый и пополняется. Незнакомый тип — пропустите (ответьте 200, если это вебхук), а
не падайте с ошибкой.
Правила, на которые можно опираться
- Хотя бы один раз. Одно событие может прийти дважды (после переподключения, повтора вебхука).
Повтор узнаётся по
id: храните номера обработанных событий и пропускайте уже виденные. - По порядку. События отдаются по возрастанию
id. Номер, раз выданный, не «обгоняется» более ранним: если вы видели событие 1042, событие 1041 позже не появится. - Номера идут с пропусками. Лента общая для всех отделов, вам приходят только ваши события — после 1042 может прийти 1057. Это нормально.
- Ничего не теряется. Событие записывается в той же транзакции, что и само изменение: нет изменения без события и события без изменения.
Опрос
GET /events?after=N — события после номера N.
- Храните у себя номер последнего обработанного события. В самом начале —
0(вся лента за 90 дней). - Запросите
GET /events?after=<номер>. - Обработайте
itemsпо порядку, после каждого сохраните егоid. has_more: true— за этой порцией есть ещё, запросите сразу сafter = next_after.has_more: false— подождите 10–30 секунд и спросите снова.
Code
Code
next_after— что передать вafterследующего запроса (номер последнего события в порции; если событий не было — ваш жеafter).limit— от 1 до 500, по умолчанию 100. Больше 100 — «тяжёлый» запрос (не чаще раза в секунду).afterбольше последнего номера ленты или не число —400 bad_after.
Веб-сокет
Адрес — wss://partners.zvezdnyy-burabai.edu.kz/ext/v1/stream. Ключ — в заголовке Authorization
запроса на соединение, как у остальных запросов (поэтому из браузера подключиться нельзя — только с
сервера). Нужно право read. Одновременно — не больше 3 соединений на подключение; четвёртое
получает 429 ещё до соединения.
Обмен — текстовые сообщения JSON:
- Сервер сразу шлёт
{"type": "hello", "last_event_id": "1042"}— номер последнего события вашего отдела. - Вы шлёте
{"type": "subscribe", "after": "1000"}.afterобязателен: номер последнего обработанного события ("0"— с начала, за 90 дней). Можно добавить"types": ["booking.created", "invoice.paid"]— тогда придут только эти типы. - Сервер досылает всё пропущенное после
after(порциями до 500), затем шлёт новые события по мере появления:{"type": "event", "event": { …событие как в GET /events… }}. Повторныйsubscribeначинает отдачу заново с новогоafter. - На неразборчивое сообщение —
{"type": "error", "code": "bad_message", "message": "…"}, соединение остаётся.
Что нужно знать о соединении:
- сервер шлёт ping каждые 30 секунд; нет ответа 90 секунд — закрывает соединение (большинство библиотек отвечают на ping сами);
- раз в минуту сервер перепроверяет ключ: отозван, истёк, подключение отключено —
{"type": "error", "code": "unauthorized"}и закрытие с кодом 1008; - клиент, который не забирает данные 30 секунд, отключается;
- каждая порция догоняния считается «тяжёлым» запросом (1 в секунду): кончился лимит — отдача ждёт, а не рвётся;
- после переподключения события после вашего
afterпридут снова — узнавайте повтор по номеру.
Node.js (пакет ws)
Как обрабатывать событие
- Уже видели этот
id? Пропустите. - Незнакомый
type? Пропустите. - Есть
object? Обновите у себя запись, если егоversionновее вашей. Нетobject(null) — запись вам больше не видна: пометьте её у себя. - Нужны свежие данные о деньгах — перечитайте бронь по
GET. - Сохраните
idкак обработанный.
Делайте обработку идемпотентной: одно и то же событие, обработанное дважды, не должно, например, дважды отправить родителю SMS «спасибо за оплату».
Если события потерялись у вас
Раз в сутки сверяйтесь: GET /reconcile/bookings отдаёт номер, статус и версию
всех броней отдела — перечитайте те, что у вас отличаются (см.
Ваша база). Если ваша CRM лежала дольше 90 дней, лента
за это время уже очищена — восстановите данные сверкой и списками (GET /bookings, GET /payments
с updated_since).