Распознанная накладная — это половина задачи. Вторая половина — заплатить поставщику по этой накладной, не открывая интернет-банк руками. У нас уже есть структурированные реквизиты получателя, сумма, НДС и номер счёта — глупо потом снова набивать всё это в форму СберБизнеса. Эта статья — о том, как мы автоматически создаём черновик платёжного поручения через Sber API, и о пяти конкретных граблях, которые встретили по дороге.
Если вы ещё не читали предыдущую часть про интеграцию с 1С:УНФ — там разобрано, как накладная попадает из фото в учётную систему. Здесь мы стартуем с того момента, когда оператор уже подтвердил накладную в дашборде, и хочет одной кнопкой отправить её в банк на оплату.
Платёжки бухгалтер тоже бьёт руками
Накладная пришла, товар принят, в 1С появилась приходная — поставщику пора платить. Это означает: открыть СберБизнес, нажать «Новое платёжное поручение», и руками перенести из накладной шесть-семь полей: ИНН поставщика, КПП, БИК, корсчёт, номер расчётного счёта, сумму, НДС. Дальше написать назначение платежа по канону: «Оплата по сч. №2841 от 30.01.2026 за товары, в т.ч. НДС 20% — 2 141.67 руб.»
На одну платёжку уходит 30-60 секунд. В небольшом оптовом бизнесе это 20+ платежей в день: одних мы оплачиваем сразу, других — в конце недели, третьих — по факту получения товара. Час-полтора времени бухгалтера в день, и это при условии, что он не отвлекается и не путает реквизиты.
Категории ошибок, которые мы видели у клиентов до подключения автоматизации:
- Опечатка в ИНН поставщика. Самый дорогой класс ошибок: платёж либо уходит «в никуда» и потом возвращается через несколько дней, либо — в худшем случае — попадает к другой организации с похожим ИНН.
- Не та сумма. Цифры на бумаге, цифры в накладной 1С, цифры в платёжке — три места, между которыми бухгалтер «прыгает» глазами. Где-то одна цифра выпадает.
- Назначение без НДС-клаузулы. Налоговая любит видеть «в т.ч. НДС 20%» прямо в назначении. Без этого пометка идёт в спорные операции при камеральной проверке.
- Не тот расчётный счёт получателя. У многих поставщиков несколько счетов (основной, для маркетплейсов, для какой-нибудь спецоперации). Бухгалтер копирует «по памяти» — и попадает не туда.
Источник всех этих ошибок — ручное перенесение уже структурированных данных из одной системы в другую. То есть именно тот сценарий, который автоматизация решает идеально.
Как обычно: открыть Сбер, скопировать реквизиты, вписать назначение
Классический маршрут оплаты накладной выглядит так:
- Открыть распознанную накладную в ScanFlow (или провести её в 1С:УНФ) и посмотреть реквизиты поставщика.
- В новой вкладке открыть СберБизнес →
Платежи и переводы→Создать платёжное поручение. - В поле «Получатель»: ввести ИНН, нажать поиск — банк подгружает наименование и КПП из своей базы (или вы создаёте контрагента вручную в первый раз).
- Выбрать расчётный счёт и БИК банка получателя. Если поставщик новый — ввести оба поля вручную, верифицировать корсчёт.
- Вписать сумму, выбрать наш расчётный счёт (если у организации их несколько), указать очерёдность платежа (обычно «5»).
- Сформулировать назначение платежа в строго определённом формате. Включить ссылку на счёт-фактуру (номер и дату) и обязательную НДС-клаузулу.
- Сохранить как черновик → подписать токеном → отправить.
Всё это — механический перенос структурированных данных, которые у нас уже есть. 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 заполняет реквизиты, человек принимает финальное решение в банке — иначе никак.
Причин три, и они складываются друг с другом:
- Юридическая. Электронная подпись платёжки — это акт волеизъявления распорядителя счёта. Делегировать его сервису распознавания накладных — путь к проблемам с банком и налоговой при первом же спорном переводе.
- Безопасность. Чем меньше критичных операций может сделать наш сервер, тем меньше последствий от потенциальной компрометации. Создать черновик в банке — это zero damage (пользователь увидит лишнюю запись и удалит её). Подписать платёжку — это уже реальные деньги.
- 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С, отправили оплату. Осталось закрыть один важный вспомогательный сценарий — заведение карточки нового поставщика. Об этом — в следующей статье.
- От фото накладной до приходной в 1С:УНФ за 3 секунды — предыдущая часть пайплайна: как накладная попадает в учётную систему.
- Автозаполнение контрагентов по ИНН через DaData — следующий шаг: когда поставщик новый, заводим его карточку одним кликом.
Хотите попробовать связку «накладная → платёжка» на своих документах — регистрируйтесь, сейчас бесплатно во время беты, подключение Сбера — в разделе «Сбербанк» личного кабинета.