Если у вас есть код, который дергает POST /messages на SMS-провайдера и молится, что ответ будет 200 OK, то наверняка все уже привыкли, что эта схема работает, и не стоит ее трогать. А потом у провайдера ночью падает шлюз, логики повторов нет, и утром вы разгребаете тикеты от пользователей, которые не смогли войти в приложение или подтвердить оплату.
В этом посте расскажу про то, как устроена доставка SMS под капотом, почему один провайдер рано или поздно подводит и как спроектировать резерв на второго поставщика так, чтобы это не превратилось в костыль, который никто не тестирует.
Что происходит между API-запросом и телефоном пользователя
Когда бэкенд отправляет запрос к SMS-шлюзу, сообщение не летит напрямую на телефон получателя. Путь выглядит примерно так:
Ваше приложение
→ SMS-шлюз провайдера (HTTP API или SMPP)
→ маршрут: прямое соединение с оператором ИЛИ агрегатор-посредник
→ SMSC оператора связи (Short Message Service Center)
→ устройство абонента
SMSC — это узел хранения-и-пересылки (store-and-forward): он ставит сообщение в очередь и пытается доставить его на устройство, повторяя попытки в течение окна валидности, если телефон недоступен.
Для России это чаще всего один-два шага: провайдер — оператор. Для международного трафика ступеней обычно больше, потому что ни один агрегатор физически не может заключить прямые договоры со всеми операторами мира. Каждый дополнительный узел создает условия, где сообщение может задержаться, попасть под фильтры или потеряться, а значит, если в проде только одна интеграция с одним провайдером, вы полагаетесь на то, что ни один узел в этой цепочке никогда не откажет. На практике так не бывает.
Почему один провайдер SMS становится точкой отказа
Причины сбоев у SMS-провайдеров банальны:
перегрузка шлюза в пиковые часы (акции, распродажи, массовая рассылка кодов после релиза);
плановые технические работы на стороне провайдера или оператора;
деградация конкретного маршрута: провайдер тот же, но конкретное направление вдруг стало медленным или ненадёжным;
изменение правил фильтрации на стороне оператора, из-за которого часть трафика начинает молча теряться.
Для рассылки промокодов задержка в полчаса некритична. Для OTP-кода при авторизации или SMS с ссылкой на оплату — это потеря конверсии: пользователь либо запрашивает код повторно, удваивая нагрузку на просевший канал, либо уходит.
Поэтому архитектуре, где SMS завязана на транзакционные сценарии, и нужен второй независимый провайдер.

Что означает «доставлено» в DLR
Прежде чем проектировать резервные пути, важно понять, на какие данные опираться при принятии решения о переключении.
DLR (Delivery Receipt) — это отчёт от сети, а не с самого устройства. Статус обычно значит, что SMSC оператора принял сообщение для передачи на телефон. Это не значит, что абонент увидел текст или прочитал его.
Три момента, которые стоит учитывать при обработке DLR-вебхуков:
Тихий отказ. Часть маршрутов, особенно низкого качества, теряет сообщения без возврата кода ошибки вообще. Если ваша система уведомляет только по явным недоставкам, такие случаи просто не попадут в мониторинг.
Недостоверные статусы. На слабоконтролируемых маршрутах провайдер может возвращать отчет о доставке независимо от того, что произошло на самом деле. У него есть коммерческий стимул отчитываться об успехе.
Подтверждение сети ≠ подтверждение пользователя. Даже честный отчет о доставке говорит только о том, что сеть приняла сообщение.
DLR полезен как сигнал деградации конкретного маршрута, резкий рост отчетов стоит проверить сразу. Но как единственная метрика качества доставки он не годится. Стройте дашборд на основе продуктовой конверсии (доля введённых кодов, доля переходов по ссылке из SMS) в паре с DLR, а не только на DLR.
С чем помогают HLR-запросы
HLR (Home Location Register) — база данных оператора, которая позволяет перед отправкой узнать, активен ли номер и на какой сети он сейчас обслуживается. Раньше по префиксу номера можно было понять оператора и маршрутизировать по нему, но после массового распространения переносимости номеров (MNP) статическая маршрутизация по диапазонам стала ненадёжной.
Если ваш SMS-шлюз или провайдер поддерживает HLR-lookup на уровне маршрутизации, это экономит бюджет при массовых рассылках и снижает число бессмысленных попыток доставки на отключённые номера.
HTTP API vs SMPP для двух провайдеров
Для интеграции с провайдером обычно доступны два варианта протокола.
HTTP REST API — проще внедрить, синхронный или асинхронный ответ, DLR приходит вебхуком на ваш эндпоинт. Подходит для большинства проектов, особенно если объём отправки не выходит за пределы десятков сообщений в секунду.
SMPP (Short Message Peer-to-Peer) — постоянное TCP-соединение с провайдером, ниже задержка при высоких объёмах, есть встроенный enquire_link для проверки соединения. Обычно выбирают, если счёт идёт на сотни сообщений в секунду или нужен полный контроль над сессией.
Для резервного канала это влияет на то, как вы детектируете сбой основного:
на HTTP API вы ориентируетесь на таймаут запроса и код ответа (5xx, timeout, отсутствие ответа за N секунд);
на SMPP — на неответ enquire_link в течение заданного интервала или на код ошибки при submit_sm.
Смешивать протоколы между основным и резервным каналом — нормально. Если у вас основной провайдер работает через SMPP, а резервный интегрирован только по HTTP API — это не проблема, если оба варианта покрыты вашим кодом отправки.
Но в целом SMPP сейчас поддерживает множество сервисов: Notificore, P1SMS, Prostor SMS и другие.
Как паттерн выглядит в коде
Базовая логика простая: пробуем основной канал, при ошибке или таймауте — резервный.
def send_sms(to: str, text: str, event_id: str): try: response = primary_provider.send( to=to, text=text, timeout=5 ) if response.status_code == 200: log_delivery_attempt(event_id, provider="primary", status="sent") return response except (Timeout, ProviderError) as e: log_failover_trigger(event_id, reason=str(e)) # Fallback на резервный канал response = backup_provider.send(to=to, text=text, timeout=5) log_delivery_attempt(event_id, provider="backup", status="sent") return response
Несколько пунктов, которые можно заложить сразу:
Короткий таймаут на основной канал. Не давайте основному API «висеть» дольше 3–5 секунд. Для OTP-сценария критична каждая лишняя секунда ожидания перед переключением.
Переключение только при реальных ошибках. 5xx, обрыв соединения, отсутствие ответа — да. Ошибка вида «невалидный номер телефона» — нет, это не повод дергать резервный канал, потому что там сообщение тоже не уйдёт.
Идемпотентность. У каждого события отправки должен быть уникальный идентификатор, который включает пользователя, событие и его версию. Повторный вызов не должен создать дубль SMS ни на основном, ни на резервном канале.
idempotency_key = f"{user_id}:{event_type}:{event_version}" if already_sent(idempotency_key): return # уже обработано, повтор игнорируем
Ещё можно прибавить синхронизацию шаблонов. Тексты сообщений на обоих провайдерах должны совпадать по смыслу и формату. Расхождения — потенциальный триггер для фильтров оператора, если резервный текст выглядит нетипично для вашего sender ID.
Каскад как более тонкий сценарий
Отдельный паттерн, который стоит рассмотреть, если у вас есть мобильное приложение с push-уведомлениями: SMS как резерв не только для основного SMS-провайдера, но и для push в целом.
Важно не путать одновременную отправку одного текста по двум каналам с каскадом. Правильная схема: приложение сначала пытается доставить push, ждёт подтверждения по заданному правилу, и только затем решает, нужно ли отправлять SMS. Такой каскад имеет смысл для ограниченного набора критичных событий — входа, оплаты, изменения бронирования, срочного статуса заказа, а не для всего подряд. И только если событие одновременно отвечает трём условиям: пользователь ожидает уведомление в рамках выполненного действия, пропуск приведёт к заметной проблеме, и информация всё ещё будет актуальна к моменту переключения на SMS.
Ответ push-сервиса о приёме запроса не гарантирует, что пользователь увидел уведомление, а отсутствие открытия само по себе не значит недоставку — текст мог быть прочитан прямо на экране блокировки. Более надёжные сигналы для переключения:
токен устройства недействителен;
push-провайдер вернул финальную ошибку;
приложение не подтвердило получение за отведённое время;
ключевое бизнес-действие не выполнено к контрольной точке (например, пользователь не подтвердил перенос бронирования).
Последний вариант обычно надёжнее остальных, потому что он завязан не на технический статус доставки, а на факт совершения действия.
Каждому уведомлению стоит задавать expires_at — если срок истёк, резервная отправка уже не имеет смысла. И отдельно fallback_after — момент, после которого разрешено переключение на SMS. Слишком короткий интервал создаёт дубли (push всё-таки дошёл чуть позже), слишком длинный оставляет пользователя без важной информации.
Пример цепочки для переноса бронирования:
Система создаёт событие.
Push отправляется на все активные устройства пользователя.
Приложение фиксирует подтверждение (или нет).
Через 15 минут проверяется актуальность версии события.
Если подтверждения нет и срок не истёк — отправляется SMS.
После подтверждения все оставшиеся задачи по этому событию отменяются.
SMS в таком каскаде должно быть самодостаточным сообщением. Пользователь может не иметь доступа к приложению именно в этот момент, так что ссылка и контекст должны умещаться в одном тексте.
Если у события есть push, SMS, email и запись во внутреннем центре уведомлений, у всех этих попыток должен быть единый идентификатор — версионированный ключ события, а не отдельные независимые кампании.
Защита от накрутки трафика на уровне маршрутизации
Отдельные проблемы для команд, у которых SMS завязаны на OTP-формы — SMS pumping и IRSF-мошенничество. Злоумышленник массово дергает форму запроса кода на подставные номера, чтобы заработать на комиссии с трафика или исчерпать бюджет жертвы через партнёрские схемы с недобросовестными операторами на дорогих направлениях.
Защита должна работать до отправки: анализ трафика на аномальные всплески по IP или отпечатку устройства, лимиты на уровне формы (не только капча, но и ограничение частоты запросов на один номер), проверка номера через HLR, понятные коды отказа, отличающие блокировку по безопасности от обычной ошибки доставки.
Как тестировать резерв, не дожидаясь инцидента
Резервный провайдер, который никогда не проверялся боевой отправкой — это впустую потраченное время. Что стоит внедрить в CI/CD или регулярный регламент:
Управляемые тесты по времени. Симулируйте разные сценарии с контролируемыми таймерами: push пришёл вовремя, финальная ошибка от провайдера, запоздалое подтверждение.
Плановое отключение основного канала. Раз в месяц искусственно роняйте основной провайдер или в контролируемом окне в проде и отправляйте сообщения через резерв, чтобы проверить переключение и задержки SMS через резервный канал.
Наблюдаемость каскада. Для диагностики недостаточно общего процента доставки. В логах нужны время создания события, попытки push, причина переключения, версия данных, идентификатор запроса SMS и финальное бизнес-действие, связанные одним ID.
Метрики, за которыми стоит следить
доля событий, закрытых основным провайдером;
доля переключений на резервный провайдер в динамике;
время от триггера события до подтверждения пользователем;
количество дублей между каналами;
стоимость завершённого действия, а не одного сообщения — резервный маршрут может быть дороже за штуку, но дешевле в пересчёте на успешную оплату.
С чего начать, если у вас пока только один провайдер
Возьмите одно критичное событие, например, отправку OTP-кода при входе, и добавьте для него резервный канал по HTTP API. Не пытайтесь сразу резервировать весь трафик.
Заложите короткий таймаут (3–5 секунд) и явные условия переключения по кодам ошибок, а не по любому исключению подряд.
Добавьте ключ на уровне события, чтобы повторы SMS не плодили дубли.
Настройте параллельный сбор DLR с обоих каналов и сравнение с продуктовой метрикой (например, долей введённых кодов).
Запустите плановое тестирование резерва хотя бы раз в месяц, с реальной тестовой отправкой.
Только после отладки этой связки расширяйте резервирование на другие сценарии.
Для роли резервного канала подойдёт провайдер, у которого на одной платформе закрыты сразу несколько типов рассылок — рекламные, сервисные и транзакционные. Например, можно взять Notificore, P1SMS или Prostor SMS. Идеально, если возьмете платформу сразу с несколькими каналами в одном API, чтобы подключить и SMS OTP и email OTP.
pr0l
а как у двух разных провайдеров держать зарегистрированное имя отправителя одновременно?
telecomgod Автор
Спасибо за вопрос. Не важно через какое количество поставщиков идут SMS. Для каждого предоставляются документы на регистрацию и каждая платформа (поставщик) подает на регистрацию имя отправителя. Можно подключить имя либо на бесплатной основе (где это возможно), либо платить абонентскую плату и там, и там.
Не работает схема, что на одной платформе заплатил, на другой использует. Оплата идет как раз за возможность использовать сендер (имя) на конкретной платформе. Поставщик попросту отправляет имя на согласование и получает разрешение отправлять SMS с него абонентам конкретного оператора.