Webhooks — проектирование событийных интеграций

Урок 7 из 8

Урок 07.07: Webhooks — проектирование событийных интеграций

Цель урока

Разобраться, как устроены webhook-интеграции («обратные вызовы по HTTP»): чем они отличаются от polling и брокеров сообщений, как проектировать подписку на события, проверять подлинность входящих запросов и обеспечивать надёжную обработку при повторных доставках. Особый фокус: разбор реальной катастрофы с дублированием платежа из-за неправильной обработки webhook, а также описание webhook-событий в OpenAPI 3.1.


Ключевые понятия

Термин Определение
Webhook HTTP-запрос (обычно POST), который система-источник отправляет на заранее зарегистрированный URL при возникновении события — «обратный вызов»
Callback URL / Endpoint URL приёмной системы, на который источник отправляет webhook
Событие (Event) Факт, произошедший в системе-источнике (task.created, payment.succeeded)
Payload Тело webhook-запроса — данные о событии в формате JSON
Подписка (Subscription) Регистрация: «отправляй мне события типов X, Y, Z на этот URL»
Endpoint Verification Проверка, что приёмный URL реально принадлежит подписчику (challenge-response)
Подпись (Signature) HMAC-хэш payload'а с секретным ключом — доказывает, что запрос отправил легитимный источник
Event ID Уникальный идентификатор события — основа для дедупликации на стороне приёмника
Idempotency (на приёмнике) Свойство обработчика: повторная доставка того же события не создаёт дублирующих эффектов
Retry Policy Правила повторной отправки webhook, если приёмник не ответил 2xx
At-least-once delivery Гарантия: событие будет доставлено хотя бы один раз, возможно — несколько
Dead Letter (для webhook) Список событий, которые источник не смог доставить после всех retry — обычно доступен в дашборде провайдера

1. Что такое Webhook и почему он нужен

1.1. Аналогия: webhook — это «обратный звонок», а не «звонок в справочную»

Представьте, что вы заказали пиццу и ждёте, когда она будет готова. Есть два варианта:

  • Polling («справочная»): вы каждые 2 минуты звоните в пиццерию и спрашиваете «готово?». 99% звонков — впустую, но зато вы точно не пропустите момент готовности (с задержкой до 2 минут).
  • Webhook («обратный звонок»): вы оставляете номер телефона, и пиццерия сама звонит вам, когда пицца готова. Ноль лишних запросов, событие приходит почти мгновенно.

Webhook — это «обратный звонок» в мире HTTP: система-источник (пиццерия) делает исходящий HTTP-запрос на URL системы-получателя (ваш телефон), когда что-то произошло.

1.2. Зачем аналитику знать webhooks

Webhooks — один из самых частых видов реальных интеграций:

  • Платёжная система уведомляет интернет-магазин: «оплата прошла» (payment.succeeded)
  • CRM уведомляет внешний сервис: «создан новый лид»
  • GitHub уведомляет CI/CD: «новый push в репозиторий»
  • Telegram-бот получает webhook при новом сообщении пользователю
  • 1С обменивается событиями с внешним складом через HTTP-callback

Когда аналитик пишет ТЗ на такую интеграцию, именно от него зависит, будут ли в требованиях прописаны: проверка подписи, обработка дублей, поведение при недоступности приёмника. Если этого нет в ТЗ — разработчик сделает «как получится», и систему ждут проблемы уровня катастрофы из раздела 5.

1.3. Webhook vs Polling vs WebSocket vs SSE

Критерий Webhook Polling WebSocket SSE (Server-Sent Events)
Кто инициирует соединение Источник делает HTTP-запрос приёмнику Приёмник периодически опрашивает источник Клиент открывает persistent-соединение Клиент открывает persistent HTTP-соединение
Реал-тайм Почти мгновенно (зависит от источника) Задержка = интервал опроса Мгновенно, двусторонне Мгновенно, односторонне (сервер → клиент)
Нагрузка на источник Низкая (1 запрос = 1 событие) Высокая (много «пустых» запросов) Средняя (держит соединения открытыми) Средняя
Требования к приёмнику Нужен публичный HTTPS-endpoint, который всегда доступен Нет — приёмник сам стучится Нужна поддержка WS-протокола Нужна поддержка SSE на клиенте
Типичный пример Stripe → магазин: payment.succeeded Старый бот: «проверять новые заказы раз в минуту» Чат, биржевые котировки Лента уведомлений в браузере

Для аналитика: если в ТЗ написано «система Б должна узнавать об изменениях в системе А почти мгновенно, без постоянного опроса» — это требование к webhook-интеграции. Если же система Б — это, например, десктопное приложение за NAT без публичного адреса, webhook physически невозможен, и нужен polling или брокер сообщений (см. Урок 07.06).


2. Анатомия webhook-запроса

2.1. HTTP-запрос: что приходит на endpoint

Источник отправляет обычный POST-запрос с JSON-телом:

POST /webhooks/task-manager HTTP/1.1
Host: partner-system.example.com
Content-Type: application/json
X-Webhook-Id: evt_8f3a1c2b
X-Webhook-Signature: sha256=4f6b3c2e8a1d...
X-Webhook-Timestamp: 1718194800
User-Agent: TaskManager-Webhooks/1.0

{
    "event_id": "evt_8f3a1c2b",
    "event_type": "task.created",
    "occurred_at": "2026-06-12T10:00:00Z",
    "data": {
        "task_id": 4521,
        "title": "Подготовить отчёт",
        "created_at": "2026-06-12T10:00:00Z"
    }
}

Это та же модель TaskCreatedEvent, которая описывается через oneOf + discriminator в Уроке 07.02 — webhook-payload и есть конкретная JSON-схема из этого oneOf.

2.2. Конверт события (Event Envelope)

Хороший webhook-payload разделяет метаданные о событии и данные о сущности:

Поле Назначение
event_id Уникальный ID самого события — основа для дедупликации (раздел 5.2)
event_type Тип события (task.created, payment.succeeded) — определяет структуру data
occurred_at Когда событие произошло в системе-источнике (не когда отправлен webhook!)
data Полезная нагрузка — сущность, к которой относится событие

Антипаттерн, который часто встречается в плохо спроектированных API: вместо конверта в payload присылают «голую» сущность без event_id и event_type. Тогда приёмник не может ни определить тип события без анализа структуры, ни дедуплицировать повторные доставки.


3. Жизненный цикл webhook-подписки

3.1. Регистрация endpoint'а

Приёмник должен зарегистрировать свой URL и список событий, на которые он подписывается:

POST /api/v1/webhook-subscriptions
Content-Type: application/json
Authorization: Bearer <token>

{
    "url": "https://partner-system.example.com/webhooks/task-manager",
    "events": ["task.created", "task.updated", "task.deleted"],
    "secret": "whsec_3f8a1c..."
}

secret — общий секрет для подписи (раздел 4.1). Он генерируется один раз и хранится только на сервере источника и в защищённой конфигурации приёмника — никогда не передаётся в открытом виде после первоначальной настройки.

3.2. Endpoint Verification (challenge-response)

Перед тем как начать слать реальные события, источник должен убедиться, что URL действительно принадлежит подписчику (а не указан по ошибке или злонамеренно — иначе можно «заDDoSить» чужой сервер чужими webhook'ами).

Типичная схема (используется Slack, Stripe и др.):

  1. Источник отправляет на новый URL специальный запрос с одноразовым токеном:
    { "type": "endpoint.verification", "challenge": "a1b2c3d4e5f6" }
    
  2. Приёмник должен вернуть этот challenge обратно (например, в теле ответа или заголовке) в течение ограниченного времени.
  3. Только после успешной верификации подписка переходит в статус active.

Для аналитика: если в системе есть UI для управления подписками, в нём обязательно должен отображаться статус (pending_verification / active / failed / disabled) — иначе пользователь не поймёт, почему события не приходят.

3.3. Управление подпиской: фильтры и версии событий

Хорошо спроектированный webhook-сервис позволяет:

  • Подписываться выборочно на типы событий (а не «на всё подряд» — это лишняя нагрузка и риски безопасности)
  • Иметь несколько подписок с разными URL под разные цели (например, отдельный endpoint для аналитики, отдельный — для уведомлений)
  • Версионировать структуру payload (event_type: task.created.v2) — без этого любое изменение формата события ломает всех подписчиков одновременно, аналогично проблеме версионирования REST API из Урока 07.01

4. Безопасность webhook-интеграций

Webhook — это входящий запрос из интернета на ваш сервер. Без проверки подлинности любой человек, узнавший URL, может отправить поддельный webhook (например, «оплата прошла» без реальной оплаты).

4.1. Подпись запроса (HMAC-SHA256)

Источник подписывает тело запроса общим секретом и передаёт подпись в заголовке:

X-Webhook-Signature: sha256=4f6b3c2e8a1d9f0e7c5b3a2d1e8f6c4b...

Алгоритм проверки на стороне приёмника:

  1. Взять «сырое» (raw) тело запроса до парсинга JSON (важно: после JSON.parse → JSON.stringify байты могут отличаться, и подпись не совпадёт)
  2. Вычислить HMAC-SHA256(secret, raw_body)
  3. Сравнить с значением из X-Webhook-Signatureс использованием constant-time сравнения (crypto.timingSafeEqual), чтобы не дать возможность подобрать подпись по времени ответа
  4. Если подписи не совпадают — вернуть 401 Unauthorized и не обрабатывать payload
# Псевдокод проверки подписи
expected = HMAC_SHA256(secret_key, raw_request_body)
received = headers["X-Webhook-Signature"].replace("sha256=", "")

if not constant_time_equals(expected, received):
    return 401 Unauthorized
# подпись верна — можно обрабатывать payload

Для аналитика: в требованиях к интеграции должно быть явно указано: «входящие webhook должны проверяться по подписи HMAC-SHA256, секрет хранится в защищённом хранилище (Vault/Secrets Manager), запросы без валидной подписи или с истёкшим timestamp отклоняются с кодом 401». Без этого пункта проверка подписи может быть «забыта» при реализации.

4.2. Защита от replay-атак: проверка timestamp

Даже с корректной подписью злоумышленник, перехвативший один легитимный webhook, может отправлять его повторно (replay attack) — подпись останется валидной, потому что она считается от тех же данных.

Решение: в payload или заголовок добавляется X-Webhook-Timestamp, который тоже входит в подписываемые данные. Приёмник проверяет:

if abs(current_time - timestamp) > 300 секунд:
    return 401  # запрос слишком старый — возможен replay

4.3. Дополнительные меры

Мера Когда нужна
IP allowlisting Если источник публикует фиксированный список IP-адресов (например, у Stripe и GitHub такие списки есть) — дополнительный, но не единственный слой защиты
mTLS (mutual TLS) Для интеграций между корпоративными системами (банки, госсистемы) — обе стороны предъявляют сертификаты
Отдельный секрет на каждую подписку Чтобы компрометация одного приёмника не давала доступ к подделке webhook для других подписчиков

4.4. Чек-лист безопасности webhook

Проверка
1 Подпись (HMAC) проверяется до обработки payload
2 Секрет хранится в Secrets Manager, не в коде/репозитории
3 Проверяется timestamp (защита от replay), окно — не более 5 минут
4 Сравнение подписи — constant-time
5 Endpoint доступен только по HTTPS
6 На запрос без валидной подписи — 401, без подробностей в ответе (не подсказывать атакующему, что не так)

5. ⚠️ КАТАСТРОФА: Двойная отправка заказа из-за дублирующегося webhook

5.1. Реальная история: «Покупатель оплатил один раз — заказ собрали и отправили дважды»

Контекст: интернет-магазин принимает оплату через внешнюю платёжную систему. После успешной оплаты платёжная система отправляет webhook payment.succeeded, и магазин по этому событию: списывает товар со склада, создаёт заявку на доставку и отправляет покупателю email с подтверждением.

Реализация обработчика (как было сделано):

POST /webhooks/payments
  1. Проверить подпись                         (~5 мс)
  2. Найти заказ по order_id                   (~20 мс)
  3. Списать товар со склада (вызов Складской системы) (~2000 мс)
  4. Создать заявку в Службе доставки (внешний вызов)   (~2500 мс)
  5. Отправить email через SMTP                (~1500 мс)
  6. Вернуть 200 OK
  ИТОГО: ~6 секунд

Что произошло:

  1. Платёжная система отправила webhook payment.succeeded для заказа №10532.
  2. Магазин начал обработку (склад → доставка → email) — это заняло 6 секунд.
  3. У платёжной системы таймаут ожидания ответа — 5 секунд. Не получив 200 OK за 5 секунд, она посчитала доставку неуспешной.
  4. Платёжная система повторила webhook (retry #1) через 10 секунд.
  5. К этому моменту первая обработка ещё не завершилась (склад был «подвисшим» из-за нагрузки) — но запрос retry #1 запустил вторую, параллельную обработку того же события.
  6. В итоге: товар списан со склада два раза (на складе образовался отрицательный остаток), создано две заявки на доставку, покупателю пришло два письма.

5.2. Sequence-диаграмма катастрофы

@startuml
participant "Платёжная\nсистема" as PSP
participant "Webhook-\nобработчик" as Handler
participant "Склад" as Warehouse
participant "Доставка" as Delivery

PSP -> Handler: POST /webhooks/payments\n(event_id=evt_001)
activate Handler
Handler -> Warehouse: списать товар
activate Warehouse
note right: 2 секунды...

PSP --> PSP: timeout 5 сек, нет ответа
PSP -> Handler: RETRY: POST /webhooks/payments\n(event_id=evt_001, тот же payload)
activate Handler #LightCoral
note right of Handler: Второй обработчик\nстартует параллельно!\nНет проверки event_id

Warehouse --> Handler: ok (списано #1)
deactivate Warehouse
Handler -> Delivery: создать заявку #1

Handler -> Warehouse: списать товар (повторно!)
activate Warehouse #LightCoral
Warehouse --> Handler: ok (списано #2 — минус на складе)
deactivate Warehouse
Handler -> Delivery: создать заявку #2 (дубль)
deactivate Handler
deactivate Handler
@enduml

5.3. Бизнес-последствия

  • Покупатель получил два письма с подтверждением — потерял доверие («магазин ничего не контролирует»)
  • Создано два заказа на доставку — расходы на логистику ×2
  • Отрицательный остаток на складе — некорректные данные для следующего покупателя («товар в наличии», хотя его уже нет)
  • Финансовый отдел зафиксировал расхождение между «оплачено один раз» и «отгружено два раза»

5.4. Анатомия ошибки: что было упущено

Что было упущено К какому разделу относится
1 Обработчик не отвечал 200 OK немедленно — синхронно делал всю тяжёлую работу Раздел 5.5
2 Не было дедупликации по event_id — повторная доставка обработана как новое событие Раздел 5.6
3 Webhook-провайдер использует at-least-once доставку — дубли это норма, а не баг провайдера Раздел 5.6

5.5. Как правильно: быстрый ACK + асинхронная обработка

Принцип: обработчик webhook должен сразу (за миллисекунды) сохранить событие и ответить 200 OK, а тяжёлую обработку выполнить асинхронно — например, положив задачу в очередь (см. Урок 07.06).

POST /webhooks/payments
  1. Проверить подпись                                (~5 мс)
  2. Проверить event_id на дубликат (см. 5.6)         (~5 мс)
  3. Сохранить событие в таблицу webhook_events       (~10 мс)
  4. Поставить задачу в очередь на обработку          (~5 мс)
  5. Вернуть 200 OK                                    (~25 мс ИТОГО)

  --- асинхронно, в очереди ---
  6. Списать товар, создать заявку на доставку, отправить email

Теперь даже если асинхронная обработка займёт 6 секунд — платёжная система получит 200 OK за 25 мс и не станет повторять запрос.

5.6. Как правильно: дедупликация по event_id

Даже с быстрым 200 OK дубликаты возможны (сетевые сбои, retry после потерянного ответа). Поэтому обработчик должен быть идемпотентным:

-- Таблица для дедупликации
CREATE TABLE webhook_events (
    event_id   VARCHAR(64) PRIMARY KEY,
    event_type VARCHAR(64),
    received_at TIMESTAMP,
    status     VARCHAR(16)  -- pending | processed | failed
);
INSERT INTO webhook_events (event_id, event_type, received_at, status)
VALUES ($1, $2, now(), 'pending')
ON CONFLICT (event_id) DO NOTHING;

-- если строка не была вставлена (конфликт) — событие уже видели,
-- сразу вернуть 200 OK без повторной обработки

Это тот же принцип Idempotency-Key, который разбирается для исходящих запросов в Уроке 07.05 — здесь он применяется к входящим webhook.

5.7. Как аналитик может предотвратить такую катастрофу

В ТЗ на webhook-интеграцию должно быть явно прописано:

  1. Контракт ответа: обработчик обязан вернуть 2xx за не более N секунд (узнать у провайдера его timeout — обычно 3-10 сек)
  2. Архитектурное требование: тяжёлая бизнес-логика выполняется асинхронно, после немедленного 200 OK
  3. Идемпотентность: обязательная дедупликация по event_id, дублирующиеся события игнорируются молча (с логированием)
  4. Поведение при ошибке: если асинхронная обработка упала — что происходит? (retry из локальной очереди, алерт, ручная обработка из DLQ)

6. Webhooks в OpenAPI 3.1

6.1. Два механизма: webhooks и callbacks

OpenAPI различает два похожих, но разных понятия:

webhooks (top-level, начиная с 3.1) callbacks (на уровне операции, с 3.0)
Что описывает Все события, которые API может прислать подписчику, независимо от конкретного запроса Callback, привязанный к конкретной операции («дай мне URL — я вызову его, когда выполню твою задачу»)
Пример «Этот сервис умеет слать task.created, task.updated, payment.succeeded» «При POST /reports укажи callbackUrl — туда придёт результат генерации отчёта»
URL подписчика Настраивается отдельно (через subscription API), в спеке не привязан к конкретному запросу Берётся из данных самого запроса через runtime expression

6.2. Пример: секция webhooks (OpenAPI 3.1)

Используя схему WebhookEvent (oneOf + discriminator) из Урока 07.02, описание исходящих webhook выглядит так:

openapi: 3.1.0
info:
  title: Task Manager API
  version: 1.0.0

webhooks:
  taskEvent:
    post:
      summary: Уведомление о событии задачи
      requestBody:
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookEvent"
      responses:
        "200":
          description: Webhook принят
        "401":
          description: Неверная подпись

components:
  schemas:
    WebhookEvent:
      oneOf:
        - $ref: "#/components/schemas/TaskCreatedEvent"
        - $ref: "#/components/schemas/TaskUpdatedEvent"
      discriminator:
        propertyName: event_type
        mapping:
          task.created: "#/components/schemas/TaskCreatedEvent"
          task.updated: "#/components/schemas/TaskUpdatedEvent"

6.3. Пример: секция callbacks (асинхронный результат операции)

paths:
  /reports:
    post:
      summary: Запросить генерацию отчёта (асинхронно)
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                callbackUrl:
                  type: string
                  format: uri
                  example: "https://partner.example.com/hooks/report-ready"
      responses:
        "202":
          description: Запрос принят, отчёт будет сгенерирован асинхронно
      callbacks:
        reportReady:
          "{$request.body#/callbackUrl}":
            post:
              requestBody:
                content:
                  application/json:
                    schema:
                      type: object
                      properties:
                        report_id: { type: string }
                        status: { type: string, enum: [ready, failed] }
                        download_url: { type: string, format: uri }
              responses:
                "200":
                  description: Подтверждение получения

Runtime-выражение {$request.body#/callbackUrl} означает: «URL для callback берётся из поля callbackUrl тела исходного запроса».

Для аналитика: если в требованиях есть фраза «после завершения долгой операции система пришлёт результат на указанный URL» — это callbacks. Если «система в принципе может присылать события подписчикам» — это webhooks.


7. Тестирование webhook-интеграций

7.1. Проблема: как протестировать входящий webhook на локальной машине

Источник webhook находится во внешней системе (часто — в облаке) и не может «достучаться» до localhost. Для тестирования нужен публично доступный URL, который проксирует запросы на локальную машину.

Инструмент Назначение
webhook.site Бесплатный сервис: даёт временный публичный URL, показывает все входящие запросы (заголовки, тело) — удобно для первичного изучения формата payload от незнакомого провайдера
ngrok / localtunnel Создают публичный туннель к localhost — разработчик может принимать реальные webhook локально во время отладки
Postman Mock Server Можно создать mock-endpoint, который принимает webhook и возвращает заданный ответ — полезно для тестирования retry-логики источника (см. Урок 07.08)

7.2. Чек-лист тестирования webhook-интеграции

Проверка
1 Endpoint отвечает 2xx при валидном payload и подписи
2 Endpoint отвечает 401 при отсутствующей/неверной подписи
3 Endpoint отвечает 401 при устаревшем timestamp (replay)
4 Повторная отправка того же event_id не создаёт дублирующих эффектов
5 Endpoint отвечает быстро (< таймаута провайдера), даже если бизнес-логика медленная
6 Обработка некорректного event_type (новый/неизвестный тип события) не приводит к ошибке 500
7 Endpoint Verification (challenge-response) проходит при регистрации подписки

8. Чек-лист аналитика при проектировании webhook-интеграции

Проверка Раздел
1 Определён список событий (event_type) и структура data для каждого 2
2 Описан формат конверта события (event_id, event_type, occurred_at, data) 2.2
3 Описан процесс регистрации подписки и endpoint verification 3
4 Указан механизм версионирования событий 3.3
5 Указано требование к подписи запроса (HMAC-SHA256) и где хранится секрет 4.1
6 Указана защита от replay (timestamp + окно) 4.2
7 Указан таймаут, за который обработчик должен ответить 2xx 5.5
8 Указано, что тяжёлая логика выполняется асинхронно после 200 OK 5.5
9 Указана дедупликация по event_id (идемпотентность обработчика) 5.6
10 Описана retry-политика источника (сколько попыток, интервалы) 5
11 Описано поведение при исчерпании retry (Dead Letter / алерт / ручная обработка)
12 Указано, гарантируется ли порядок доставки событий (как правило — нет) 3.3
13 Endpoint доступен только по HTTPS 4.3
14 Предусмотрен мониторинг: сколько webhook доставлено / отклонено / в очереди на retry

Вопросы для самопроверки

Базовый уровень

  1. Чем webhook отличается от polling? Приведите пример ситуации, где webhook невозможен.
  2. Что такое «конверт события» (event envelope)? Какие поля в нём обязательны и почему?
  3. Зачем нужна подпись (HMAC) webhook-запроса? Что произойдёт, если её не проверять?
  4. Что такое Endpoint Verification и зачем она нужна перед началом отправки реальных событий?
  5. В чём разница между webhooks и callbacks в OpenAPI 3.1?

Продвинутый уровень

  1. At-least-once: Почему webhook-провайдеры почти всегда гарантируют «доставлено хотя бы один раз», а не «ровно один раз»? Что это значит для приёмника?
  2. Replay-атака: Злоумышленник перехватил легитимный webhook payment.succeeded с валидной подписью и повторно отправляет его на endpoint магазина каждый час. Подпись каждый раз верна. Как это остановить?
  3. Таймаут провайдера: У провайдера webhook таймаут ответа — 3 секунды, а обработка заказа в вашей системе занимает 8 секунд. Спроектируйте обработчик так, чтобы провайдер не считал доставку неуспешной.
  4. Версионирование событий: Источник изменил структуру data для task.updated (убрал поле old_status, добавил changes: []). У вас 5 подписчиков, использующих старый формат. Как внедрить изменение без даунтайма для подписчиков?
  5. Порядок событий: Подписчик получил task.deleted для задачи №42 раньше, чем task.created для той же задачи (из-за повторной доставки и сетевых задержек). К каким проблемам это приведёт и как их избежать?

Практическое задание

Задание 1. Анализ payload и проектирование конверта события (1,5 балла)

Источник присылает webhook в следующем «голом» виде (без конверта):

{
    "task_id": 4521,
    "title": "Подготовить отчёт",
    "status": "done",
    "previous_status": "in_progress"
}
  1. (0,5 балла) Назовите минимум 3 проблемы такого формата с точки зрения приёмника.
  2. (0,5 балла) Перепроектируйте payload, добавив конверт события (event_id, event_type, occurred_at, data). Укажите подходящий event_type.
  3. (0,5 балла) Опишите, какое поле и почему должно использоваться для дедупликации повторных доставок.

Задание 2. Проверка подписи: пошаговый алгоритм (2 балла)

Платёжная система присылает webhook с заголовками X-Webhook-Signature: sha256=<hex> и X-Webhook-Timestamp: <unix_time>. Секрет подписи: whsec_abc123.

  1. (1 балл) Опишите пошаговый алгоритм проверки запроса на стороне приёмника (от получения запроса до решения «обработать» или «отклонить»). Укажите конкретные коды ответа для каждого случая отказа.
  2. (0,5 балла) Почему важно вычислять HMAC от raw body, а не от объекта после JSON.parse?
  3. (0,5 балла) Почему сравнение подписей должно быть constant-time? Что произойдёт, если использовать обычное ==?

Задание 3. Кейс «Двойная отправка заказа» — анализ катастрофы (2,5 балла)

Вернитесь к катастрофе из раздела 5. Магазин получил webhook payment.succeeded, обработка заняла 6 секунд, провайдер сделал retry через 5 секунд, в результате заказ обработан дважды.

  1. (0,5 балла) Кто допустил ошибку — провайдер платежей (отправил retry) или магазин (медленно обработал)? Обоснуйте.
  2. (0,5 балла) Опишите изменение архитектуры обработчика (что происходит синхронно, что асинхронно).
  3. (0,5 балла) Опишите механизм, который предотвратит повторную обработку, даже если retry всё же произойдёт.
  4. (0,5 балла) Что должно произойти с заявкой на доставку и письмом, если обработка события упала с ошибкой на этапе создания заявки (после списания со склада)? Опишите стратегию восстановления.
  5. (0,5 балла) Какие 3 пункта вы добавите в ТЗ на эту интеграцию, чтобы такая катастрофа не повторилась на следующем проекте?

Задание 4. OpenAPI: описание webhook-события (2 балла)

Для Task Manager (см. Урок 07.02) система отправляет подписчикам событие task.assigned при назначении задачи на пользователя.

  1. (1 балл) Напишите схему TaskAssignedEvent (конверт + data с полями task_id, assignee_id, assigned_by, assigned_at) и добавьте её в oneOf схемы WebhookEvent с соответствующим discriminator.mapping.
  2. (1 балл) Опишите секцию webhooks в OpenAPI 3.1 для этого события, включая requestBody и ответы 200/401.

Задание 5. Retry-политика и чек-лист (2 балла)

Вы проектируете интеграцию, в которой ваша система — источник webhook (отправляет события внешним подписчикам).

  1. (1 балл) Спроектируйте таблицу retry-политики: сколько попыток, с какими интервалами (например, экспоненциальный backoff), что происходит после последней неуспешной попытки. Свяжите с подходами Retry из Урока 07.05.
  2. (1 балл) Опишите, какую информацию о доставке webhook должна видеть служба поддержки, если подписчик жалуется «мы не получили событие N часов назад» (минимум 4 пункта: что логировать и где это отображать).

Критерии оценки

Задание Баллы
Задание 1: Конверт события 1,5
Задание 2: Проверка подписи 2
Задание 3: Кейс «Двойная отправка» 2,5
Задание 4: OpenAPI webhook 2
Задание 5: Retry-политика 2
Итого 10

Дополнительные материалы

  • Стандарт: OpenAPI 3.1 Specification — секции webhooks и callbacks
  • Статья: «Webhooks: How to design a robust webhook system» (Hookdeck / Svix engineering blogs)
  • Документация: Stripe Webhooks — пример подписи (Stripe-Signature), retry-политики и Dead Letter в дашборде
  • Документация: GitHub Webhooks — пример Endpoint Verification и X-Hub-Signature-256
  • Инструмент: webhook.site — изучение формата входящих webhook
  • Инструмент: ngrok — туннелирование для локальной отладки webhook
  • Связанные темы: Урок 07.05 — Idempotency-Key и Retry, Урок 07.06 — Dead Letter Queue

📚 Материалы модуля

🖼️ Схема и инфографика

🎬 Видео-лекция

🎬 API и интеграции

📄 Дополнительные материалы (PDF)

📄API Integration Blueprints
Скачать
Спросить ИИ