26 августа, 2026

Идемпотентность SMS API: как не отправлять сообщения дважды

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

Идемпотентность SMS API означает, что повтор одного логического запроса не создаёт вторую отправку. Это особенно важно для SMS: клиентское приложение может не получить ответ из-за таймаута, хотя шлюз уже принял сообщение. Если приложение просто повторит POST-запрос, адресат рискует получить два одинаковых кода, напоминания или уведомления об оплате.

Идемпотентность не заменяет обработку ошибок и не гарантирует доставку до телефона. Она решает более узкую задачу: отделяет повтор технического запроса от нового бизнес-события. В статье про SMS API для разработчиков разобрана интеграция в целом, а здесь сосредоточимся на защите от повторных отправок.

Откуда берутся дубли

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

Дубли возникают и по другим причинам:

  • задача повторно появилась в очереди после перезапуска обработчика;
  • пользователь дважды нажал кнопку получения кода;
  • CRM несколько раз прислала одно событие;
  • воркер завершился после отправки, но до фиксации результата в базе;
  • два экземпляра приложения одновременно обработали одну запись.

Проверка «такой текст на такой номер уже отправляли» ненадёжна. Два одинаковых сообщения могут быть законными, например два кода входа в разное время. Нужен идентификатор именно бизнес-операции.

Как работает ключ идемпотентности

Для каждой логической отправки приложение создаёт уникальный ключ: idempotency_key, request_id или event_id. При повторе той же операции передаётся тот же ключ. Провайдер либо ваш промежуточный сервис сохраняет связь «ключ → результат» и возвращает уже созданный идентификатор сообщения вместо новой отправки.

Ключ удобно строить из стабильного идентификатора события. Для уведомления о заказе это может быть сочетание типа события, номера заказа и версии статуса. Для OTP лучше использовать идентификатор конкретной попытки выдачи кода, а не только номер телефона: новый запрос пользователя должен оставаться новой операцией.

Один бизнес-факт — один ключ. Повтор доставки того же факта использует прежний ключ, новое событие получает новый.

Практика idempotency token применяется в надёжных API: повтор с тем же токеном и теми же параметрами возвращает прежний результат, а попытка использовать ключ с другими параметрами считается конфликтом. Этот подход описан в AWS Well-Architected Framework.

Где хранить результат

Минимальная запись содержит ключ, состояние операции, идентификатор сообщения у провайдера, контрольную сумму значимых параметров и срок хранения. Запись нужно создать атомарно до внешнего эффекта или в рамках механизма, который не позволит двум воркерам одновременно стать владельцами одного ключа.

Удобная последовательность:

  1. Приложение пытается создать запись с уникальным idempotency_key.
  2. Если запись уже есть, параметры сравниваются с сохранёнными.
  3. Если операция завершена, возвращается прежний message_id.
  4. Если операция выполняется, клиент получает предсказуемый статус и повторяет проверку позже.
  5. Только владелец новой записи вызывает SMS API.
  6. Ответ провайдера сохраняется рядом с ключом.

Обычной проверки SELECT, за которой идёт INSERT, недостаточно: два параллельных процесса могут одновременно не увидеть запись. Нужны уникальный индекс и атомарная вставка либо транзакционная блокировка.

Что включать в контрольную сумму

К одному ключу нельзя принимать разные операции. Поэтому вместе с ним фиксируют получателя, шаблон или текст, Sender ID, тип сообщения и важные параметры. Полный текст не обязательно хранить открыто: можно сохранить его хеш и отдельно — безопасный идентификатор версии шаблона.

Если при повторе ключ совпал, а параметры изменились, запрос лучше отклонить как конфликт. Иначе ошибка вызывающей системы будет скрыта: она получит идентификатор старого сообщения, хотя рассчитывает отправить другое.

В контрольную сумму не стоит включать нестабильные технические поля, например время каждой попытки или случайный trace ID. Иначе один логический повтор будет выглядеть как новая операция.

Срок жизни ключа

Хранить ключи вечно обычно не требуется, но срок должен покрывать максимально возможное окно повторов. Для синхронного вызова это не только HTTP-таймаут: очередь может повторить задачу через часы, а аварийное восстановление — поднять старую задачу на следующий день.

Срок задают по сценарию. Для короткоживущего OTP он может быть связан с жизненным циклом попытки входа. Для уведомления о платеже или заказе полезно хранить ключ дольше, потому что повтор такого сообщения сильнее путает клиента. Удаление записей должно быть контролируемым, а не зависеть от случайной очистки кеша.

Идемпотентность на стороне клиента и провайдера

Лучший вариант — когда ключ поддерживает сам SMS API. Тогда провайдер не создаст дубль даже при потере ответа после приёма сообщения. Если такой возможности нет, защиту строят в своём сервисе отправки: все системы компании обращаются к нему, а он ведёт журнал ключей и единолично вызывает внешний API.

Локальная защита не закрывает редкий разрыв между внешней отправкой и сохранением ответа. Поэтому при отсутствии провайдерского ключа важно иметь поиск по собственному client_message_id или согласованный способ выяснить судьбу запроса. Если API этого не позволяет, автоматический повтор неоднозначной операции должен быть ограничен.

Связь с очередями и ретраями

Идемпотентность делает повтор технически безопаснее, но не отвечает на вопрос, когда именно повторять запрос. Для этого нужна отдельная политика по кодам ответа, таймаутам, числу попыток и задержкам. Её проектируют как продолжение общей интеграции SMS API, а не как бесконечный цикл вокруг одного HTTP-вызова.

В очереди ключ должен проходить вместе с задачей от момента создания события до вызова провайдера. Генерировать новый ключ внутри каждой попытки нельзя — так каждая попытка снова станет уникальной и защита потеряет смысл.

Что проверить перед запуском

  • повтор одного запроса с тем же ключом возвращает тот же message_id;
  • два параллельных запроса создают только одну отправку;
  • тот же ключ с другим номером или текстом отклоняется;
  • перезапуск воркера не генерирует новый ключ;
  • срок хранения покрывает задержанные повторы из очереди;
  • в логах можно найти все попытки по ключу, но нет OTP и секретов;
  • метрика дублей и конфликтов ключей попадает в мониторинг.

Итог

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

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