Прикольно получать пуши от банка, когда тебе на счёт приходят деньги. У нас начали всё чаще просить такой функционал, поэтому мы решили вписаться в эту историю и сделать для бизнеса такие уведомления.
Меня зовут Никита Бугуев — я бэкенд‑разработчик на Python в команде, которая разрабатывает открытое банковское API и пытается захватить контроль над всеми интеграциями наружу. В статье расскажу, как мы написали свои велосипеды для вебхуков и какие грабли собрали по пути.

С чего всё началось
У нас в Точка Банк есть открытое API, через которое компании и разработчики могут работать со счетами клиентов:
интегрировать банк в CRM, ERP, систему аналитики или мессенджеры;
принимать платежи по QR‑коду или банковской карте;
выставлять счета;
отслеживать актуальный баланс счёта.
Сервис используют разные партнёры: онлайн‑бухгалтерии подтягивают выписки клиентов, доставка принимает оплату по QR‑кодам, а предприниматели подключают свои сайты к приёму платежей.
Раньше, чтобы понять, прошёл ли платёж, им приходилось запрашивать выписку. Но это неудобно — платёж может прийти не моментально, выписка формируется несколько секунд, плюс сетевые задержки, лишняя логика в коде, поэтому многие просили сделать уведомления. Мы прикинули объём работ и оценили задачу в четыре senior‑часа, но по итогу вышло три недели работы junior‑разработчика.
Почему вебхуки
Для рассылки обычных уведомлений у нас есть микросервис Spammer — он умеет отправлять SMS, пуши и письма по шаблонам. Но он в данном случае не подходит, потому что:
Работает только с реальными физическими пользователями.
Нам нужно общаться с серверными приложениями, а не с людьми.
Написан на Java, поддерживается другой командой и отправляет сообщения по шаблону.
Мы заранее поняли, что пахнет сильным кастомом, поэтому решили сразу пилить свой велосипед.
Как это работает
Вебхук — это обычный POST‑запрос на url, в теле которого находится полезная нагрузка. В нашем случае отправляется информация о платеже.
Чтобы партнёр понял, что вебхук прислали именно мы, а не хакер, используется jwt‑токен. Это строка полезной нагрузки (а все запросы в интернете это просто строки), склеенная со строкой, которая содержит в себе подпись. Её мы генерируем нашим приватным ключём.
Чтобы партнеру её проверить, ему нужно взять эту подпись, расшифровать нашим публичным ключом (который лежит у нас на сайте и доступен кому угодно) и сверить получившееся тело (на самом деле хеш от тела). Если проверка прошла успешно, партнёр может быть уверен, что информацию отправили именно мы.
Как мы сами получаем информацию о том, что прошёл платёж?
В банке есть ESB (на основе RabbitMQ) и банковское ядро, которое переводит деньги со счёта на счёт и умеет через ESB отправлять об этом события. Ещё мы сами умеем подключатся к ESB и слушать события.
Получается такая картинка: в банковском ядре генерируется событие про платёж → оно попадает к нам → мы проверяем, что счёт принадлежит клиенту, который попросил и разрешил отправлять по нему вебхуки → собираем тело вебхука и отправляем запрос на url партнёра.
Как мы реализовали эту систему
Итерация 1: решение в лоб
Мы начали с самой простой схемы: напрямую слушали события платежа из ESB банка и отправляли вебхук партнёрам, если это нужно. Когда событие приходило в очередь, обработчик проверял, не отправляли ли мы его ранее, формировал вебхук и отсылал на адрес клиента.

Разумеется, почти сразу возникли проблемы:
Если клиент не успевал ответить за наш таймаут, то мы по умолчанию делали reject этого события, и оно возвращалось в обработку только после стандартного ретрая сообщений в 10 минут. Согласитесь, неприятно стоять на кассе в два ночи и ждать, пока кассир одобрит твою покупку чипсиков.
Если несколько клиентов долго висели на таймаутах, обработчики забивались такими вебхуками и не могли взять в обработку новые события о платежах. Из‑за этого копилась очередь сообщений и увеличивало��ь время доставки вебхуков.
Если копилась очередь входящих сообщений, это было видно на дашборде у дежурных, и они эпизодически тегали нас в чатах и спрашивали, всё ли у нас хорошо.
То есть чисто технически всё работало, но мы поняли, что можем лучше.
Итерация 2: разделение обработки и очереди
Мы разделили прослушивание событий о платежах с отправкой вебхуков — просто сделали для них отдельную очередь. Теперь событие платежа приходило и откладывалось в очередь вебхуков, но это всё ещё делал один обработчик.

Это не сильно помогло решить проблему:
Если в моменте было много клиентов, у которых всё пошло не так, то обработчик всё равно забивался.
Если клиент не ответил вовремя, следующая попытка всё равно была через 10 минут.
То есть клиенты, которые ломались, всё ещё влияли на других, но тут мы заложили основы архитектуры, которая позволит жить проще в будущем. И хотя бы дежурным инженерам стало спокойнее.
Итерация 3: очередь «неудачников»
Здесь нам всё ещё было страшно делать большие изменения. Но чтобы ускорить общее время доставки, мы ввели отдельную очередь для клиентов, которые не успели ответить вовремя (или не с ожидаемым http‑кодом). Сообщения попадали туда и через короткие интервалы в 10 секунд возвращались в основной обработчик.

Это помогло:
уменьшилось среднее время доставки;
обработчик меньше прокручивал просто так подвисших клиентов;
медленные клиенты стали сильно меньше влиять на уведомления «живым».
Но минусы всё равно остались. Если «падающих» клиентов было много, то новые сообщения частично блокировались.
Итерация 4: разные обработчики
Это финальная итерация, которую используем сейчас. Спустя n‑ное количество правок мы построили систему, где есть несколько очередей:
Очередь № 1 (esb.events.in): её слушает один обработчик, который работает только с событиями о платежах — проверяет на дубли, решает, надо ли вообще слать вебхук. И если он нужен, то создаёт новое сообщение в очередь № 2.
О��ередь № 2 (notification): её слушает другой обработчик, который умеет только отправлять вебхуки. Если отправить не получилось, он выбирает, в какую очередь нужно закинуть сообщение на переотправку (на основании того, что это за вебхук).
Очередь № 3 (notification.slow): для вебхуков, которые уже пытались отправить хотя бы раз. Её слушает отдельный обработчик, который копирует обработчик для второй очереди — отличаются они только очередью, которую слушают. Он также пытается отправить вебхук и при неудаче выбирает, куда дальше закинуть сообщение.
Очереди № 4-6 (notification.slow.*): их никто не слушает. Для них настроены x‑message‑ttl (время, после которого сообщение будет перемещено из очереди) и x‑dead‑letter‑exchange, ведущий на очередь № 3. То есть сообщения, попадая сюда, просто лежат и ждут, пока Кролик переложит их туда, где их обработают.

Благодаря этому:
Сообщения, которые не получилось доставить, перестали влиять на новые платежи.
Таймауты были локализованы и перестали блокировать всю систему.
«Живые» клиенты почти перестали получать вебхуки с задержкой (хотя тут у нас ещё есть неизвестные аномалии, которые мы разбираем индивидуально).
В остальном — хорошая работающая модель, хоть и получилась не сразу.
Наши ошибки
Итеративный подход хорош тем, что можно быстро пробовать и улучшать. Но вместе с этим мы несколько раз наступали на грабли. И, по моим подсчетам, допустили, как минимум, три ошибки:
Не сделали метрики. После первой итерации мы узнали о проблемах только от клиентов, которые писали на почту, что уведомление пришло не сразу, а спустя десять минут. Сейчас у нас уже есть мониторинг времени доставки, и мы точно знаем, сколько прошло между платежом в банке и ответом от сервера клиента.
Мало тестировали. В начале разработки у нас был заложен баг. По невнимательности, прежде чем отправить вебхук, мы оборачивали строку в кавычки, то есть делали двойную сериализацию. Инструменты тестировщиков на тот момент не замечали этой проблемы, а сами кодом обрабатывать такие вебхуки мы начали относительно недавно. Так что узнали о проблеме очень не скоро, а могли бы исправить в самом начале.
Плохо прописали документацию. Некоторые клиенты писали на почту, что после подключения на сервер пришло пустое тело. Со временем стало понятно, что если пишет один клиент, то это проблема одного клиента. Чаще всего ошибки появлялись у ребят, работающих на php. Поэтому прямо в документации мы прописали рабочие примеры кода, которые обрабатывают вебхук. Теперь сразу отсылаем туда и проблема быстро решается.

Что мы получили в итоге
Так путём проб и ошибок мы сделали быстрые и удобные уведомления. Сейчас клиентам доступны пять видов событий о:
входящих платежах;
исходящих платежах;
входящих платежах через СБП;
входящих платежах через B2B QR‑код по СБП;
входящих платежах только по платёжным ссылкам.
Классно, что у нас получилось построить своё решение, хоть и не без сложностей (:
