SportAPI Документация
RU
C Документация продуктаCoupon API
v1
Услуга и цены ↗ Получить доступ ↗
Coupon API / Восстановление callback

Сквозной пример: восстановление после пропущенного 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. Они возвращают текущее состояние купона.

Для каждого купона:

  1. найдите локальную запись по coupon_code;
  2. обновите поля купона;
  3. переберите весь events_data;
  4. обновите ставки по coupon_code + id;
  5. определите, требуется ли новое финансовое действие;
  6. зафиксируйте данные и финансовую операцию одной транзакцией.

Соответствие 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

и предыдущий финальный результат уже был финансово обработан:

  1. проверьте, выполнялось ли повторное списание для этого перехода;
  2. если нет — один раз повторно спишите amount;
  3. переведите локальный купон в ожидание нового результата;
  4. не выполняйте финальное начисление при статусе 15;
  5. продолжайте проверять купон по coupon_code;
  6. после нового финального статуса один раз начислите новое real_win.

Если повторное списание уже было выполнено через callback или предыдущий polling, не выполняйте его снова.

Особенность проигравшего экспресса

Проигрыш экспресса может быть известен до расчета всех ставок:

status купона = 4
status ставок = [1, 2, 0]

Финансовый результат уже является проигрышем.

Позднее полная модель может стать:

status купона = 4
status ставок = [1, 2, 1]

Обновите последнюю ставку, но не создавайте вторую финансовую операцию проигрыша.

Если callback и API расходятся

Не определяйте актуальность только по порядку прихода HTTP-запросов.

Например, после polling может задержаться более старый callback. Если состояния отличаются:

  1. не выполняйте финансовое действие немедленно;
  2. запросите текущую модель через GET /api/partner/coupons/get;
  3. синхронизируйте локальный купон с ответом API;
  4. примените только отсутствующую финансовую операцию.

calculate_date полезна для сверки, но бизнес-решение должно учитывать статус купона, статусы ставок и уже обработанный цикл расчета.

Шаг 4. Вернитесь к обычной работе

После сверки:

  1. убедитесь, что callback endpoint стабильно отвечает;
  2. проверьте новые callback на HMAC;
  3. продолжайте дедупликацию по batchId;
  4. запустите регулярный резервный polling;
  5. используйте перекрывающиеся временные окна.

Рекомендуемый пример:

каждые 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.
  • Обрабатываются все купоны и ставки.
  • id API сопоставляется с uuid callback.
  • Финансовая идемпотентность не зависит только от batchId.
  • Статус 15 обработан отдельным циклом.
  • Повтор процедуры не изменяет баланс.
  • Callback снова отвечает HTTP 200.
  • Резервный polling снова работает с перекрытием.

Подробнее:

Следующий раздел: «Справочник endpoint».