Перейти к содержанию

Webhooks: ретраи

Если первая попытка доставки webhook'а не удалась, мы повторяем по заранее заданному графику.

График ретраев

Попытка Задержка от предыдущей
1 — (сразу после события)
2 +30 секунд
3 +2 минуты
4 +10 минут
5 +1 час
6 +6 часов
(всё) + ещё +24 часа после 6-й

После 6 неудач webhook помечается как failed и больше не повторяется автоматически. Суммарное окно — около 31 часа от момента события.

Что считается провалом

Ретрай инициируется при:

  • HTTP 5xx (включая 500, 502, 503, 504, 520+ от Cloudflare).
  • HTTP 429 (rate-limit вашего приёмника).
  • Сетевая ошибка: ECONNREFUSED, ECONNRESET, DNS-фейл, TLS handshake fail.
  • Таймаут: ответ не пришёл за 10 секунд.

Не ретраится:

  • HTTP 4xx кроме 429 — считаем, что у вас баг и ретрай не поможет. Дашборд → Deliveries → можно перезапустить вручную.
  • HTTP 3xx — мы не следуем редиректам. webhook_url должен быть финальным.
  • HTTP 2xx — успех, ретрай не нужен.

Идемпотентность — обязательно

Один и тот же verification.completed для одного verification_id может прилететь несколько раз:

  • Если ваш приёмник ответил 2xx, но мы не успели это зафиксировать до перезапуска (редко, но возможно).
  • При ручном Retry из дашборда.

Поэтому ваш обработчик обязан быть идемпотентным. Простейший способ — UNIQUE constraint по (verification_id, event_type):

def handle(payload: dict) -> None:
    try:
        Delivery.objects.create(
            verification_id=payload["verification_id"],
            event_type=payload["type"],
            payload=payload,
        )
    except IntegrityError:
        # Уже обработали — успех, ничего не делаем.
        return

    # ... ваша основная логика

Ручной перезапуск

Дашборд → WebhooksDeliveries → найти нужную попытку → кнопка Retry.

При ручном Retry счётчик попыток не сбрасывается — это просто ещё одна попытка сверху, не запускающая новый цикл из 6 попыток.

Что делать, если webhook'и не доходят

  1. Проверьте в дашборде Deliveries — что наша сторона возвращает? Какой HTTP-код пришёл от вашего эндпоинта?
  2. Если стабильно 5xx — у вас в приёмнике баг.
  3. Если стабильно 4xx — мы передаём что-то, что ваш парсер не принимает.
  4. Если timeout — ваш приёмник отвечает медленнее 10 секунд. Перенесите тяжёлую обработку в фон, отвечайте сразу 200 OK.
  5. Если connection refused / DNS failwebhook_url стал недоступен. Создавайте новые верификации с актуальным URL.

Фоллбек через поллинг

Если вы пропустили webhook (например, ваш приёмник был в офлайне всю 31-часовую цепочку ретраев) — состояние верификации всё равно сохранено в БД. Получите его через:

curl https://app.truenum.ru/api/v1/verifications/<id>/ \
  -H 'Authorization: Bearer tn_live_<prefix>_<secret>'

Поле status будет completed / expired / failed. Поле completed_at и caller_id_received — заполнены если status=completed.

Не злоупотребляйте поллингом — у эндпоинта тот же лимит 120 req/min.

Тонкая настройка graceful degradation

Рекомендуемая схема приёма:

  1. Acknowledge сразу: получили POST → проверили подпись → положили в очередь / БД → ответили 200 OK. ≤500 мс.
  2. Обработали в фоне: воркер берёт задачу из очереди, делает бизнес-логику. При ошибке — ретрай внутри вашей системы, не полагаясь на наши.
  3. Не возвращайте 5xx если можете обработать позже. Если вы ответили 5xx 6 раз подряд — webhook потерян, придётся восстанавливать через дашборд.