Типичные ошибки при интеграции API цифрового поставщика
Интеграция с поставщиком цифровых товаров ломается не там, где ожидают. Счастливый путь — «создали заказ, получили код» — пишется за день. Всё остальное время уходит на состояния, о которых в первой версии никто не подумал. Ниже восемь режимов отказа, которые в этой отрасли встречаются чаще всего, каждый с конкретным фиксом.
1. Retry без дедупликации → дубли заказов
Что происходит. Ваш сервис отправляет POST /orders. Поставщик создаёт заказ и начинает выдачу, но ответ теряется по сети. HTTP-клиент по своей политике повторяет запрос. Поставщик видит новый запрос — создаёт второй заказ и второй раз списывает с депозита. Покупатель получил один код, вы заплатили за два.
Многие интеграторы считают, что от этого защищает заголовок идемпотентности. Проверьте, поддерживает ли ваш поставщик его на самом деле — далеко не все делают серверный dedup, и отправка заголовка «на всякий случай» не даёт ничего.
Фикс. Дедупликация на вашей стороне, всегда:
- Записываете локальную
pending-запись с собственным UUID до вызова API. - Вызываете создание заказа.
- При успехе сохраняете ID заказа поставщика в эту же запись.
- При таймауте или сетевой ошибке — не повторяете вслепую, а опрашиваете список заказов и ищете совпадение по SKU, количеству и временному окну.
- Создаёте новый заказ только если совпадения нет.
Подробный разбор паттерна — в материале про идемпотентность и безопасные повторы.
2. Вера в вебхуки как в гарантию
Что происходит. Обработчик написан так, будто каждое событие придёт ровно один раз и строго по порядку. В реальности вебхуки теряются при вашем деплое, приходят дважды после ретрая на стороне отправителя и обгоняют друг друга: completed может прийти раньше processing.
Результат — заказы, навсегда застрявшие в промежуточном статусе, и коды, выданные покупателю дважды, потому что обработчик отработал повторную доставку как новое событие.
Фикс.
- Обработчик обязан быть идемпотентным: повторное событие с тем же ID не должно менять состояние.
- Введите монотонность: игнорируйте событие, которое описывает состояние более раннее, чем уже записанное.
- Считайте вебхук подсказкой «сходи и проверь», а не фактом: по событию делайте запрос статуса заказа и записывайте то, что вернул API.
- Держите опрос как страховку для заказов, по которым долго нет финального статуса.
Если ваш поставщик вебхуков не предоставляет вовсе, опрос становится единственным механизмом — как его строить, разобрано в отслеживании статусов заказов.
3. Retry без backoff и джиттера
Что происходит. Поставщик отдаёт 429 или 503. Ваш клиент повторяет через фиксированную секунду, в десять потоков, по всем зависшим заказам сразу. Вы превращаете короткую деградацию на его стороне в устойчивый шторм, продлеваете её и рискуете попасть под rate-limit надолго.
Фикс.
- Экспоненциальная задержка с потолком плюс случайный джиттер, чтобы клиенты не синхронизировались.
- Ретраить только то, что безопасно ретраить:
GET— всегда, создание заказа — только по схеме из пункта 1. - Различать классы ошибок:
4xxиз-за неверных данных повторять бессмысленно,429и5xx— да, но с уважением кRetry-After. - Circuit breaker: после серии отказов переставать долбить и уводить трафик на альтернативный источник.
4. Отсутствие задачи сверки
Что происходит. Пока всё работает, никто не сравнивает свои данные с данными поставщика. Расхождения копятся тихо: заказ оплачен, но код не записан; код записан, но продажа отменена; депозит списан без успешной выдачи. Всплывает это через месяц, когда логи уже ротированы, а окно претензий к поставщику может быть закрыто.
Фикс. Ежедневная автоматическая сверка трёх списков: ваши продажи, заказы у поставщика, движения по депозиту. Задача должна помечать расхождения в отдельную таблицу и создавать оператору задачу на разбор, а не просто писать в лог. Отдельно проверяйте «зависшие»: заказы, которые дольше разумного времени не в финальном статусе.
5. Коды в открытом виде и в логах
Что происходит. Ключи и PIN лежат в базе plaintext, попадают в отладочные логи, в трейсы APM и в тела писем поддержки. Один дамп базы или один слишком подробный лог-агрегатор — и весь актив уходит.
Здесь важно понимать масштаб: цифровой код — это деньги на предъявителя. Отменить активацию нельзя, вернуть — тоже.
Фикс.
- Шифрование в покое отдельным ключом, ключ — в менеджере секретов.
- Строгий запрет на попадание кодов в логи, трейсы и тексты ошибок. Отдельный редактор для маскировки в логгере.
- Ограниченный список сервисов с правом расшифровки, аудит каждого обращения.
- Политика хранения: доставленные коды вне окна споров не должны храниться вечно.
6. Игнорирование частичного исполнения и стокаута
Что происходит. Заказано десять единиц, выдано семь. Код обрабатывает ответ как булево «успех/неуспех» — и либо отдаёт покупателю семь кодов, взяв деньги за десять, либо считает весь заказ провалившимся и не отдаёт ничего, хотя семь уже списаны с депозита.
Сюда же относятся стокаут и отзыв: и то и другое — штатные состояния, а не аварии.
Фикс.
- Модель заказа должна быть позиционной: статус и результат хранятся на уровне каждой позиции, а не всего заказа.
- Явные состояния:
fulfilled,partially_fulfilled,out_of_stock,failed,revoked. - Для частичного исполнения — определённая политика: доставить выданное, вернуть деньги за невыданное, дозаказать остаток из альтернативного источника.
- На витрине — честный статус вместо ошибки, а в бэкенде — маршрутизация на альтернативный SKU или регион, как в мультиисточниковой маршрутизации.
7. Отсутствие тестирования на sandbox и на реальных отказах
Что происходит. Интеграцию проверили тремя успешными заказами и выкатили. Первый же таймаут, первый 429 и первый частично исполненный заказ встречаются на живых деньгах и живых покупателях.
Фикс.
- Используйте тестовый режим поставщика, если он есть, и прогоняйте не только успех: отказ, таймаут, частичное исполнение, недостаток средств на депозите, недоступный SKU.
- Тестируйте свою сторону через мок с искусственными задержками, обрывами и дублирующимися событиями.
- Отдельно проверьте, что происходит при деплое во время активных заказов — самый недооценённый сценарий.
- Полный набор состояний, которые нужно покрыть, описан в объяснении order flow.
8. Захардкоженный курс и валюта
Что происходит. Курс конвертации зашит константой в коде или взят один раз при старте приложения. Через месяц вы продаёте ниже себестоимости и не понимаете, почему маржа уехала. Вариант той же ошибки — предполагать, что цена в API всегда придёт в одной валюте.
Фикс.
- Курс — конфигурируемый параметр с явным источником, временем обновления и буфером на движение, а не константа.
- Валюту читайте из ответа API, а не предполагайте.
- Все денежные величины — в минорных единицах целым числом, никакой плавающей точки для денег.
- Логируйте курс, применённый к каждому заказу: без этого сверка маржи задним числом невозможна.
- Как курс встраивается в итоговую цену, разобрано в расчёте скидки реселлера.
Короткий чек-лист перед выкаткой
| Проверка | Готово? |
|---|---|
| Локальная pending-запись создаётся до вызова API | |
| Повтор создания заказа проходит через проверку существующих | |
| Обработчик событий идемпотентен и игнорирует устаревшие | |
| Ретраи с экспоненциальной задержкой, джиттером и circuit breaker | |
| Ежедневная сверка продаж, заказов и депозита | |
| Коды зашифрованы, отсутствуют в логах и трейсах | |
| Частичное исполнение и стокаут — явные состояния | |
| Отказы протестированы, не только счастливый путь | |
| Курс конфигурируем, валюта читается из ответа |
Где брать товар
Половина перечисленных проблем снимается на уровне выбора поставщика, а не кода: понятная модель статусов заказа, честные состояния при нехватке стока, широкий каталог, чтобы не поддерживать пять разных интеграций с пятью разными наборами багов. FoxReload даёт 900+ SKU по ключам, подарочным картам, топ-апам, eSIM и софт-лицензиям через один REST API с автовыдачей — то есть один контракт, одна логика ретраев и одна сверка вместо зоопарка.
Дальше логично прочитать быстрый старт по API и сравнение предзакупки и выдачи по запросу — оно определяет, какую часть этого списка вам вообще придётся реализовывать.
