Бронь и оплата

Счета Kaspi и оплата

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

Как устроена оплата

  • Платит только плательщик и только счётом Kaspi. Счёт уходит с Kaspi лагеря на телефон плательщика: телефон родителя из брони, а если указан payer_phone — на него.
  • В запросе на счёт телефона нет. Сменить, кому уходят счета, можно только правкой брони (PATCH /bookings/{id} с payer_phone). Так ключ не может отправить требование заплатить на чужой номер от имени лагеря.
  • У брони одновременно только один неоплаченный счёт.
  • Счёт — только на целые тенге (Kaspi не знает тиынов) и не больше остатка по брони. Переплатить счётом нельзя.
  • Не больше 5 счетов в час на одну бронь.
  • Ввести платёж через API нельзя. Если родитель заплатил лагерю другим способом (наличными в кассе, переводом на счёт организации), такой платёж вносит сам лагерь — вы увидите его в платежах брони с source: manual.

Нужное право — invoices (выставить, отменить) и read (смотреть).

Состояния счёта

stateЧто значитСобытие
queuedСчёт создан и ждёт отправки в Kaspi (обычно секунды)invoice.queued
issuedСчёт у плательщика в приложении Kaspiinvoice.issued
paidОплачен. Рядом появляется платёж (payment_id)invoice.paid
expiredНе оплачен в срок, Kaspi закрыл егоinvoice.expired
cancelledОтменён (вами, лагерем или системой при замене)invoice.cancelled
errorНе выставлен — причина в поле errorinvoice.error
mismatchKaspi сообщил о сумме, отличной от счёта. Лагерь проверяет вручнуюinvoice.mismatch

«Живой» (незакрытый) счёт — в состоянии queued, issued или mismatch. Список состояний может пополняться: незнакомое значение не должно ломать вашу CRM.

Выставить счёт

POST /bookings/{id}/invoices с Idempotency-Key. Что выставить — поле kind:

kindСумма счёта
minСколько ещё не хватает до минимального платежа. Если минимум уже внесён — отказ 409 min_already_paid
fullВесь остаток по брони
amountСвоя сумма из поля amount: целые тенге строкой ("50000" или "50000.00"), не больше остатка
TerminalCode
curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/15230/invoices \ -H "Authorization: Bearer $ZB_KEY" \ -H "Idempotency-Key: 4c27e1a0-5d8b-4b6f-8e13-7a90d2c4f5e8" \ -H "Content-Type: application/json" \ -d '{ "kind": "amount", "amount": "100000" }'

Ответ 201 — счёт в очереди:

JSONCode
{ "id": "735", "booking_id": "15230", "amount": { "amount": "100000.00", "currency": "KZT" }, "phone": "+77010000001", "state": "queued", "cancelling": false, "payment_type": "prepayment", "purpose": "Путёвка 03-154, смена 3, 01.07–14.07, Тестов Тимур Аскарович", "created_at": "2027-05-20T06:15:00.311802Z", "issued_at": null, "paid_at": null, "payment_id": null, "error": null, "version": "2027-05-20T06:15:00.311802Z" }

Ответ приходит сразу, а в Kaspi счёт уходит очередью — следите за событиями invoice.issued и invoice.paid или перечитайте счёт: GET /invoices/735.

payment_type система ставит сама: prepayment — если счёт меньше остатка, payment — если на весь остаток.

Частые отказы:

ОтветЧто значит
409 invoice_liveУ брони уже есть неоплаченный счёт. Дождитесь его оплаты, отмените его или замените (replace: true)
400 amount_over_debtСумма больше остатка по брони
400 fractional_amountСумма с тиынами. Только целые тенге
400 nothing_to_payОстаток по брони равен нулю — платить нечего
409 min_already_paidkind: min, а минимум уже внесён. Выставьте full или amount
409 booking_cancelledБронь отменена — счёт по ней не выставляется
409 invoice_mismatchПо брони разбирается расхождение суммы — новый счёт до решения лагеря не выставить
400 bad_kind, bad_amountОпечатка в kind или сумма не по шаблону; amount передан без kind: amount
429 rate_limitedБольше 5 счетов в час по этой брони

Оплата частями

Родитель может платить путёвку несколькими платежами. Пример: путёвка 255 000 ₸, минимальный платёж 50 000 ₸.

ШагЧто происходитpaiddebtpayment_state
1Бронь оформлена, ушёл счёт на 50 000 (invoice: min)0.00255000.00awaiting
2Родитель оплатил 50 00050000.00205000.00secured
3Родитель просит счёт на 100 000 — вы выставляете kind: amount, amount: "100000"50000.00205000.00secured
4Родитель оплатил 100 000150000.00105000.00secured
5В день full_payment_due лагерь сам выставил счёт на остаток 105 000 и родитель оплатил255000.000.00secured

Правила:

  • Следующий счёт можно выставить, только когда предыдущий закрыт (оплачен, отменён, истёк) — или заменить его.
  • Место закрепляется (secured), когда сумма всех оплат брони достигла минимального платежа. Можно набрать минимум и несколькими счетами меньше минимума.
  • «Оплачено» (paid) и «остаток» (debt) в брони считаются по всем платежам, включая ещё не сверенные бухгалтерией — менеджеру не нужно ждать сверки.
  • Автоматический счёт на остаток лагерь выставляет один раз на бронь — начиная с дня срока полной оплаты и до дня заезда. Если в этот момент у брони есть неоплаченный счёт, лагерь не дублирует его, а ждёт, пока тот закроется, и выставляет счёт на остаток после. Бронь, оформленная уже после срока полной оплаты, получает такой счёт, как только закроется счёт на минимальный платёж.

Заменить счёт

Родитель получил счёт на 205 000, а хочет заплатить сейчас 100 000. Два живых счёта у брони быть не может, поэтому замените старый новым — replace: true:

TerminalCode
curl https://partners.zvezdnyy-burabai.edu.kz/ext/v1/bookings/15230/invoices \ -H "Authorization: Bearer $ZB_KEY" \ -H "Idempotency-Key: 9a3f6b20-1e4c-4d7a-b8f5-62c0e9d1a734" \ -H "Content-Type: application/json" \ -d '{ "kind": "amount", "amount": "100000", "replace": true }'

Ответ 202 — замена принята, но ещё не завершена:

JSONCode
{ "state": "replacing", "amount": { "amount": "100000.00", "currency": "KZT" }, "replaces": { "id": "733", "booking_id": "15230", "amount": { "amount": "205000.00", "currency": "KZT" }, "phone": "+77010000001", "state": "issued", "cancelling": true, "payment_type": "payment", "purpose": "Путёвка 03-154, смена 3, 01.07–14.07, Тестов Тимур Аскарович", "created_at": "2027-05-18T11:00:02.557100Z", "issued_at": "2027-05-18T11:00:06.009412Z", "paid_at": null, "payment_id": null, "error": null, "version": "2027-05-20T06:15:00.311802Z" } }

Что происходит дальше:

  1. Старый счёт гасится в Kaspi — это занимает от секунд до минут. Придёт invoice.cancelled.
  2. Сразу после этого лагерь выставляет новый счёт — придёт invoice.queued, затем invoice.issued. Номер нового счёта вы узнаете из этих событий.

Пока старый счёт гасится, плательщик ещё может его оплатить — такая оплата принимается как обычно, а новый счёт не превысит того, что останется к оплате. Предупредите родителя, чтобы дождался нового счёта.

Отменить счёт

POST /invoices/{id}/cancel с Idempotency-Key, без тела:

TerminalCode
curl -X POST https://partners.zvezdnyy-burabai.edu.kz/ext/v1/invoices/735/cancel \ -H "Authorization: Bearer $ZB_KEY" \ -H "Idempotency-Key: e5b1c7d2-08af-4e63-91c4-3f7a2d6b0e95"

Ответ 200 — счёт после отмены:

  • счёт ещё не ушёл в Kaspi (queued) — отменяется сразу, state: cancelled;
  • счёт уже у плательщика (issued) — гасится в Kaspi очередью: сначала cancelling: true, потом событие invoice.cancelled. Пока это так, счёт ещё можно оплатить — оплата будет принята.

Отказы: 409 invoice_paid — счёт уже оплачен (это уже вопрос возврата, см. Возвраты); 409 invoice_not_live — счёт уже закрыт, отменять нечего; invoice_mismatch — по счёту расхождение суммы, его разбирает только лагерь.

Изменение брони и счёт

Если вы меняете цену, скидку или трансфер брони (PATCH /bookings/{id}), остаток меняется, и живой счёт на старую сумму становится неправильным. Лагерь сам гасит его и выставляет новый на верную сумму (события invoice.cancelled, затем invoice.queued). Делать ничего не нужно.

Если счёт не выставился

У счёта в состоянии error (и иногда у живого счёта) заполнено поле error:

JSONCode
"error": { "code": "rejected", "message": "Kaspi отклонил счёт. Проверьте телефон плательщика в брони: номер должен быть зарегистрирован в Kaspi" }
error.codeЧто делать
waiting, waiting_statusНичего: ждём ответа Kaspi, счёт выставится или обновится сам
rejectedТелефон плательщика не зарегистрирован в Kaspi. Исправьте payer_phone (или телефон родителя) в брони и выставьте счёт снова
rejected_in_kaspiПлательщик сам отклонил счёт в Kaspi. Уточните у него и выставьте новый
queue_expiredСчёт не удалось выставить за сутки. Выставьте новый или обратитесь в лагерь
rejected_sent, attempts_exhausted, queue_expired_sentНе выставляйте новый — счёт мог дойти до плательщика. Обратитесь в лагерь
cancel_waiting, cancel_retryОтмена в процессе. Пока не завершена — счёт можно оплатить
cancel_failed, orphan_liveОтменить автоматически не удалось, лагерь отменит вручную. Предупредите плательщика не оплачивать этот счёт
mismatchKaspi сообщил о другой сумме — лагерь проверяет оплату
expired_in_kaspiСчёт не оплачен в срок
другой кодСчёт требует внимания лагеря — обратитесь в лагерь

Список кодов может пополняться.

Одно юрлицо на бронь

У лагеря несколько юрлиц — ТОО и ИП, — и у каждого своя касса Kaspi. Лагерь время от времени переключает, через какую кассу выставляются новые счета (это решение лагеря, вы на него не влияете).

Главное правило: деньги одной брони никогда не делятся между ИП и ТОО.

  • Пока по брони нет подтверждённой оплаты, новые счета уходят через кассу, которая сейчас активна у лагеря.
  • Как только по брони пришла и подтвердилась первая оплата, все следующие счета этой брони — доплата, остаток, автоматический счёт к сроку — уходят через кассу того же юрлица, даже если лагерь уже переключил активную кассу на другое юрлицо.
  • Уже выставленный счёт навсегда помнит свою кассу. Счёт ушёл от ТОО, лагерь переключился на ИП, родитель оплатил — деньги пришли на ТОО, и платёж записан на ТОО.
  • Возврат делается с того же юрлица, которое приняло деньги.

Пример:

  1. 14 мая активна касса ТОО. Родитель оплатил минимальный платёж 50 000 — деньги на ТОО.
  2. 1 июня лагерь переключил активную кассу на ИП. Новые брони теперь оплачиваются на ИП.
  3. 24 июня (срок полной оплаты) лагерь выставляет этой брони счёт на остаток — через кассу ТОО, потому что первая оплата брони пришла на ТОО.

На какое юрлицо пришли деньги, видно в каждом платеже — поле legal_entity (name, bin — БИН ТОО или ИИН ИП). Вашей CRM ничего делать не нужно; правило нужно знать, чтобы правильно отвечать родителям и сверять акты с лагерем.

Платежи — только чтение

НужноЗапрос
Платежи брониGET /bookings/{id}/payments
Все платежи отделаGET /payments (фильтры paid_from, updated_since)
Один платёжGET /payments/{id}
Платежи семьиGET /families/{id}/payments

Поля платежа:

ПолеЧто значит
amountСумма. Возврат — отрицательной суммой
typeprepayment — предоплата, payment — оплата, refund — возврат
sourcekaspi_invoice — оплата счёта Kaspi, manual — платёж внёс лагерь
methodКак заплатили: kaspi_invoice, kaspi_transfer, bank_transfer, cash; null — не указано
paid_onДень оплаты по календарю лагеря (время Казахстана), без времени
verifiedtrue — бухгалтерия лагеря сверила платёж с банком
legal_entityЮрлицо лагеря, на которое пришли деньги
invoice_idСчёт Kaspi, оплатой которого создан платёж
Last modified on