Партнёрский API продажи билетов
Руководство по интеграции для разработчиков Ticketon: как подписывать запросы, в каком порядке звать ручки, что означает каждый статус и код ошибки, и как пройти сертификацию на стенде.
Этот документ и OpenAPI-файл описывают одно и то же. При расхождении по форме полей побеждает OpenAPI-файл; этот текст объясняет, почему поля устроены именно так и что с ними делать.
Как это работает
Ticketon продаёт билеты на мероприятия ДСС у себя. Мероприятия, зал и статусы мест остаются на стороне ДСС; Ticketon получает доступ к живому инвентарю и проводит оплату самостоятельно. Пять утверждений, на которых стоит всё остальное:
- Живой инвентарь, а не квота. Вы работаете с теми же местами, что и витрина ДСС. Место, проданное на витрине, исчезает у вас немедленно, и наоборот. Заранее выделенных пачек мест не существует; ДСС остаётся мастер-системой статусов.
- Доступ выдаётся на мероприятие. Вы видите и продаёте только то, на что ДСС выдала грант. Список меняется на стороне ДСС без правок вашего кода.
- Деньги у вас. Вы принимаете оплату и сами фискализируете чек. Платёжный шлюз ДСС в этом потоке не участвует;
payment_infoв подтверждении заказа это факт вашего платежа, который ДСС хранит для сверки. Взаиморасчёт между ДСС и партнёром периодический и идёт вне API. - Заказ раньше денег, билеты после подтверждения. Заказ запирает места платёжным окном до списания. После списания вы зовёте
confirmи получаете билеты: подписанныйqr_tokenиpdf_urlна каждый. Писем покупателю ДСС не отправляет, доставка на вашей стороне. - Возврат денег делаете вы. ДСС гасит билеты, погашенный не проходит на вход. В первой фазе возврат оформляется обращением к персоналу ДСС, и место после него в продажу не возвращается; освобождение мест появится вместе с ручкой
refundво второй фазе.
Списывать деньги раньше ответа 201 на POST /orders запрещено. Заказ и есть гарантия места: до него бронь может истечь, и получится «деньги списаны, места нет». Сверка проверяет, что payment_info.paid_at не раньше created_at заказа.
Заказ, не получивший ни confirm, ни cancel, истекает сам и возвращает места в продажу. Подвисших билетов без денег не бывает по построению.
Если у вас на руках ТЗ СКУП ДСС
Контракт следует разделу 4.2.14 технического задания заказчика и отличается от него в шести местах, каждое ради покупателя или денег: места отдаются посекторно, а не всем залом одним ответом; успешный ответ обёрнут в {"data": …}; ключи идемпотентности partner_booking_id и partner_order_id обязательны; формула подписи зафиксирована проверочным вектором, а не словами; заказ создаётся до списания, а оплата подтверждается отдельным вызовом; гарантированный канал отмены это опрос, вебхуки подключаются позже как ускоритель.
Быстрый старт
- Стенд
- адрес и ключи
pk_sandbox_…передаются техническому контакту (планируетсяapi-sandbox.dss.ndsvs.kz; адрес подтверждается вместе с ключами) - Продуктив
https://api-dss.ndsvs.kz/api/v1/partners, ключиpk_live_…после сертификации- Транспорт
- HTTPS, TLS 1.2 и выше; JSON в UTF-8; имена полей
snake_case - Заголовки
X-Api-Key,X-Timestamp,X-Signatureна каждом запросе;Content-Type: application/jsonна POST
Подпись запроса
Каждый запрос подписывается HMAC-SHA256. Строка для подписи собирается из четырёх частей, разделённых одним символом перевода строки (байт 0x0A):
<timestamp>\n<METHOD>\n<uri>\n<body>| Часть | Что подставлять |
|---|---|
timestamp | Unix-время в секундах, десять цифр, то же значение, что в X-Timestamp. Окно валидности пять минут в обе стороны. |
METHOD | GET или POST, заглавными. |
uri | Полный путь с query-строкой ровно как он уходит на провод, включая префикс /api/v1/partners, без хоста. Параметры не пересортировываются. |
body | Тело запроса байт в байт. Для GET пустая строка, но разделитель \n перед ней остаётся. |
Подпись: HMAC-SHA256(key = secret_key, message = canonical), результат в нижнем регистре hex. Секрет это ключ HMAC, а не часть сообщения.
import hmac, hashlib, json, time
def headers(api_key: str, secret: str, method: str, uri: str, body: bytes = b"") -> dict:
ts = str(int(time.time())) # секунды, не миллисекунды
canonical = f"{ts}\n{method.upper()}\n{uri}\n".encode() + body
sig = hmac.new(secret.encode(), canonical, hashlib.sha256).hexdigest()
return {"X-Api-Key": api_key, "X-Timestamp": ts, "X-Signature": sig}
# тело сериализуется ОДИН раз, и отправляются ровно эти байты
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode()X-Timestamp в миллисекундах. Тринадцать цифр окажутся вне пятиминутного окна, и ответ будет INVALID_SIGNATURE, хотя подпись посчитана верно.
Тело сериализовано дважды. Подписали одну строку, а HTTP-клиент пересобрал JSON с другими пробелами или порядком ключей. Подписывайте те же байты, что отправляете.
Литеральный \n. В примерах ниже перевод строки показан как \n ради читаемости. Скопированные два символа «обратный слэш и n» дадут другую подпись.
Отсутствующий X-Api-Key отвечает 401 INVALID_API_KEY; любая другая проблема аутентификации, включая timestamp вне окна, отвечает 401 INVALID_SIGNATURE. Отдельного nonce нет намеренно: каждый изменяющий вызов идемпотентен по вашим ключам (Идемпотентность), поэтому повтор запроса внутри окна воспроизводит тот же результат и нового эффекта не создаёт.
Проверочный вектор
Сойдитесь с ним до первого запроса на стенд: расхождение здесь стоит минуты, расхождение на стенде стоит дня переписки.
- secret
sandbox_secret_do_not_use_in_prod- X-Timestamp
1790841600
1790841600\nPOST\n/api/v1/partners/bookings\n{"event_id":32,"partner_booking_id":"TKT-BKG-2026-0009871","seats":["seat-AA-A-03-005","seat-AA-A-03-006"],"customer":{"email":"buyer@ticketon.kz"}}
X-Signature: d2b089654f675c25a6f8e3dacce46f5850c7a5a6c62a8a697fcd92e8fc6c55271790841600\nGET\n/api/v1/partners/events?date_from=2026-10-01&limit=50\n
X-Signature: d783761a531da8c84c1e6fba1c048f5ebc6cf8c9ac66fe43a7cf060dd95616caЧеклист перед первым запросом на стенд
- Обе подписи проверочного вектора сошлись на вашей реализации.
X-Timestampдесятизначный, берётся из синхронизированных часов.- Тело сериализуется один раз; подписанные байты и отправленные байты это один буфер.
- Клиент читает
X-Request-Idиз ответа и пишет его в журнал рядом с телом отказа. - Таймаут клиента около 10 секунд, повтор с нарастающей задержкой на 5xx и на сетевых ошибках.
Общие правила
- Успех
{"data": …}; списки дополнительно несут{"meta": {"total", "limit", "offset"}}- Ошибка
{"error": {"code", "message", "details"}}; ветвиться поcode,messageписать в журнал, а не показывать покупателю- Деньги
- целые тенге без копеек;
currencyвсегдаKZT - Время
- ISO 8601 со смещением, например
2026-10-05T19:00:00+05:00; площадки в UTC+5 - Идентификаторы
seat-AA-A-03-007,sector-AA-A,row-AA-A-03это непрозрачные строки платформы: не разбирать и не собирать самостоятельно- Трассировка
- заголовок
X-Request-Id(UUID) принимается на любом запросе и возвращается тем же заголовком в каждом ответе, включая ошибки; по нему ДСС находит запрос у себя - Пагинация
limitдо 100 (по умолчанию 50),offset; при опросе изменений сupdated_sinceответ полный и пагинация не применяется
Ограничение частоты
По умолчанию 100 запросов в минуту на партнёра; боевое значение договорное и согласуется под онсейл заранее, как и срок брони с платёжным окном. Каждый ответ несёт X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset (Unix-секунды). При превышении 429 RATE_LIMIT_EXCEEDED.
Рекомендуемая каденция чтений: сводку секторов не чаще раза в 10 секунд на мероприятие; места сектора запрашивать, когда пользователь открывает сектор, а не фоном по всем секторам. Доступность в чтениях справочная, истина только за успешной бронью, поэтому частый опрос точности не добавляет.
Перед приложением стоит общая защита инфраструктуры. Её 429 может прийти без тела нашего формата и без заголовков X-RateLimit-*. То же касается 5xx: тело может быть вне формата {"error": …}. Ветвитесь по HTTP-статусу, повторяйте с нарастающей задержкой; все изменяющие вызовы идемпотентны, повтор безопасен.
Справочник ручек
Девять операций. Восемь доступны в первой фазе, refund появляется во второй. Все пути относительно префикса /api/v1/partners. У любой операции возможны 401 (аутентификация) и 429 (лимит), в таблицах ниже они не повторяются; 403 (ACCESS_DENIED, PARTNER_DISABLED) перечислен там, где он возможен.
| Операция | Зачем |
|---|---|
GET /events | что можно продавать; с updated_since это фид изменений |
GET /events/{event_id} | карточка одного мероприятия |
GET /events/{event_id}/seats | сводка по секторам, затем места сектора |
POST /bookings | удержать места на время выбора |
POST /orders | заказ: места заперты платёжным окном |
POST /orders/{order_id}/confirm | оплата прошла, билеты в ответе |
POST /orders/{order_id}/cancel | эквайер отказал, места свободны сразу |
GET /orders?partner_order_id= | восстановление после потери ответа |
POST /orders/{order_id}/refund | отметка возврата, вторая фаза |
/eventsСписок мероприятий и фид изменений
Возвращает мероприятия, на которые у вас есть грант. Без updated_since в выдаче sales_scheduled, sales_open и sales_paused. С updated_since ручка становится фидом изменений: приходят все мероприятия с грантом, изменившиеся после курсора, в том числе cancelled, sales_closed и отозванные (access: "revoked"). Подробно об опросе в разделе Изменения мероприятий.
Параметры запроса
| Параметр | Значение |
|---|---|
updated_since | meta.cursor из предыдущего ответа. Не своё время: курсор серверный и непрозрачный. Первый запрос без параметра. |
date_from, date_to | период по дате начала, YYYY-MM-DD, конец включительно |
venue_id | объект, например almaty-arena |
event_type | тип из словаря |
limit, offset | пагинация; с updated_since игнорируются |
Поля, на которые стоит смотреть: title гарантированно на трёх языках; description и hall_name гарантируют только ru, остальные языки могут отсутствовать, показывайте с откатом на русский. max_seats_per_booking это действующий потолок мест в одной брони именно для этого мероприятия (по умолчанию 8). price_from_tenge может быть null, если цены ещё не заданы.
Ошибки
| Код | Когда |
|---|---|
401INVALID_API_KEY, INVALID_SIGNATURE | аутентификация |
422INVALID_PARAMETERS | неверная дата, неизвестный тип, limit вне диапазона |
429RATE_LIMIT_EXCEEDED | превышена частота |
/events/{event_id}Карточка мероприятия
Текущее состояние одного мероприятия с грантом: обновление вашей страницы между опросами фида. Тело такое же, как элемент списка. Отменённое мероприятие с грантом читается.
Ошибки
| Код | Когда |
|---|---|
403ACCESS_DENIED | мероприятие существует, гранта нет |
403PARTNER_DISABLED | доступ партнёра приостановлен ДСС целиком; отличайте от проблемы ключа |
404EVENT_NOT_FOUND | такого event_id нет |
/events/{event_id}/seatsДоступность мест
Места отдаются посекторно: зал первого объекта это 960 мест в шести секторах. Ручка работает в двух режимах. Без параметра sector приходит сводка по секторам с минимальной ценой и числом свободных мест; с параметром sector приходят места этого сектора. Схему зала стройте одним запросом сводки плюс по запросу на сектор, который открывает пользователь.
Параметры запроса
| Параметр | Значение |
|---|---|
sector | идентификатор сектора из сводки, например sector-AA-A |
category | фильтр по категории билета: standard, vip, fan, invitation, service |
status живой: held значит, что место прямо сейчас удерживает кто-то, в том числе покупатель на витрине ДСС. Продавать можно только места с is_sellable: true, это поле учитывает и статус, и цену. Геометрию для отрисовки (SVG-план зала, в котором id элементов совпадают с space_id) можно запросить у технического контакта ДСС.
Ошибки
| Код | Когда |
|---|---|
403ACCESS_DENIED, PARTNER_DISABLED | нет гранта, доступ приостановлен |
404EVENT_NOT_FOUND | мероприятия нет |
422INVALID_PARAMETERS | неизвестный sector (в details.sector) или категория |
/bookingsВременное бронирование
Удерживает места на срок брони. Захват атомарный: либо удержаны все перечисленные места, либо ни одно. Срок настраивается на партнёра, по умолчанию 3 минуты; действующее значение приходит в expires_at и ttl_seconds, не зашивайте его константой. Бронь не является продажей и ничего не резервирует за покупателем: по истечении срока места освобождаются сами.
Цена фиксируется в момент брони. price_tenge каждого места и total_tenge из ответа наследуются заказом как есть; до брони цены могут меняться. Итог покупателю показывайте из ответа брони, а не из закэшированных чтений. Льготных цен в первой фазе нет: каждое место бронируется по полной цене своей категории.
Тело запроса
| Поле | Значение |
|---|---|
event_id | мероприятие с грантом |
partner_booking_id | обязателен: ключ идемпотентности, уникальный в пределах партнёра, до 64 символов |
seats | от 1 до max_seats_per_booking мероприятия; абсолютный потолок схемы 20 действует в первой фазе, групповые продажи сверх него обсуждаются отдельно |
customer | необязательный: name, email, phone |
Ошибки
| Код | Когда | Что делать |
|---|---|---|
409SEATS_NOT_AVAILABLE | хотя бы одно место занято, ни одно не удержано; список в details.seats | обновить сектор, предложить другие места |
409SALES_PAUSED | продажи по мероприятию приостановлены | показать паузу, читать можно |
409SALES_NOT_OPEN | продажи не открыты, закрыты или мероприятие отменено | снять с продажи до изменения статуса |
409BOOKING_LIMIT_EXCEEDED | мест больше действующего предела; details: {limit, requested} | ограничить корзину по max_seats_per_booking |
409IDEMPOTENCY_CONFLICT | ваш partner_booking_id уже использован с другим телом | новый идентификатор для новой брони |
422SEATS_UNKNOWN | место не существует или принадлежит другому мероприятию; список в details.seats | ошибка на вашей стороне, проверить источник идентификаторов |
422INVALID_PARAMETERS | тело не прошло проверку | разбор по полям в details |
| 403404 | ACCESS_DENIED, PARTNER_DISABLED, EVENT_NOT_FOUND | как у чтений |
/ordersЗаказ до списания денег
Превращает бронь в заказ со статусом pending. Места остаются за заказом до expires_at: платёжное окно партнёра, по умолчанию 5 минут. Дальше вы списываете деньги и зовёте confirm; если эквайер отказал, зовёте cancel. Если не случилось ни того ни другого, заказ истекает сам. Билеты в этом ответе не приходят, они выпускаются на confirm.
Тело запроса
| Поле | Значение |
|---|---|
booking_id | из ответа брони |
partner_order_id | обязателен, до 64 символов, уникален в пределах партнёра; должен существовать до списания: по нему заказ восстанавливается |
customer.email | обязателен: по нему заказ находит поддержка ДСС; если настоящий адрес не передаётся, согласуем уникальный на заказ псевдоним. Данные покупателя ДСС хранит как персональные и не использует для рассылки по партнёрскому каналу |
consent | обязан быть true: согласие покупателя на обработку данных собираете вы |
total_tenge в ответе это ровно та сумма, которую вы списываете за билеты. Ваш сервисный сбор с покупателя сверх неё ДСС не видит, не сверяет и не ограничивает.
Ошибки
| Код | Когда | Что делать |
|---|---|---|
409BOOKING_EXPIRED | срок брони истёк | новая бронь, потом новый заказ |
409BOOKING_NOT_FOUND | такой брони нет или она чужая | проверить booking_id |
409BOOKING_ALREADY_CONFIRMED | по этой брони уже создан заказ с другим partner_order_id | найти его через GET /orders |
409IDEMPOTENCY_CONFLICT | ваш partner_order_id уже использован с другой бронью | новый идентификатор |
403ACCESS_DENIED, PARTNER_DISABLED | грант на мероприятие отозван или доступ приостановлен после брони | остановить продажу по мероприятию |
429PENDING_LIMIT_EXCEEDED | слишком много незакрытых pending-заказов | повторить после Retry-After; закрывать заказы confirm или cancel, не бросать |
422INVALID_PARAMETERS | например consent: false или пустой email | разбор по полям в details |
/orders/{order_id}/confirmОплата подтверждена, выпуск билетов
Зовётся сразу после успешного списания, не из отложенной очереди. payment_info описывает ваш платёж и сохраняется у ДСС для сверки. В ответе билеты: qr_token для турникета и pdf_url для скачивания. Повтор по уже подтверждённому заказу возвращает 200 с теми же билетами; первый принятый payment_info при повторе не перезаписывается.
Тело запроса
| Поле | Значение |
|---|---|
payment_info.amount_tenge | обязан совпадать с total_tenge заказа; при расхождении 422 с обеими суммами в details, заказ остаётся pending, окно идёт, повтор с верной суммой допустим до expires_at |
payment_info.paid_at | момент списания у вас, ISO 8601 |
payment_info.external_payment_id | идентификатор платежа в вашей системе или у эквайера |
payment_info.currency, method | KZT; способ оплаты в свободной форме, необязателен |
Ошибки
| Код | Когда | Что делать |
|---|---|---|
409ORDER_EXPIRED | платёжное окно истекло до confirm, места уже вернулись в продажу | немедленно вернуть покупателю списанное; каждый такой случай считается дефектом интеграции и попадает в сверку |
409ORDER_CANCELLED | заказ уже отменён через cancel (поздний успех эквайера) | вернуть деньги, оформить новый заказ при желании покупателя |
409EVENT_CANCELLED | мероприятие отменено между заказом и оплатой | вернуть деньги |
422INVALID_PARAMETERS | сумма не совпала или тело неполное | исправить и повторить в окне |
404ORDER_NOT_FOUND | заказа нет или он чужой | проверить order_id |
/orders/{order_id}/cancelСписание не удалось
Зовётся при отказе эквайера. Отменяет pending-заказ и возвращает места в продажу сразу, не дожидаясь конца платёжного окна: покупатель, который тут же пробует другую карту, не должен ждать собственные места. Тела запроса нет. Повтор по уже отменённому или истёкшему заказу возвращает 200.
Ошибки
| Код | Когда |
|---|---|
409ORDER_NOT_CANCELLABLE | заказ уже подтверждён; текущий статус в details. Для оплаченного заказа существует только возврат |
404ORDER_NOT_FOUND | заказа нет или он чужой |
/orders?partner_order_id=…Чтение заказа по вашему идентификатору
Ручка восстановления и разбора. Сценарий, ради которого она существует: ответ на POST /orders потерян (таймаут, падение процесса), order_id неизвестен, тело для повтора не сохранилось. Этот запрос отдаёт заказ по единственному, что у вас точно есть: вашему partner_order_id. Дальше confirm или cancel по полученному order_id. Тело такое же, как у создания заказа; после подтверждения в нём есть билеты и те же ссылки на PDF.
Статус expired вычисляется в момент чтения по expires_at: заказ с прошедшим окном читается как expired сразу, независимо от внутренней уборки.
Ошибки
| Код | Когда |
|---|---|
404ORDER_NOT_FOUND | заказа с таким partner_order_id у вас нет |
422INVALID_PARAMETERS | параметр отсутствует или длиннее 64 символов |
/orders/{order_id}/refundвторая фазаОтметка возврата
Недоступна в первой фазе. До неё возврат оформляется обращением в ДСС (контакт и срок обработки фиксируются договором): персонал ДСС гасит билет вручную, деньги покупателю в любом случае возвращаете вы. Во второй фазе ручка гасит перечисленные билеты и освобождает их места; операция идемпотентна по билету. Причина возврата в свободной форме попадает в журнал ДСС.
Ошибки
| Код | Когда |
|---|---|
409TICKET_NOT_REFUNDABLE | билет уже использован на входе, погашен по другой причине или относится к другому заказу; текущий статус в details |
404ORDER_NOT_FOUND | заказа нет или он чужой |
422INVALID_PARAMETERS | пустой список билетов или причина вне длины 1–500 символов |
Состояния брони и заказа
200, а не ошибкой.| Объект | Статус | Значение |
|---|---|---|
| бронь | active | места удержаны до expires_at |
expired | срок вышел, места свободны; повтор POST /bookings с тем же ключом вернёт её в этом состоянии и мест заново не удержит | |
converted | по брони создан заказ, его номер в order_id | |
| заказ | pending | места заперты до expires_at, ждём confirm или cancel |
paid | оплата подтверждена, билеты выпущены, expires_at становится null | |
cancelled | отменён вами через cancel | |
expired | окно прошло без confirm; вычисляется по expires_at при чтении | |
refunded | вторая фаза: все билеты возвращены | |
partially_refunded | вторая фаза: часть билетов возвращена |
Идемпотентность и повторы
Каждый изменяющий вызов безопасен к повтору, и ключи это ваши же идентификаторы. Оба идентификатора обязаны существовать до вызова и не меняться при повторах.
| Вызов | Ключ | Повтор возвращает |
|---|---|---|
POST /bookings | partner_booking_id | 200 и ту же бронь в её текущем состоянии; второй набор мест не удерживается |
POST /orders | partner_order_id | 200 и тот же заказ в его текущем статусе |
confirm, cancel | order_id | 200 с тем же результатом |
refund | ticket_id | успех по уже погашенному билету с той же причиной |
Повтор ключа с другим телом это не повтор, а конфликт. Тот же partner_booking_id с другими местами или тот же partner_order_id с другой бронью отвечает 409 IDEMPOTENCY_CONFLICT, в details лежат сохранённое и присланное значения поля.
Восстановление после потери ответа
Если ответ на POST /orders потерян и order_id неизвестен, есть два равноценных пути: повторить POST /orders с тем же partner_order_id (вернётся тот же заказ) или прочитать его через GET /orders?partner_order_id=…. Дальше confirm или cancel по полученному order_id. Терять заказы навсегда в этой схеме негде.
Повторы на ошибках сети и 5xx
Повторяйте с нарастающей задержкой; рекомендуемый таймаут клиента 10 секунд. Поскольку каждый изменяющий вызов идемпотентен, повтор после таймаута не создаёт второй брони и второго заказа. Пишите X-Request-Id из ответа рядом с телом отказа: по нему ДСС находит запрос в своих журналах.
Изменения мероприятий
Гарантированный канал с первого дня: опрос. Опрашивайте GET /events?updated_since=… не реже раза в 5 минут. Именно так доезжают отмена, перенос, закрытие продаж и отзыв доступа, и именно на этом канале лежит ваша обязанность остановить продажу и начать возвраты по отменённому мероприятию.
- Первый запрос делается без
updated_since. Из ответа берётсяmeta.cursor. - Каждый следующий запрос передаёт этот курсор в
updated_sinceи берёт новый из ответа. Курсор серверный и непрозрачный; собственное время сюда не подставляется, часы двух систем расходятся. - С
updated_sinceответ полный:limitиoffsetигнорируются. Опрос ведётся без фильтровdate_from,venue_id,event_type, иначе перенос мероприятия за пределы фильтра пропал бы молча. - Изменившееся мероприятие остаётся в выдаче со своим новым состоянием. Исчезновение из списка событием не является.
| Что пришло | Что это значит | Что делать |
|---|---|---|
status: cancelled | мероприятие отменено; билеты ДСС гасит, на вход они не проходят | остановить продажу, начать возвраты покупателям |
новая starts_at | перенос | уведомить покупателей, обновить страницу |
status: sales_closed | продажи закрыты (в том числе мероприятие уже идёт или прошло) | снять с продажи |
status: sales_paused | пауза продаж, бронь отвергается | показать паузу, не бронировать |
access: revoked | ДСС отозвала ваш доступ к мероприятию; новые брони ответят 403 | остановить продажу; проданные билеты остаются действительными, а confirm и cancel по уже созданным pending-заказам работают до конца окна |
status: sales_scheduled | грант есть, продажи ещё не открыты, дата в sales_start_at | собрать страницу заранее, бронь до открытия отвечает SALES_NOT_OPEN |
Вебхуки, вторая фаза
Вебхуки booking_expired, event_updated и event_cancelled подключаются во второй фазе как ускоритель и обязательный опрос не отменяют. ДСС вызывает ваш URL и подписывает запрос той же схемой HMAC с теми же тремя заголовками; uri в канонической строке это полный путь вашего URL без хоста. Доставка не менее одного раза: любой ответ кроме 2xx или таймаут означает повтор с нарастающей задержкой, поэтому обработчик обязан быть идемпотентным по booking_id или event_id. URL для них понадобится позже.
| Вебхук | Тело |
|---|---|
booking_expired | booking_id, expired_at; дополнительно event_id, seats |
event_updated | event_id, change_type из rescheduled, updated, sales_paused, sales_resumed; new_date только при rescheduled |
event_cancelled | event_id, cancellation_reason, cancelled_at; билеты ДСС гасит сама, вы начинаете возвраты |
Билеты, QR и PDF
Токен подписан ключом мероприятия, и сканер на входе разбирает именно его. Если закодировать в QR ссылку на страницу билета, контролёр получит отказ QR_INVALID при полностью исправном билете. Это уже случалось на витрине ДСС, поэтому предупреждаем заранее.
- qr_token
- JWT в ASCII, примерно 530–560 символов. Кодировать в байтовом режиме QR с уровнем коррекции M (получится примерно 20-я версия символа); не усекать и не перекодировать. Между чтениями токен стабилен. Значение
nullозначает, что билет погашен и QR рисовать не нужно. - pdf_url
- подписанная ссылка без состояния, действует до окончания мероприятия плюс 30 дней.
GET /ordersвозвращает те же ссылки. Отдавайте её покупателю как ссылку, а не скачивайте PDF пачками со своего сервера: маршрут защищён отдельным лимитом на адрес. - number
- номер билета вида
DSS-TKT-000061, для поддержки и сверки; номер заказа видаDSS-2026-001042 - status
activeдействителен ·usedгость прошёл ·voidпогашен (возврат, отмена, инициатива ДСС) ·expiredмероприятие прошло
Письма покупателю по партнёрскому каналу ДСС не отправляет: доставка билета, QR и PDF на вашей стороне. Покупатель партнёра находится поддержкой ДСС по номеру заказа и по customer.email, поэтому адрес обязателен.
Ошибки
Единый формат: {"error": {"code", "message", "details"}}. Ветвитесь по code, message пишите в журнал: это текст для оператора, а не для покупателя. Неизвестный код обрабатывайте по HTTP-статусу, а не падайте.
| HTTP | Код | Когда |
|---|---|---|
| 401 | INVALID_API_KEY | ключ неизвестен или заголовок отсутствует |
| 401 | INVALID_SIGNATURE | подпись не сошлась, X-Timestamp вне окна или не число |
| 403 | ACCESS_DENIED | на мероприятие нет гранта |
| 403 | PARTNER_DISABLED | доступ партнёра приостановлен ДСС целиком; существующие заказы живут до конца окна |
| 404 | EVENT_NOT_FOUND | мероприятия нет |
| 404 | ORDER_NOT_FOUND | заказа нет или он чужой |
| 409 | SEATS_NOT_AVAILABLE | хотя бы одно место занято, ни одно не удержано; details.seats |
| 409 | SALES_PAUSED | бронь на мероприятие с приостановленными продажами |
| 409 | SALES_NOT_OPEN | бронь на мероприятие, продажи которого не открыты, закрыты или отменены |
| 409 | BOOKING_LIMIT_EXCEEDED | мест больше действующего предела; details: {limit, requested} |
| 409 | IDEMPOTENCY_CONFLICT | ваш ключ с другим телом; details: {field, stored, received} |
| 409 | BOOKING_EXPIRED | бронь истекла |
| 409 | BOOKING_NOT_FOUND | брони не существует |
| 409 | BOOKING_ALREADY_CONFIRMED | по броне уже создан другой заказ |
| 409 | ORDER_EXPIRED | платёжное окно истекло до confirm; немедленно вернуть списанное |
| 409 | ORDER_CANCELLED | confirm по заказу, уже отменённому через cancel |
| 409 | EVENT_CANCELLED | confirm, когда мероприятие отменено между заказом и оплатой |
| 409 | ORDER_NOT_CANCELLABLE | заказ уже подтверждён, отмене не подлежит |
| 409 | TICKET_NOT_REFUNDABLE | билет использован на входе или уже погашен |
| 422 | INVALID_PARAMETERS | тело или параметры не прошли проверку; разбор по полям в details |
| 422 | SEATS_UNKNOWN | место не существует или принадлежит другому мероприятию; details.seats |
| 429 | RATE_LIMIT_EXCEEDED | превышена частота или конкурентность; заголовки X-RateLimit-* |
| 429 | PENDING_LIMIT_EXCEEDED | слишком много незакрытых pending-заказов; повторить после Retry-After |
Словари
| Поле | Значения |
|---|---|
Event.status | sales_scheduled, sales_open, sales_paused, sales_closed, cancelled. Словарь закрыт: внутренние состояния платформы сворачиваются в эти пять, новые появятся только с новой версией контракта |
Event.access | active, revoked |
Event.event_type | concert, sport, theatre, exhibition, conference, festival, kids, other; может быть null |
Event.age_rating | 0+, 6+, 12+, 16+, 18+, 21+; может быть null |
Seat.status | free, held, sold, blocked |
category_code | standard, vip, fan, invitation, service |
Booking.status | active, expired, converted |
Order.status | pending, paid, cancelled, expired; во второй фазе refunded, partially_refunded |
Ticket.status | active, used, void, expired |
payment_info.currency | KZT |
| локализованный текст | объект {"ru", "kk", "en"}; у title все три языка, у description, hall_name и category_name гарантирован только ru |
Песочница и сертификация
ДСС предоставляет отдельный стенд (планируемая доступность с 15 сентября 2026; адрес и ключи pk_sandbox_… передаются техническому контакту), этот документ с OpenAPI-файлом, коллекцию Postman, сгенерированную из него, и тестовое мероприятие с небольшим залом, на котором проходятся все сценарии, включая возврат и повторный проход. Нагрузочные прогоны по стенду только по предварительному согласованию: стенд разделяет машину с другими системами.
Что нужно от Ticketon
- Подтверждение проверочного вектора, до всего остального.
- Технический контакт: кто у вас ведёт интеграцию.
- Две даты: когда ваша разработка готова и когда вы готовы сертифицироваться.
- Диапазон исходящих адресов, если хотите ограничение по адресу.
- URL для вебхуков понадобится позже, ко второй фазе.
Сценарии сертификации
Продуктивные ключи выдаются после того, как эти сценарии пройдены на стенде вашим клиентом. Список можно использовать как собственный чеклист до сертификации.
- Проверочный вектор HMAC сходится.
- Счастливый путь:
events→seats→bookings→orders→confirm, билеты в ответе. - Повтор брони с тем же
partner_booking_idвозвращает ту же бронь, места не удвоены. - Повтор заказа с тем же
partner_order_idвозвращает тот же заказ. - Повтор
confirmотвечает200с теми же билетами. - Бронь истекла:
BOOKING_EXPIRED, места свободны. - Заказ истёк до
confirm:ORDER_EXPIRED. - Отказ эквайера:
cancel, места свободны немедленно. amount_tengeне равен сумме заказа:422с обеими суммами.- Мероприятие без гранта:
403, в списке отсутствует. - Превышение лимита частоты:
429с заголовками. - Отмена мероприятия видна опросом с
updated_since. - QR из
qr_tokenпроходит валидатор на демо-точке, повторный скан отвергается. - Потеря ответа: повтор
POST /ordersиGET /orders?partner_order_id=возвращают тот же заказ, второй не создаётся. - Отзыв гранта виден опросом (
access: "revoked"), бронь по нему отвечает403,confirmпо живомуpendingпроходит. - Бронь сверх предела:
BOOKING_LIMIT_EXCEEDED; бронь наsales_paused:SALES_PAUSED. - Повтор ключа с другим телом:
IDEMPOTENCY_CONFLICT; повтор брони после истечения возвращает её соstatus: expiredи мест не удерживает.
Версии
Добавление полей и кодов ошибок происходит без предупреждения: клиент обязан переживать неизвестные поля и ветвиться по HTTP-статусу на неизвестных кодах. Удаление и переименование только новой версией контракта с уведомлением и окном параллельной работы.
| Версия | Дата | Что изменилось |
|---|---|---|
| 0.2.0 | 02.09.2026 | Первая редакция контракта, выданная партнёрам |
| 0.1.0 | 01.09.2026 | Внутренняя редакция, партнёрам не выдавалась |