Сквозной пример: восстановление после пропущенного callback
Callback ускоряет получение результатов, но не является единственным источником данных. Актуальное состояние купонов сохраняется в Системе расчета купонов SportAPI и доступно через клиентский API.
Если callback не был доставлен или обработан, партнер должен:
восстановить endpoint
↓
определить период сбоя
↓
получить актуальные купоны через API
↓
идемпотентно обновить купоны и ставки
↓
выполнить только отсутствующие финансовые операции
↓
вернуться к обычному callback + polling
Когда нужен этот сценарий
Руководство применяется, если:
- callback endpoint был недоступен;
- запрос превысил таймаут 10 секунд;
- приложение вернуло временную HTTP-ошибку;
- callback был отклонен из-за неверной подписи или IP-фильтра;
- приложение вернуло HTTP
200до надежного сохранения данных; - был возвращен
success: falseили неполныйprocessed; - все автоматические попытки доставки исчерпаны;
- внутренний обработчик партнера временно не обновлял купоны;
- необходимо проверить, не были ли пропущены расчетные результаты.
Сначала определите тип проблемы
| Ситуация | Автоматический повтор |
|---|---|
| Timeout или транспортная ошибка | Да |
HTTP 500, 502, 503, 504 | Да |
HTTP 200 с пустым телом | Нет: доставка считается успешной |
HTTP 200, success: true, processed = couponCount | Нет: доставка считается успешной |
HTTP 200, success: false | Нет: финальная ошибка |
HTTP 200, processed < couponCount | Нет: финальная ошибка |
HTTP 401 или 403 | Нет: финальная ошибка |
HTTP 201, 202, 204 | Нет: успехом не считаются, но автоматически не повторяются |
| Любой другой HTTP-код | Нет: финальная ошибка |
Если обработчик временно не может сохранить пакет, он должен вернуть 500, 502, 503 или 504. Не используйте HTTP 200 с success: false в расчете на повтор.
Расписание автоматических попыток
При временной ошибке выполняется не более пяти HTTP-отправок:
1-я: сразу
2-я: через 1 минуту
3-я: через 5 минут
4-я: через 15 минут
5-я: через 1 час
После исчерпания попыток автоматическая доставка конкретного пакета прекращается. Его данные нужно восстановить через методы чтения API.
Шаг 1. Восстановите callback endpoint
Проверьте:
- endpoint доступен извне;
- production использует HTTPS;
- маршрут принимает
POSTиapplication/json; - тело читается как исходные байты;
X-Coupon-Signatureпроверяется до разбора JSON;- используется правильный
callback_secret; - IP allowlist, если он включен, содержит актуальный адрес от менеджера;
- транзакция фиксируется до ответа HTTP
200; - полный обработчик укладывается в 10 секунд;
batchIdимеет уникальное ограничение.
Не отключайте проверку HMAC для ускорения восстановления.
Если пакет уже полностью сохранен и пришел повторно с тем же batchId, не повторяйте обработку и верните HTTP 200.
Шаг 2. Определите границы сбоя
Зафиксируйте:
- время последнего заведомо успешного callback;
- время восстановления endpoint;
- продолжительность недоступности;
- были ли успешные запуски резервного polling;
- какие купоны оставались активными или ожидали нового результата;
- какие
batchIdбыли сохранены до сбоя.
Добавьте небольшой временной запас к началу периода. Перекрытие безопасно, если обработка идемпотентна.
Как выбрать способ восстановления
| Ситуация | Рекомендуемый метод |
|---|---|
| Перерыв не более 120 минут | GET /api/partner/coupons/calculated |
| Перерыв более 120 минут, коды известны | POST /api/partner/coupons/results с coupon_ids |
| Нужно восстановить купоны, созданные во время сбоя | POST /api/partner/coupons/results с датами |
| Один проблемный купон | GET /api/partner/coupons/get |
| Нужно сверить незавершенные купоны | GET /api/partner/coupons/active |
Все эти методы требуют Bearer JWT.
Сценарий A. Перерыв не превышал 120 минут
Предположим:
callback не работал 40 минут
текущее время: 14:00
Запросите рассчитанные купоны за 50 минут:
BASE_URL="https://coupon-api.example.com"
TOKEN="<jwt-token>"
curl --request GET \
--url "$BASE_URL/api/partner/coupons/calculated?time=50" \
--header "Accept: application/json" \
--header "Authorization: Bearer $TOKEN"
Дополнительные 10 минут перекрывают границу сбоя.
Метод:
- отбирает по времени окончательного расчета;
- не зависит от времени создания купона;
- не возвращает активные купоны;
- возвращает массив полных моделей прямо в
body; - ограничивает окно максимум 120 минутами.
Пример ответа
{
"code": 1,
"body": [
{
"coupon_code": "000000000272",
"amount": 10,
"real_win": 18.5,
"calculate_coef": 1.85,
"status": 2,
"calculate_date": 1784973600000,
"events_data": [
{
"id": 912,
"status": 1,
"calc_coef": 1.85,
"calculate_date": 1784973600000
}
]
}
],
"error_code": null,
"error_message": null,
"date": 1784973600100,
"time_ms": 12,
"path": "/api/partner/coupons/calculated"
}
Обработайте весь массив body.
Пустой массив при code = 1 является успешным результатом и означает, что подходящих купонов в окне нет.
Сценарий B. Перерыв превышал 120 минут
Один вызов /calculated не может покрыть период больше 120 минут.
Получите из локальной базы коды купонов:
- которые были активны перед сбоем;
- по которым нет подтвержденного актуального финального результата;
- которые находятся в статусе ожидания после
15; - состояние которых вызывает сомнение.
Разделите список на пакеты не более 100 кодов.
curl --request POST \
--url "$BASE_URL/api/partner/coupons/results" \
--header "Authorization: Bearer $TOKEN" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"coupon_ids": [
"000000000272",
"000000000273",
"000000000274"
]
}'
Ответ:
{
"code": 1,
"body": {
"query_type": "ids",
"coupons": []
},
"error_code": null,
"error_message": null,
"date": 1784973600100,
"time_ms": 10,
"path": "/api/partner/coupons/results"
}
Особенности:
- неизвестные и чужие коды пропускаются;
- порядок ответа может отличаться от
coupon_ids; - количество результатов может быть меньше количества кодов;
- сопоставлять нужно по
coupon_code, а не по индексу.
Если часть кодов не была сохранена
Чтобы восстановить купоны, созданные во время сбоя, используйте поиск по периоду создания:
curl --request POST \
--url "$BASE_URL/api/partner/coupons/results" \
--header "Authorization: Bearer $TOKEN" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"start_date": 1784880000000,
"end_date": 1784966400000
}'
Один интервал:
- должен быть положительным;
- не может превышать 24 часа;
- относится ко времени создания, а не расчета.
Для длительного периода выполните несколько запросов по интервалам не более 24 часов.
Важно. Поиск по датам создания не найдет старый купон только потому, что он был рассчитан во время сбоя. Для старых известных купонов используйте
coupon_ids.
Сценарий C. Один проблемный купон
COUPON_CODE="000000000272"
curl --request GET \
--url "$BASE_URL/api/partner/coupons/get?coupon_code=$COUPON_CODE" \
--header "Accept: application/json" \
--header "Authorization: Bearer $TOKEN"
Этот метод возвращает одну полную актуальную модель непосредственно в body.
Используйте его:
- при обращении пользователя;
- при расхождении callback и локальной базы;
- для получения
amount, которого нет в callback; - для точечной проверки после статуса
15.
Сверка активных купонов
После восстановления запросите:
curl --request GET \
--url "$BASE_URL/api/partner/coupons/active" \
--header "Accept: application/json" \
--header "Authorization: Bearer $TOKEN"
Сравните список с локальными купонами, которые считаются активными.
Если завершенный купон остался активным только в локальной базе, получите его по coupon_code или через /results.
Не используйте /active вместо поиска рассчитанных результатов: завершенные купоны в нем отсутствуют.
Шаг 3. Идемпотентно примените актуальное состояние
Ответы чтения API не содержат batchId. Они возвращают текущее состояние купона.
Для каждого купона:
- найдите локальную запись по
coupon_code; - обновите поля купона;
- переберите весь
events_data; - обновите ставки по
coupon_code + id; - определите, требуется ли новое финансовое действие;
- зафиксируйте данные и финансовую операцию одной транзакцией.
Соответствие ID ставки:
API: events_data[].id = 912
callback: events_data[].uuid = "912"
Не создавайте отдельные записи для одного ID только из-за различия типа.
Отдельная идемпотентность финансовых операций
Уникальность batchId защищает только от повторной доставки одного callback.
После восстановления возможна последовательность:
1. Результат получен через polling.
2. Партнер начислил real_win.
3. Позднее пришел callback с неизвестным партнеру batchId.
4. Callback содержит уже примененный расчетный результат.
Если проверять только batchId, появится второе начисление.
Финансовому действию нужен собственный уникальный ключ, например:
coupon_code + settlement_generation + operation_type
Пример:
000000000272:0:final_credit
При повторном появлении того же финансового состояния:
- обновите необходимые данные;
- зарегистрируйте новый
batchId, если это новый callback; - не повторяйте списание или начисление.
Как применять статусы
| Статус купона | Значение | Действие при восстановлении |
|---|---|---|
0 | Купон активен или экспресс рассчитан частично | Обновить данные, выплату не выполнять. |
2 | Выигрыш | Один раз начислить real_win, если этот результат еще не обработан. |
4 | Проигрыш | Зафиксировать результат; начисление равно 0. |
8 | Возврат | Один раз начислить real_win, если этот результат еще не обработан. |
15 | Купон возвращен на перерасчет | Выполнить отдельный сценарий перерасчета. |
Не определяйте результат только по real_win. Всегда проверяйте status.
Восстановление статуса 15
Если API возвращает:
status купона = 15
status соответствующей ставки = 4
и предыдущий финальный результат уже был финансово обработан:
- проверьте, выполнялось ли повторное списание для этого перехода;
- если нет — один раз повторно спишите
amount; - переведите локальный купон в ожидание нового результата;
- не выполняйте финальное начисление при статусе
15; - продолжайте проверять купон по
coupon_code; - после нового финального статуса один раз начислите новое
real_win.
Если повторное списание уже было выполнено через callback или предыдущий polling, не выполняйте его снова.
Особенность проигравшего экспресса
Проигрыш экспресса может быть известен до расчета всех ставок:
status купона = 4
status ставок = [1, 2, 0]
Финансовый результат уже является проигрышем.
Позднее полная модель может стать:
status купона = 4
status ставок = [1, 2, 1]
Обновите последнюю ставку, но не создавайте вторую финансовую операцию проигрыша.
Если callback и API расходятся
Не определяйте актуальность только по порядку прихода HTTP-запросов.
Например, после polling может задержаться более старый callback. Если состояния отличаются:
- не выполняйте финансовое действие немедленно;
- запросите текущую модель через
GET /api/partner/coupons/get; - синхронизируйте локальный купон с ответом API;
- примените только отсутствующую финансовую операцию.
calculate_date полезна для сверки, но бизнес-решение должно учитывать статус купона, статусы ставок и уже обработанный цикл расчета.
Шаг 4. Вернитесь к обычной работе
После сверки:
- убедитесь, что callback endpoint стабильно отвечает;
- проверьте новые callback на HMAC;
- продолжайте дедупликацию по
batchId; - запустите регулярный резервный polling;
- используйте перекрывающиеся временные окна.
Рекомендуемый пример:
каждые 5 минут:
GET /api/partner/coupons/calculated?time=10
Не перемещайте локальное окно polling вперед, если запрос завершился транспортной или серверной ошибкой. Сначала успешно повторите проверку с перекрытием.
Как понять, что восстановление завершено
Проверьте:
- все купоны периода сопоставлены по
coupon_code; - все локально активные купоны сверены;
- все элементы
events_dataобновлены; - нет необработанных финальных статусов;
- нет статусов
15без запланированной повторной проверки; - для каждой финансовой операции существует уникальная запись;
- повторный запуск процедуры не меняет баланс повторно;
- новые callback принимаются и получают HTTP
200; - резервный polling снова выполняется по расписанию.
Последний пункт особенно важен: процедура восстановления должна быть идемпотентной. Ее повторный запуск с теми же данными не должен создавать новые списания или начисления.
Типичные ошибки
- Запрашивать
/calculated?time=120и считать, что он покрывает многодневный сбой. - Искать результаты только по времени создания купона.
- Сопоставлять ответ
/resultsпо позиции массива. - Обрабатывать только первый купон или первую ставку.
- Создавать отдельную запись для callback и polling.
- Считать каждый неизвестный
batchIdновым финансовым результатом. - Повторно начислять уже примененный через polling результат.
- Игнорировать статус
15. - Перезаписывать новое состояние более старым callback.
- Возвращать HTTP
200до надежного сохранения пакета. - Отключать HMAC во время аварийного восстановления.
Краткий runbook
1. Исправить callback endpoint.
2. Определить начало и конец сбоя.
3. Если сбой <= 120 минут:
вызвать /calculated с запасом.
4. Если сбой > 120 минут:
запросить известные coupon_code пакетами до 100.
5. При необходимости восстановить созданные купоны окнами до 24 часов.
6. Сверить /active.
7. Upsert купонов по coupon_code.
8. Upsert ставок по coupon_code + id.
9. Применить только отсутствующие финансовые операции.
10. Возобновить callback и перекрывающий polling.
Контрольный список
- Причина пропуска определена.
- HMAC-проверка не отключалась.
- Период сбоя известен.
- Короткий сбой покрыт окном не более 120 минут.
- Длительный сбой восстановлен по известным
coupon_code. - Запросы по кодам разделены максимум по 100 элементов.
- Периоды создания разделены максимум по 24 часа.
- Ответы сопоставляются по
coupon_code. - Обрабатываются все купоны и ставки.
idAPI сопоставляется сuuidcallback.- Финансовая идемпотентность не зависит только от
batchId. - Статус
15обработан отдельным циклом. - Повтор процедуры не изменяет баланс.
- Callback снова отвечает HTTP
200. - Резервный polling снова работает с перекрытием.
Подробнее:
- «Повторы и идемпотентность»;
- «Резервный polling»;
- «Получение купонов списком и по периоду»;
- «Активные и недавно рассчитанные купоны».
Следующий раздел: «Справочник endpoint».