СБИС API: OAuth2, retry и что не написано в официальной документации

Официальная документация СБИС API описывает эндпоинты, поля запросов и примеры на curl. Но в реальной интеграции разработчик сталкивается с поведением, которое в документации не задокументировано: токены протухают раньше срока, retry провоцирует дубли, а 200 OK иногда означает «ошибка внутри тела». Разбираем подводные камни, на которые мы наступили при построении коннектора.

Интеграция с оператором ЭДО — задача, которая кажется простой до первого продакшена. Документация СБИС API выглядит структурированной: есть раздел авторизации, раздел документов, раздел контрагентов. Но между строками скрывается масса нюансов, которые не влияют на прохождение тестовых запросов в Postman, но ломают стабильность при сотнях документов в день. Эта статья — про то, что мы узнали о СБИС API, только когда начали работать с ним в бою.

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

OAuth2: авторизация, которая работает не так, как написано

СБИС использует OAuth2 для аутентификации. Логика стандартная: запрашиваешь токен по логину и паролю, используешь его в заголовке Authorization: Bearer, при протухании делаешь refresh. На бумаге всё просто. На практике — нет.

Токен живёт непредсказуемо

В документации указано время жизни access-токена. Мы реализовали клиент с учётом этого TTL: запоминаем время выдачи, перед запросом проверяем, не истёк ли срок. И столкнулись с ситуацией, когда токен, по нашим расчётам ещё валидный, возвращал 401. Причина оказалась в том, что СБИС иногда инвалидирует токены при конкурентных запросах из разных IP или при одновременном refresh из двух процессов.

Решение: ленивый refresh с fallback. Если запрос вернул 401 — не падаем, а молча обновляем токен и повторяем запрос один раз. Это добавляет latency, но не ломает транзакцию. При этом refresh-token хранится в Redis с блокировкой, чтобы два параллельных процесса не пытались обновить его одновременно.

Refresh не всегда возвращает новую пару

Стандарт OAuth2 предполагает, что при refresh вы получаете новый access и, возможно, новый refresh токен. СБИС иногда возвращает тот же refresh, а иногда — новый. Если ваш клиент ожидает новый refresh и перезаписывает старый — вы рискуете получить ситуацию, когда при одновременном запросе один процесс перезаписывает refresh другого, и второй остаётся с невалидным токеном.

Мы решили это через атомарное обновление в Redis: refresh-токен хранится как единое значение, обновление происходит через SET NX (или аналог), и только один процесс получает право на refresh. Остальные ждут, пока первый не обновит access-токен в общем хранилище.

Логин-пароль vs сертификат

Документация упоминает два способа авторизации: по паре логин-пароль и по сертификату. Парольный способ проще для старта, но у него есть неочевидный минус: при смене пароля в личном кабинете СБИС интеграция молча перестаёт работать, и вы узнаёте об этом не по алерту, а по звонку бухгалтера. Сертификатный способ сложнее в настройке, но не зависит от действий пользователя в веб-интерфейсе. Для продакшена рекомендуем сертификат — он стабильнее.

Retry: когда повторный запрос — это не лекарство

Любой HTTP-клиент в 2026 году должен уметь retry. Symfony HttpClient даёт это из коробки: задаёшь количество попыток, backoff-стратегию, и он сам повторяет при 5xx и таймаутах. Но СБИС API требует кастомной логики retry, потому что не всё, что кажится временной ошибкой, ею является.

200 OK с ошибкой внутри тела

СБИС иногда возвращает HTTP 200, но в JSON-теле содержит поле error или error_message. Это происходит при валидации бизнес-логики: неверный ИНН контрагента, отсутствие обязательного поля, превышение лимита на размер документа. Стандартный retry на HTTP-статус такое не поймает — он увидит 200 и решит, что всё хорошо.

Мы добавили слой парсинга ответа, который проверяет не только статус, но и структуру тела. Если присутствует поле ошибки — выбрасываем кастомное SbisApiException с флагом isRetriable. Бизнес-ошибки (неверный ИНН) помечаются как non-retriable: повторный запрос не исправит данные. Технические ошибки (таймаут, 5xx) — как retriable.

Rate limiting: лимиты, о которых не предупреждают

В документации СБИС API нет явного раздела про rate limiting. Но на практике при интенсивной отправке документов мы начали получать 429 Too Many Requests. Лимит не задокументирован численно, и он, похоже, зависит от типа аккаунта и времени суток.

Наше решение — адаптивный rate limiter на стороне клиента. Мы используем Symfony RateLimiter с фиксированным окном: не более N запросов в минуту, где N подбирается эмпирически. При получении 429 включаем exponential backoff и временно снижаем лимит. Это замедляет отправку, но не ломает очередь.

Дубли документов при retry

Самый неприятный сюрприз: СБИС API не идемпотентен. Если вы отправили УПД, соединение оборвалось по таймауту, и клиент сделал retry — документ может быть создан дважды. СБИС не вернёт ошибку «дубль», потому что каждый запрос генерирует новый внутренний ID.

Бороться с этим можно только на стороне клиента. Мы реализили локальное хранилище отправленных документов с уникальным ключом по номеру и дате. Перед отправкой проверяем, не было ли успешной отправки этого документа ранее. Это не гарантия на 100% — при гонке условий окно остаётся — но сводит вероятность дубля к минимуму.

Структура документа: JSON, который не совпадает с УПД

Когда бухгалтер говорит «отправь УПД», он подразумевает документ, который он видит в 1С. Когда разработчик читает документацию СБИС API, он видит JSON-схему. Между ними — пропасть.

Поля с разной семантикой

В 1С поле «Покупатель» — это контрагент из справочника. В JSON СБИС это вложенная структура с ИНН, КПП, наименованием, адресом и ролью. В 1С сумма НДС может быть рассчитана автоматически. В СБИС она должна быть явно указана в каждой позиции. В 1С маркировка — это отдельный механизм. В СБИС — поле внутри товара с определённым форматом.

Мы ввели промежуточный DTO UpdDto, который описывает документ в терминах бизнеса: продавец, покупатель, товары, сумма, НДС. Отдельный маппер преобразует DTO в JSON СБИС. Это позволяет:

  • Тестировать бизнес-логику отдельно от формата API;
  • Менять структуру JSON при обновлении API, не трогая ядро;
  • Поддерживать несколько версий формата параллельно.

Обязательные поля, которых нет в 1С

СБИС требует ряд полей, которые в типовой 1С не всегда заполнены: адрес контрагента для роуминга, коды ТН ВЭД для импорта, признак ГИС МТ для маркированных товаров. Если отправить документ без них — СБИС вернёт ошибку валидации, но не скажет, какое именно поле пропущено. Сообщение будет общим: «Неверный формат документа».

Мы добавили предварительную валидацию на стороне коннектора: перед отправкой DTO проходит через Symfony Validator с кастомными constraints. Если чего-то не хватает — документ не уходит в СБИС, а помечается ошибкой с понятным описанием. Бухгалтер видит в интерфейсе: «У контрагента Иванов ООО не заполнен адрес для ЭДО» — и исправляет в 1С, а не ловит абстрактную ошибку от API.

Ошибки, которые не должны быть фатальными

В интеграции с внешним API важно различать ошибки, от которых можно оправиться, и ошибки, которые требуют вмешательства человека. Мы выделили четыре класса ошибок СБИС API и для каждого настроили своё поведение.

Класс ошибки Пример Поведение
Транзиентная Таймаут, 5xx, 429 Retry с backoff, затем dead-letter
Валидационная Неверный ИНН, отсутствие поля Сразу dead-letter, уведомление пользователю
Авторизационная 401, 403 Refresh токена, затем retry; при неудаче — алерт
Бизнес-логика Контрагент не найден, дубль документа Dead-letter, ручная обработка

Чего не хватает в документации: список для разработчика

По итогам работы с СБИС API мы составили список вещей, которые хотели бы видеть в документации явно, но которые приходится добывать опытным путём.

Точные лимиты rate limiting

Сколько запросов в минуту допустимо? Зависит ли лимит от типа аккаунта? Есть ли burst-режим? Сейчас это чёрный ящик.

Полная схема ошибок

Какие коды ошибок может вернуть каждый эндпоинт? Какие из них retriable? Документация описывает happy path, но не сценарии отказа.

Идемпотентность

Можно ли безопасно повторить запрос? Если нет — какой механизм дедупликации рекомендуется? Это критично для надёжной интеграции.

Изменения API

Где публикуется changelog? Какой срок поддержки старых версий? Без этого планировать обновление коннектора невозможно.

Вебхуки vs polling

Есть ли механизм push-уведомлений о смене статуса документа? Или единственный способ узнать, что документ подписан — это регулярный polling? Документация умалчивает.

Тестовая среда

Есть ли sandbox, в котором можно отправлять документы без риска создать реальные записи? Если да — как получить к ней доступ?

Как мы тестируем интеграцию со СБИС

Тестирование интеграции с внешним API — задача, которая решается в несколько слоёв. Мы не полагаемся на ручные прогоны, потому что они не масштабируются и не покрывают граничные случаи.

Unit-тесты клиента

HTTP-клиент СБИС обёрнут в интерфейс SbisClientInterface. В тестах мы подменяем его моком, который возвращает заранее заготовленные ответы. Это позволяет тестировать логику retry, обработку ошибок и парсинг тела без реальных запросов к API. Моки хранятся как JSON-файлы в репозитории — это наши «золотые записи» ответов СБИС.

Интеграционные тесты

Раз в суток CI запускает тесты против реального тестового аккаунта СБИС. Они отправляют один тестовый документ, проверяют статус, забирают его обратно. Это ловит изменения в API, которые моки не предскажут: новые обязательные поля, изменение структуры ответа, неожиданные 403.

Contract testing

Мы используем подход, при котором фиксируем структуру запроса и ответа. Если ответ СБИС перестаёт соответствовать ожидаемой схеме — тест падает ещё до того, как это сломает продакшен. Для JSON-схем используем JSON Schema Validator.

Выводы для разработчиков

Интеграция с СБИС API — это не сложная задача в принципе, но она требует аккуратности в деталях. OAuth2, retry, валидация, дедупликация — всё это стандартные паттерны, которые Symfony даёт из коробки или с минимальными доработками. Главное — не доверять документации на 100% и проектировать клиент так, чтобы он выживал в реальном мире, а не только в тестовых запросах.

Если вы начинаете интеграцию с СБИС — начните не с эндпоинтов, а с архитектуры клиента: как он будет авторизовываться, как будет обрабатывать ошибки, как будет предотвращать дубли. Эти решения сложнее изменить на лету, чем добавить ещё один вызов API. Наш опыт вынесен в open-source коннектор sbis-1c-connector — там можно посмотреть реализацию клиента, DTO, retry-логики и обработки ошибок, описанных в статье.