Автоматическое создание платёжки в СберБизнес из накладной

Содержание
  1. Проблема
  2. Как обычно
  3. Как это работает в ScanFlow
  4. Что важно учесть
  5. FAQ
  6. Что дальше

Распознанная накладная — это половина задачи. Вторая половина — заплатить поставщику по этой накладной, не открывая интернет-банк руками. У нас уже есть структурированные реквизиты получателя, сумма, НДС и номер счёта — глупо потом снова набивать всё это в форму СберБизнеса. Эта статья — о том, как мы автоматически создаём черновик платёжного поручения через Sber API, и о пяти конкретных граблях, которые встретили по дороге.

Если вы ещё не читали предыдущую часть про интеграцию с 1С:УНФ — там разобрано, как накладная попадает из фото в учётную систему. Здесь мы стартуем с того момента, когда оператор уже подтвердил накладную в дашборде, и хочет одной кнопкой отправить её в банк на оплату.

Платёжки бухгалтер тоже бьёт руками

Накладная пришла, товар принят, в 1С появилась приходная — поставщику пора платить. Это означает: открыть СберБизнес, нажать «Новое платёжное поручение», и руками перенести из накладной шесть-семь полей: ИНН поставщика, КПП, БИК, корсчёт, номер расчётного счёта, сумму, НДС. Дальше написать назначение платежа по канону: «Оплата по сч. №2841 от 30.01.2026 за товары, в т.ч. НДС 20% — 2 141.67 руб.»

На одну платёжку уходит 30-60 секунд. В небольшом оптовом бизнесе это 20+ платежей в день: одних мы оплачиваем сразу, других — в конце недели, третьих — по факту получения товара. Час-полтора времени бухгалтера в день, и это при условии, что он не отвлекается и не путает реквизиты.

Категории ошибок, которые мы видели у клиентов до подключения автоматизации:

  • Опечатка в ИНН поставщика. Самый дорогой класс ошибок: платёж либо уходит «в никуда» и потом возвращается через несколько дней, либо — в худшем случае — попадает к другой организации с похожим ИНН.
  • Не та сумма. Цифры на бумаге, цифры в накладной 1С, цифры в платёжке — три места, между которыми бухгалтер «прыгает» глазами. Где-то одна цифра выпадает.
  • Назначение без НДС-клаузулы. Налоговая любит видеть «в т.ч. НДС 20%» прямо в назначении. Без этого пометка идёт в спорные операции при камеральной проверке.
  • Не тот расчётный счёт получателя. У многих поставщиков несколько счетов (основной, для маркетплейсов, для какой-нибудь спецоперации). Бухгалтер копирует «по памяти» — и попадает не туда.

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

Как обычно: открыть Сбер, скопировать реквизиты, вписать назначение

Классический маршрут оплаты накладной выглядит так:

  1. Открыть распознанную накладную в ScanFlow (или провести её в 1С:УНФ) и посмотреть реквизиты поставщика.
  2. В новой вкладке открыть СберБизнес → Платежи и переводыСоздать платёжное поручение.
  3. В поле «Получатель»: ввести ИНН, нажать поиск — банк подгружает наименование и КПП из своей базы (или вы создаёте контрагента вручную в первый раз).
  4. Выбрать расчётный счёт и БИК банка получателя. Если поставщик новый — ввести оба поля вручную, верифицировать корсчёт.
  5. Вписать сумму, выбрать наш расчётный счёт (если у организации их несколько), указать очерёдность платежа (обычно «5»).
  6. Сформулировать назначение платежа в строго определённом формате. Включить ссылку на счёт-фактуру (номер и дату) и обязательную НДС-клаузулу.
  7. Сохранить как черновик → подписать токеном → отправить.

Всё это — механический перенос структурированных данных, которые у нас уже есть. ScanFlow распознал ИНН поставщика, его наименование, банковские реквизиты, сумму и ставку НДС. 1С знает наш расчётный счёт. Очерёдность платежа — константа. Назначение платежа собирается из полей накладной по шаблону.

Логичный следующий шаг после OCR-автоматизации — научиться передавать это всё в банк через API, чтобы человек только нажал «подписать» в СберБизнесе. Этим мы и занялись.

Как это работает в ScanFlow

На странице деталей накладной появляется кнопка «Отправить в Сбербанк». По ней мы собираем payload платёжного поручения и вызываем Sber API. Через несколько секунд в СберБизнесе у пользователя в разделе «Черновики» появляется готовая платёжка — реквизиты заполнены, остаётся только подписать токеном.

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

mTLS + OAuth-flow

Sber API — это не «получил API-ключ и понеслась». Это полноценный mTLS-канал поверх OAuth. Чтобы вообще достучаться до сервера, нам нужны:

  • Клиентский сертификат в формате PFX (выдаётся в Sber Developer Portal при регистрации приложения). Храним в ./certs/sber.p12, пароль — в env-переменной SBER_TLS_PFX_PASSWORD.
  • CA-сертификат Сбера для проверки серверного certificate chain. Лежит в ./certs/sber-ca.pem.
  • OAuth-токены на конкретную организацию-плательщика. Получаем через стандартный authorization code flow со scope openid GET_CLIENT_ACCOUNTS PAY_DOC_RU.

Без mTLS даже OAuth-эндпоинт не отвечает — TLS-хендшейк просто рвётся. Папка certs/ у нас в .gitignore, разворачивается на проде вручную при первом деплое.

После прохождения OAuth у нас в таблице sber_tokens лежит пара токенов. Платёжное поручение создаётся одним POST-запросом:

POST https://fintech.sberbank.ru:9443/fintech/api/v1/payments
Authorization: <access_token>
Content-Type: application/json

{
  "date": "2026-02-01",
  "externalId": "<UUID>",
  "amount": 12850.00,
  "operationCode": "01",
  "priority": "5",
  "purpose": "Оплата по сч. №2841 от 30.01.2026 за товары, в т.ч. НДС 20%",
  "payerName": "ООО Ромашка",
  "payerInn": "7707083893",
  "payerAccount": "40702810100000000001",
  "payerBankBic": "044525225",
  "payerBankCorrAccount": "30101810400000000225",
  "payeeName": "ООО Поставщик",
  "payeeBankBic": "044525974"
}

Без поля digestSignatures Sber сохраняет документ как черновик и возвращает его externalId — то, что нам нужно.

Особенность Authorization: без «Bearer»-префикса

Самая неочевидная грабля из тех, что мы прошли. RFC 6750 предписывает передавать OAuth-токен в заголовке как Authorization: Bearer <token>. Любой клиент, любая библиотека, любой Postman-сниппет по умолчанию делает именно так. Так делает fetch, так делают все HTTP-клиенты, так пишут во всех туториалах.

Sber Business API возвращает 401, если в заголовке есть Bearer. Ожидается голый токен: Authorization: <access_token>, без всякого префикса. В документации Сбера это упоминается, но мимоходом, и большинство примеров кода в открытых репозиториях написаны с Bearer — потому что копипастятся из других OAuth-провайдеров. Мы выяснили это опытным путём за пару часов до запуска: 401 на каждый запрос, токен валидный, рефреш проходит, но `/v1/payments` упорно отказывает.

В нашем коде это выглядит так — обратите внимание на отсутствие Bearer в обоих местах:

// src/sber/payments.ts
const res = await sberFetch(PAYMENTS_URL, {
  method: 'POST',
  headers: {
    Authorization: accessToken,           // ← БЕЗ Bearer
    'Content-Type': 'application/json',
    Accept: 'application/json',
  },
  body: JSON.stringify(body),
});

// src/sber/clientInfo.ts
const res = await sberFetch(CLIENT_INFO_URL, {
  headers: { Authorization: accessToken, Accept: 'application/json' },
});

Если в будущем будете писать собственный клиент Sber API — закладывайте это сразу. Все остальные OAuth-провайдеры, с которыми мы работаем (Google, Anthropic, Telegram bot) ждут именно Bearer — поэтому соблазн «исправить» этот код будет высоким. Не исправляйте.

Поле «purpose»: 210 символов ASCII-safe — почему

Поле назначения платежа (purpose) — отдельная история. Sber API накладывает на него два ограничения, и оба легко проворонить.

Первое: максимум 210 символов. Кажется, что много, но если в назначении есть длинное наименование поставщика и развернутая НДС-клаузула, лимит уезжает быстро. Если превысить — сервер возвращает 400 с непонятной формулировкой про validation error, не уточняя, какое поле «слишком длинное».

Второе: только ASCII-friendly пунктуация. Sber отвергает:

  • «Ёлочки» — типографские кавычки « » и их западные кузены " "
  • Em-dash и en-dash — нужно заменять на обычный дефис -
  • Неразрывный пробел   — заменять на обычный пробел

При первой реализации мы наивно отправили в Sber пример вида «Оплата по сч. №2841 от 30.01.2026 — в т.ч. НДС 20%» с типографским em-dash. Ответ — 400, без полезного диагноза. Полтора часа на дебаг, прежде чем заподозрили punctuation.

Сейчас у нас две функции: renderPurpose() собирает текст по шаблону из полей накладной и обрезает на 210 символов с многоточием в конце, а sanitizePurpose() нормализует пунктуацию:

// src/sber/purposeTemplate.ts
export function sanitizePurpose(s: string): string {
  return s
    .replace(/[«»""„]/g, '"')
    .replace(/[''']/g, "'")
    .replace(/ /g, ' ')
    .replace(/[—–]/g, '-');
}

На выходе получаем стабильную строку, которую Sber всегда принимает: «Оплата по сч. №2841 от 30.01.2026 за товары, в т.ч. НДС 20% — 2 141.67 руб.» — где «—» уже превращён в обычный дефис на этапе санитизации.

Почему только черновик, а не подписанный платёж

Sber API технически позволяет отправлять платёжки уже подписанные — через поле digestSignatures с цифровой подписью документа. Мы сознательно этого не делаем.

Деньги уходят только с подписи владельца. ScanFlow заполняет реквизиты, человек принимает финальное решение в банке — иначе никак.

Причин три, и они складываются друг с другом:

  1. Юридическая. Электронная подпись платёжки — это акт волеизъявления распорядителя счёта. Делегировать его сервису распознавания накладных — путь к проблемам с банком и налоговой при первом же спорном переводе.
  2. Безопасность. Чем меньше критичных операций может сделать наш сервер, тем меньше последствий от потенциальной компрометации. Создать черновик в банке — это zero damage (пользователь увидит лишнюю запись и удалит её). Подписать платёжку — это уже реальные деньги.
  3. UX-фильтр. Пользователь, перед тем как ткнуть подпись токеном, ещё раз глазами видит сумму, получателя и назначение. Это последняя возможность поймать ошибку OCR-распознавания (или нашу ошибку маппинга реквизитов поставщика), прежде чем деньги ушли. Терять этот барьер не хочется.

Итог: ScanFlow подготовил черновик, банк принял его и положил в раздел «Черновики», пользователь зашёл с токеном и подписал. От 30-60 секунд ручной работы остаётся только финальное «нажми кнопку и приложи палец». Это и есть нужный нам уровень автоматизации — не больше, не меньше.

Что важно учесть

Две грабли, на которые наступаешь не на этапе интеграции, а позже — при первом боевом использовании. Стоит знать заранее.

Двойная отправка: UNIQUE(invoice_id) спасает от повторов

Пользователь нажал «Отправить в Сбер». Кнопка моргнула. Запрос пошёл. Пользователь, не дождавшись ответа, нажал ещё раз — потому что «вроде ничего не происходит». Если бы мы не защитились — в банке появилось бы два одинаковых черновика на одну и ту же накладную. И, в худшем случае, оба были бы подписаны и оплачены.

У нас в таблице sber_payments колонка invoice_id объявлена UNIQUE. При первом успешном POST в Sber мы делаем INSERT INTO sber_payments (invoice_id, …). Второй INSERT на ту же накладную падает на constraint violation — наш роут отвечает 409 Conflict, UI показывает «Платёжка уже отправлена, посмотрите в Сбере». Никаких дублей.

Эта же логика защищает от другого сценария: пользователь подписал черновик в Сбере, потом передумал и вернулся в ScanFlow «отправить ещё раз». Не получится — и это правильно. Если действительно нужна повторная отправка (предыдущая бухгалтерия удалила черновик в Сбере, например), есть кнопка «сбросить отправку в Сбер» — она удаляет строку из sber_payments и разблокирует повторную попытку. Но это явное осознанное действие, не двойной клик.

Если кто-то в будущем будет добавлять частичные оплаты (одну накладную тремя платежами по графику) — это отдельная фича, с миграцией таблицы и снятием UNIQUE на invoice_id. Не «ослабить constraint», а спроектировать новую модель данных, где у одной накладной несколько связанных платежей. Не путайте.

Что делать с истёкшим access_token

Sber выдаёт access_token примерно на час, refresh_token — на несколько дней (по нашим наблюдениям — около 5-7). Мы храним оба в таблице sber_tokens. При каждом исходящем запросе проверяем срок жизни access_token: если он истёк или истечёт в ближайшие 60 секунд — делаем refresh через POST /v2/oauth/token и обновляем строку.

Если на сам платёжный запрос мы всё-таки получили 401 (refresh успешно прошёл несколько минут назад, но Sber уже считает токен невалидным — такое бывает после серверного rebound) — делаем ещё один refresh и повторяем запрос ровно один раз. Больше — нет, иначе можно зациклиться на сломанной аутентификации.

💡

Операционный совет: прежде чем катить новый деплой, проверьте, что PFX-сертификат не истёк и расположен по правильному пути. Sber выдаёт сертификат на год, и забыть про это легко — он молча работает 364 дня, а на 365-й все запросы начинают падать с непонятной TLS-ошибкой в кишках undici. У нас в скриптах деплоя есть проверка openssl pkcs12 -in certs/sber.p12 -nokeys с парсингом notAfter — алертит за 30 дней до истечения. Рекомендуем заложить такую же.

Отдельная история — если refresh_token тоже истёк (пользователь не заходил в систему две недели, токены отлежались и протухли). Тогда любой запрос к Sber падает с 401 уже на refresh. В этом случае мы редиректим пользователя на /#/sber для повторного OAuth flow — браузер открывает страницу Сбера, пользователь логинится, мы получаем свежие токены и кладём их в БД. Это происходит редко (у активных пользователей токены продлеваются автоматически), но если случилось — UX должен быть понятным: «нужно переподключить Сбер», одна кнопка, минута времени.

FAQ

Что если у меня не Сбер, а другой банк?

Пока поддерживается только СберБизнес. В roadmap — ВТБ Бизнес и Альфа-Бизнес: оба банка имеют публичные API для создания платёжных поручений, и оба используют схожую модель с mTLS + OAuth. Но детали разные: у ВТБ другой формат payload (ближе к ISO 20022), Альфа использует JSON Web Signatures для подписи. Это не «поменять URL», а отдельная интеграция для каждого банка. Будем подключать по мере спроса от клиентов.

Кто видит черновик платёжки и как его одобрить?

Черновик попадает в СберБизнес в раздел «Черновики» или «Документы на подпись» — в зависимости от региональной версии интерфейса. Видят его все сотрудники организации, у которых есть права на работу с этим расчётным счётом. Подписывает — тот, у кого есть право «Подпись платёжных документов»: обычно директор или главбух. Подпись делается через токен Сбера: USB-токен, TouchID на маке/iPhone, либо одноразовый пароль из мобильного приложения СберБизнес — это настраивается в банке при выпуске токена и от ScanFlow не зависит.

Отзывается ли черновик автоматом, если я его не подписал?

Нет. Черновик живёт в СберБизнесе до тех пор, пока его не подписали или не удалили вручную. Если вы отменили накладную в ScanFlow после того, как успели отправить черновик в Сбер — лучше зайти в банк и удалить черновик руками. У нас нет ручки для удаления уже созданных черновиков из Sber API (с точки зрения API это не баг, а фича — банк не хочет, чтобы внешние сервисы могли отзывать платёжки), поэтому работает только с обеих сторон вручную. На практике это не больно: черновиков накапливается мало, и операция занимает несколько секунд.

Что дальше

Сценарий «фото накладной → платёжка в банке» теперь закрыт от начала до конца: распознали, провели в 1С, отправили оплату. Осталось закрыть один важный вспомогательный сценарий — заведение карточки нового поставщика. Об этом — в следующей статье.

Хотите попробовать связку «накладная → платёжка» на своих документах — регистрируйтесь, сейчас бесплатно во время беты, подключение Сбера — в разделе «Сбербанк» личного кабинета.