26 августа, 2026

Webhook для статусов SMS: прием DLR без дублей и потерь

1 мин
Время чтения

Webhook для статусов SMS позволяет провайдеру асинхронно сообщать, что произошло с отправкой: сообщение доставлено, отклонено, просрочено или ещё обрабатывается. Такой отчёт часто называют DLR. Надёжный обработчик должен принять событие быстро, проверить его, пережить повторную доставку и не откатить финальный статус более старым уведомлением.

Значения самих статусов разобраны в материале про отчёты о доставке SMS. Здесь речь о техническом канале, который переносит эти статусы из SMS-шлюза в вашу CRM, приложение или систему поддержки.

Почему webhook асинхронный

Ответ на запрос отправки подтверждает прежде всего приём команды шлюзом. До оператора и телефона сообщение доходит позже, поэтому держать исходное HTTP-соединение открытым невозможно. Провайдер сохраняет ваш callback URL и вызывает его, когда состояние меняется.

Одна отправка может породить несколько событий: принято, передано оператору, доставлено. Набор промежуточных статусов зависит от API. Финальное событие тоже может прийти повторно, если провайдер не получил подтверждение от вашего endpoint.

Поэтому обработчик нельзя проектировать как «один POST — одно новое действие». Он получает поток событий с моделью как минимум at-least-once.

Быстрый ответ и фоновая обработка

Webhook должен проверить минимальные обязательные поля, устойчиво сохранить событие и быстро вернуть успешный HTTP-код. Тяжёлую работу — изменение CRM, уведомление сотрудников, аналитику — лучше выполнять в фоне.

Если endpoint долго отвечает, провайдер считает доставку неуспешной и повторяет её. Так медленная внутренняя операция создаёт ещё больше нагрузки. Практичная последовательность выглядит так:

  1. Принять запрос и ограничить размер тела.
  2. Проверить источник и формат.
  3. Зафиксировать уникальный идентификатор события и исходные данные.
  4. Поставить нормализованное событие во внутреннюю очередь.
  5. Вернуть 2xx.
  6. Обновить бизнес-системы отдельным воркером.

Если сохранение не удалось, возвращать успех нельзя: иначе провайдер не повторит событие, и статус потеряется.

Идентификаторы и дедупликация

У события должен быть delivery_id, а у исходного сообщения — message_id или ваш client_message_id. Уникальный индекс по идентификатору доставки защищает от повторной обработки одного callback.

Если провайдер не даёт отдельный event ID, ключ можно составить из стабильных полей: провайдер, message ID, нормализованный статус и время события. Делать ключ только по message ID нельзя, потому что у одного сообщения законно меняется несколько состояний.

Повторный webhook должен возвращать тот же успешный ответ, не создавая повторных писем, начислений или изменений. Это тот же принцип, что и идемпотентность SMS API, только применённый к входящим событиям.

Порядок событий

Сеть не гарантирует, что callbacks придут в хронологическом порядке. Сначала может появиться DELIVERED, а затем повтор старого SENT. Если безусловно записывать последнее полученное значение, система откатит финальный статус назад.

Задайте явную модель переходов. Финальные состояния не заменяются промежуточными. Для каждого события сохраняйте время у провайдера и время приёма у себя: они нужны для разбора задержек, но ни одно поле в одиночку не гарантирует порядок.

Не стоит придумывать единый линейный ранг для всех кодов без учёта документации. FAILED, EXPIRED и DELIVERED — разные финальные ветви, а не просто числа на одной шкале.

Нормализация статусов

Разные маршруты и провайдеры называют состояния по-разному. Внутри компании полезно иметь небольшой стабильный словарь, например:

  • accepted — запрос принят в обработку;
  • sent — передан дальше по маршруту;
  • delivered — получен финальный отчёт о доставке;
  • failed_permanent — повтор без изменения данных не поможет;
  • failed_temporary — временная проблема;
  • expired — закончился срок доставки;
  • unknown — код пока не классифицирован.

Исходный код провайдера сохраняют рядом. Нормализованный статус удобен бизнес-логике, а оригинальный нужен поддержке и для обновления маппинга.

Проверка подлинности

Endpoint доступен из интернета, поэтому нельзя доверять любому POST. Используйте механизм, который поддерживает провайдер: подпись тела общим секретом, API-токен, mTLS или список исходящих адресов. IP allowlist полезен как дополнительный слой, но адреса могут меняться и не защищают от всех ошибок конфигурации.

Сравнивайте подпись с исходными байтами тела до преобразования JSON. Учитывайте timestamp и допустимое окно, если схема подписи это предусматривает, чтобы снизить риск повторного воспроизведения старого запроса. Секреты не выводите в логи.

Ответы endpoint

Возвращайте 2xx только после устойчивой фиксации события. 4xx означает, что запрос некорректен и повтор в том же виде обычно не поможет. 5xx сообщает о временной проблеме вашей стороны и просит провайдера повторить доставку.

Документируйте, какой именно код ожидает конкретный шлюз: некоторые системы считают успехом только 200, другие принимают весь диапазон 2xx. Максимальный размер тела, таймаут и политика повторов тоже должны быть известны заранее.

Публичные webhook-системы поддерживают просмотр попыток и redelivery по идентификатору доставки; например, такой подход документирован в GitHub Webhooks API. Для SMS-провайдера возможности могут отличаться, поэтому проверьте наличие журнала и ручного повторения при выборе интеграции.

Логи и наблюдаемость

Для каждого callback фиксируйте event ID, message ID, провайдера, нормализованный и исходный статус, время события, время приёма, HTTP-результат и идентификатор внутренней задачи. Номер телефона лучше маскировать, а текст SMS обычно не нужен.

Полезные метрики:

  • количество событий и дублей;
  • доля 4xx и 5xx endpoint;
  • время ответа webhook;
  • задержка между событием и приёмом;
  • неизвестные коды статуса;
  • события без найденного исходного message ID;
  • попытки отката финального статуса.

Состав безопасного журнала задают заранее: технические идентификаторы сохраняются, а содержание и телефоны минимизируются по принципам защиты данных в SMS.

Как тестировать

Отправьте одно и то же событие дважды, поменяйте порядок промежуточного и финального статуса, передайте неизвестный код, неверную подпись и событие для отсутствующего message ID. Затем временно верните 500 и убедитесь, что повтор приходит и обрабатывается один раз.

Тест завершён успешно, если бизнес-система сохраняет всю историю, но выполняет побочные действия один раз; финальный статус не откатывается; неизвестные события не теряются; повтор после временной ошибки восстанавливает поток.

Отдельный контрактный тест полезно запускать после каждого изменения интеграции. Он сверяет обязательные поля, типы значений, допустимые статусы и алгоритм подписи с зафиксированным примером события. Так несовместимое изменение обнаруживается до того, как реальные callbacks начнут уходить в ошибки или молча терять данные.

Итог

Надёжный webhook — это короткий публичный вход в устойчивую внутреннюю обработку. Он проверяет источник, сохраняет событие до ответа, дедуплицирует повторы, нормализует коды и защищает финальные состояния от устаревших callbacks. Тогда DLR становится не случайным полем в CRM, а проверяемой историей жизни каждого сообщения.

Обновлено 26 августа, 2026
Подходит каждому
SMS-рассылка для любой сферы бизнеса
Узнайте как внедрить SMS-рассылку в ваш бизнес вместе с quicktel
Оставьте номер и вам перезвонит менеджер.