Интеграция с API выставления счетов

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

Полный список маршрутов — в справочнике; здесь сценарий с рабочими curl.

Оглавление: С чего начать · Авторизация · Контрагенты · Проекты · Выставление счёта · Идемпотентность · Нумерация · Отправка в Т-Банк · Проверка оплаты · Вебхуки · Неразнесённые поступления · Печать · Коды ошибок · Полный сценарий


С чего начать

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

Ниже тот же путь целиком через API — для тех, кто разворачивает Поток сам или автоматизирует настройку. Четыре шага, понадобятся curl и jq. В кабинете каждому шагу соответствует свой экран в разделе «Настройки»: «Реквизиты и налоги», «Банковские счета», «Ключи приложений».

Шаг 1. Администратор

Регистрация открыта только на пустой базе: так заводится первый администратор. После этого она закрыта, а сотрудников добавляет администратор в «Настройках» → «Пользователи» (POST /api/users):

BASE=https://app.example.ru/api   # домен вашего кабинета

ADMIN_TOKEN=$(curl -s -X POST "$BASE/auth/register" \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@romashka.ru","password":"secret123","name":"Иван Иванов"}' \
  | jq -r .token)

Ответ 201:

{
  "token": "eyJhbGciOi...",
  "user": {
    "id": "…",
    "email": "admin@romashka.ru",
    "name": "Иван Иванов",
    "createdAt": "…",
    "isPlatformAdmin": true,
    "memberships": [
      { "tenantId": "000000000000000000000001", "tenantName": "Моя компания", "tenantInn": null, "tenantStatus": "active", "role": "admin" }
    ],
    "tenantId": "000000000000000000000001",
    "role": "admin"
  }
}

Роль — у членства в компании, а не у человека: в каждой компании из memberships она своя. Пока компания одна, заголовок X-Tenant-Id можно не передавать — сервер возьмёт единственную. Если компаний у человека несколько, передавайте его в каждом запросе, иначе 400 (см. «Компания запроса» в справочнике). Отказы по компании приходят с "code": "tenant_access": например, человека убрали из компании, пока он работал, — перечитайте GET /auth/me и выберите компанию из оставшихся.

Если пользователи уже есть, /auth/register закрыт — вернётся 403. Тогда войдите: POST /auth/login с email/password → 200 с такой же парой token/user. Свою компанию в общем «Потоке» регистрируют через POST /auth/signup (или кнопку «Подключиться» на сайте): она заработает, когда вы подтвердите почту и оператор её одобрит — до этого запросы к её данным отвечают 403 с "code": "tenant_inactive".

Шаг 2. Реквизиты компании

Без названия компании и ИНН выставить счёт нельзя (получите 409). Реквизиты заполняет администратор:

curl -s -X PATCH "$BASE/settings" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "companyName": "ООО «Ромашка»",
        "inn": "7701234567",
        "kpp": "770101001",
        "ogrn": "1157746123456",
        "legalAddress": "127006, Москва, ул. Тверская, 1",
        "directorName": "Генеральный директор Иванов И.И."
      }'

Ответ 200 — полный объект настроек. Полезные поля: timezone (IANA, по умолчанию Europe/Moscow — по ней считаются даты документов), invoiceDueDays (срок оплаты по умолчанию, дней; по умолчанию 5), paymentCheckCooldownSec (см. Проверка оплаты).

Шаг 3. Банковский счёт

Счета выставляются от имени компании в Т-Банке. Подключите расчётный счёт — токен выдаётся в личном кабинете Т-Бизнеса:

curl -s -X POST "$BASE/bank-accounts" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "accountNumber": "40802810800000012345",
        "tbankToken": "<токен из личного кабинета Т-Бизнеса>",
        "corrAccount": "30101810100000000100"
      }'

Без подключённого счёта (точнее, без его номера и БИК) выставление вернёт 409.

Шаг 4. Ключ приложения

Ключ выпускает администратор:

curl -s -X POST "$BASE/api-clients" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Моё приложение",
        "scopes": ["invoices:read","invoices:write","counterparties:read","counterparties:write"]
      }'

Ответ 201:

{
  "client": { "id": "…", "name": "Моё приложение", "keyTail": "…a1b2", "scopes": ["…"], "…": "…" },
  "key": "ooo_sk_<64 шестнадцатеричных символа>"
}

Ключ показывается только один раз — в этом ответе. В базе хранится лишь SHA-256-хеш: потерянный ключ восстановить нельзя, можно только выпустить новый и отозвать старый. Сохраните key сразу.

Необязательное поле seriesId привязывает приложение к отдельной серии нумерации. Управление ключами: GET /api-clients (список), PATCH /api-clients/:id (имя, scopes, серия), POST /api-clients/:id/revoke (отзыв; используется пользовательский JWT, не ключ приложения).

Дальше все запросы идут с ключом приложения — JWT больше не нужен.


Авторизация

Каждый запрос несёт заголовок:

Authorization: Bearer ooo_sk_…ключ…

Ключи приложений начинаются с префикса ooo_sk_. Тот же формат заголовка принимает и пользовательский JWT (веб-интерфейс работает через него), но для интеграции нужен именно ключ: только у ключей работает идемпотентность и только они видны в истории счетов как «кто выставил».

Права ключа задаются списком scopes при выпуске:

Scope Что даёт
invoices:read Чтение счетов, истории, статуса оплаты, статуса в банке, печать
invoices:write Создание, изменение, выставление, аннулирование, отправка в банк
counterparties:read Чтение контрагентов
counterparties:write Создание и изменение контрагентов

Ошибки авторизации:


Контрагенты

Создание с дедупликацией по ИНН

POST /api/counterparties
curl -s -X POST "$BASE/counterparties" \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "ООО «Покупатель»",
        "kind": "legal",
        "inn": "7704123456",
        "kpp": "770401001",
        "legalAddress": "123456, Москва, ул. Лесная, 5",
        "email": "buh@pokupatel.ru"
      }'

Поля: name — обязательно; kind — legal (юр. лицо, по умолчанию) / entrepreneur (ИП) / individual (физлицо); остальные необязательны: inn (до 12 символов), kpp (до 9), ogrn (до 15), legalAddress, email, phone, comment.

Дедупликация — важное свойство: перед выставлением счёта можно смело отправлять контрагента «как в первый раз» при каждом заказе и не хранить у себя идентификаторы — по ИНН всегда вернётся та же карточка. Дедупликация работает по непустому inn; карточки без ИНН (физлица) создаются каждую заново.

Ответ:

{
  "id": "68f0…",
  "name": "ООО «Покупатель»",
  "kind": "legal",
  "inn": "7704123456",
  "kpp": "770401001",
  "ogrn": "",
  "legalAddress": "123456, Москва, ул. Лесная, 5",
  "email": "buh@pokupatel.ru",
  "phone": "",
  "comment": "",
  "createdAt": "2026-09-10T10:00:00.000Z"
}

Ошибки: 400 — нет name (Укажите название контрагента); 401/403 — см. авторизацию.

Остальное


Проекты

Проект — то, подо что выставляют счета: услуга, направление, договор. Его код из двух цифр стоит внутри номера счёта: 26020007 — седьмой счёт 2026 года по проекту 02. Счета вне проектов идут с кодом 00.

Справочник живёт в кабинете (/api/projects, доступ по JWT — ключу приложения он не открыт). Стороннее приложение проект не заводит, а использует его код при создании счёта:

{ "projectCode": "02", "counterpartyId": "…", "items": [ … ] }

Поле необязательное: без него счёт получает 00. Код должен быть ровно из двух цифр, иначе 400. Заводить проект под код заранее не требуется — номер сформируется в любом случае, — но тогда в кабинете вместо названия будет видна только цифра.

Список счетов фильтруется по проекту:

GET /api/invoices?projectCode=02

projectCode=00 — счета вне проектов, это тоже осмысленный фильтр.

Выставление счёта

POST /api/invoices

Жизненный цикл: draft (черновик) → issued (выставлен) → partially_paid → paid, отдельно cancelled. Счёт создаётся черновиком; issue: true выставляет его сразу, одним запросом. Выставленный счёт — юридический документ: позиции, суммы, стороны и дата замораживаются.

Выставление (issue: true или POST /invoices/:id/issue) необратимо: номер получен, счёт виден в отчётности. Скрыть его можно только аннулированием (POST /invoices/:id/cancel), номер при этом остаётся занят.

Пример: сразу выставленный счёт

curl -s -X POST "$BASE/invoices" \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{
        "counterpartyId": "68f0…",
        "externalId": "order-2026-000123",
        "projectCode": "01",
        "issue": true,
        "pricesIncludeVat": true,
        "basis": "Договор № 5 от 01.09.2026",
        "dueOn": "2026-09-25",
        "items": [
          {
            "name": "Разработка мобильного приложения",
            "unit": "услуга",
            "quantityMilli": 1000,
            "priceKopecks": 3500000,
            "vatRate": "vat_22"
          },
          {
            "name": "Техподдержка, месяц",
            "unit": "мес.",
            "quantityMilli": 1500,
            "priceKopecks": 500000,
            "vatRate": "none"
          }
        ]
      }'

Поля запроса

Поле Тип Обяз. По умолчанию Значение
counterpartyId string да — id контрагента
items array да — минимум 1 позиция
issue boolean нет false true — сразу выставить
externalId string нет — идемпотентность, до 120 симв.
projectCode string нет "00" код проекта, ровно 2 цифры — попадает в номер
seriesId string нет серия ключа или «Основная» серия нумерации
bankAccountId string нет первый подключённый счёт счёт для реквизитов оплаты
issuedOn string нет сегодня в зоне компании дата счёта, ISO
dueOn string|null нет issuedOn + invoiceDueDays дней срок оплаты; null — без срока
basis string нет "" основание (договор), до 500 симв.
comment string нет "" комментарий, до 1000 симв.
pricesIncludeVat boolean нет false трактуют ли цены позиций НДС (см. ниже)
sendToBank boolean нет false после выставления отправить в Т-Банк; только вместе с issue: true

Позиции (items[])

Поле Тип Обяз. По умолчанию Значение
name string да — наименование
article string нет "" артикул (код номенклатуры), до 40 симв. Печатается колонкой «Артикул» — её нет, если артикул не заполнен ни в одной позиции.
unit string нет "шт." единица, до 20 симв.
quantityMilli int да — количество в тысячных: 1000 = 1 шт., 1500 = 1,5, 250 = 0,25. Минимум 1.
priceKopecks int да — цена за единицу в копейках: 3500000 = 35 000,00 ₽. От 0.
vatRate string нет "none" none / vat_0 / vat_5 / vat_7 / vat_10 / vat_20 / vat_22

Итоги (netKopecks, vatKopecks, totalKopecks) считаются на сервере и приходят в ответе — передавать их не нужно.

pricesIncludeVat и НДС

Поле на уровне счёта, определяет смысл priceKopecks:

Ставки none и vat_0 на расчёт не влияют: сумма равна цена × количество.

Счета с pricesIncludeVat: false и ненулевой ставкой НДС пока не отправляются в Т-Банк (см. ограничения отправки).

Ответ

201 (повтор с тем же externalId — 200, см. ниже) — объект счёта:

{
  "id": "680a…",
  "number": "26010001",
  "status": "issued",
  "projectCode": "01",
  "issuedOn": "2026-09-10T00:00:00.000Z",
  "dueOn": "2026-09-25T00:00:00.000Z",
  "counterpartyId": "68f0…",
  "counterparty": { "name": "ООО «Покупатель»", "inn": "7704123456", "…": "…" },
  "seller":      { "name": "ООО «Ромашка»",    "inn": "7701234567", "…": "…" },
  "bank": { "accountNumber": "40802810800000012345", "bic": "…", "…": "…" },
  "items": [
    { "id": "…", "name": "Разработка мобильного приложения", "unit": "услуга",
      "quantityMilli": 1000, "priceKopecks": 3500000, "vatRate": "vat_22",
      "netKopecks": 2868852, "vatKopecks": 631148, "totalKopecks": 3500000 },
    { "id": "…", "name": "Техподдержка, месяц", "unit": "мес.",
      "quantityMilli": 1500, "priceKopecks": 500000, "vatRate": "none",
      "netKopecks": 750000, "vatKopecks": 0, "totalKopecks": 750000 }
  ],
  "pricesIncludeVat": true,
  "netKopecks": 3618852,
  "vatKopecks": 631148,
  "totalKopecks": 4250000,
  "paidKopecks": 0,
  "overpaidKopecks": 0,
  "dueKopecks": 4250000,
  "paidAt": null,
  "payments": [],
  "basis": "Договор № 5 от 01.09.2026",
  "comment": "",
  "externalId": "order-2026-000123",
  "createdByClientName": "Моё приложение",
  "tbank": null,
  "createdAt": "…", "updatedAt": "…"
}

У черновика (issue: false) number пустой и status: "draft".

Остальные операции


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

externalId — идентификатор счёта на стороне вашего приложения (номер заказа, платежа и т.п.). Это защита от дублей при ретраях:

Передавайте externalId в каждом создании счёта. Найти счёт по нему можно и в списке: GET /api/invoices?externalId=order-2026-000123.

Важно: идемпотентность работает только для ключей приложений. От пользовательского JWT (Bearer eyJ…) поле externalId молча игнорируется и в документ не пишется.

Дедупликация контрагентов по ИНН (выше) устроена так же и по той же причине.


Нумерация

Номер формата ГГППНННН: две цифры года, две цифры кода проекта, четыре цифры сквозного номера внутри серии за период. Пример: счёт 2026 года, проект 01, первый в году → 26010001; тот же год без проекта → 26000001.

У ключа приложения может быть своя серия (seriesId при выпуске ключа или в запросе) — тогда «свои» приложения не смешивают нумерацию с чужими.


Отправка в Т-Банк

Отправленный в Т-Банк счёт появляется у контрагента с кнопкой «Оплатить».

Способ 1: вместе с созданием

sendToBank: true в POST /api/invoices — но только вместе с issue: true (иначе 400). Счёт сначала выставляется, затем уходит в банк; отказ банка не отменяет выставление — ошибка видна в tbank.error ответа, повторная отправка — эндпоинтом ниже.

Способ 2: отдельно

POST /api/invoices/:id/send-to-bank

Тело необязательное:

{ "contacts": ["buh@pokupatel.ru"], "contactPhone": "+79001234567", "force": false }

Рассылка (contacts, contactPhone) — внешнее действие без отмены: письмо и SMS контрагенту уходят сразу. Из карточки контрагента адреса никогда не подставляются — только явно переданные в запросе. Без contacts и contactPhone никто никуда не пишет: счёт просто появляется в банке.

Успех — 200:

{
  "invoice": { "…": "…", "tbank": {
      "invoiceId": "TnkInv_…",
      "pdfUrl": "https://…/invoice.pdf",
      "incomingInvoiceUrl": "https://…/pay",
      "sentAt": "2026-09-10T10:05:00.000Z",
      "mode": "sandbox",
      "error": null
  } },
  "alreadySent": false
}

Куда приходит счёт. Если у контрагента указан корректный ИНН (10 цифр для юрлица, 12 для ИП) и он — клиент Т-Банка, счёт падает ему в личный кабинет Т-Бизнеса. Остальным контрагентам счёт доступен по ссылке incomingInvoiceUrl/pdfUrl — её можно показать в вашем интерфейсе или отправить самому (email/SMS от банка — только через contacts/contactPhone).

Статус в банке. GET /api/invoices/:id/bank-status — статус как его отдаёт банк (например SUBMITTED), по счётам, которые в банк не уходили — 409.

Ограничения отправки (код 422 rejected)

Отказ «до сети» — счёт в текущем виде не отправляется, текст в error:

Отправка черновика или не-issued счёта → 409 conflict. Параллельная отправка того же счёта → 409 busy (повторите через минуту). Ошибка/недоступность банка → 502 с текстом; выставленный статус счёта при этом не страдает.


Проверка оплаты

GET /api/invoices/:id/payment-status

Опрашивать можно так часто, как угодно: в пределах cooldown (paymentCheckCooldownSec настроек, по умолчанию 300 секунд) статус отдаётся из базы — запрос в банк не идёт. Когда пауза истекла, один из запросов тянет свежую выписку и разносит оплаты (в т.ч. частичные) по счетам.

Опрашивать и не обязательно: «Поток» сам забирает выписку и разносит оплаты по расписанию — раз в paymentPollMinutes минут (настройки компании: 5, 15, 30 или 60; 0 — только вручную; по умолчанию 15). Окно у банковского счёта общее: если только что сходил планировщик, запрос статуса в банк не пойдёт, и наоборот.

{
  "invoiceId": "680a…",
  "number": "26010001",
  "status": "partially_paid",
  "totalKopecks": 4000000,
  "paidKopecks": 1500000,
  "dueKopecks": 2500000,
  "paidAt": null,
  "payments": [
    { "transactionId": "…", "amountKopecks": 1500000, "date": "…",
      "matchedAt": "…", "reason": "number_in_purpose", "confidence": 90 }
  ],
  "checkedAt": "2026-09-10T10:10:00.000Z",
  "nextCheckAt": "2026-09-10T10:15:00.000Z",
  "cooldownRemainingMs": 137000,
  "refreshed": false,
  "syncError": null
}

Ошибка банка не роняет запрос: всегда 200, при сбое синхронизации статус вернётся из базы, а текст ошибки — в syncError; cooldown при этом взводится, чтобы каждый следующий опрос не бился в недоступный банк. Логика на вашей стороне: syncError != null → показать/залогировать и попробовать позже.


Вебхуки

Вместо опроса статуса «Поток» может сам сообщать вашему приложению о событиях:

Событие Когда
invoice.issued счёт выставлен (получил номер)
invoice.paid счёт оплачен полностью
invoice.partially_paid к счёту привязан платёж, но оплачен он не целиком
invoice.payment_removed бухгалтер отвязал платёж от счёта — оплата, на которую вы могли отгрузить, признана чужой
invoice.cancelled счёт аннулирован
payment.received на расчётный счёт пришли деньги (новая входящая операция в выписке)
document.issued документ продажи выставлен: акт, накладная (ТОРГ-12) или УПД — по нему покупатель закрывает свою сторону
document.cancelled документ продажи аннулирован; номер остаётся за ним и другому документу не достанется
automation.task.ready задача конвейера автоматизации готова к действию человека: подготовлен черновик или требуется решение
automation.task.failed задача конвейера автоматизации упала — с причиной в error

Адрес задаётся в кабинете («Настройки» → «Вебхуки») или через API ключом со скоупом webhooks:write:

curl -s -X POST "$BASE/webhooks" \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url": "https://shop.example.ru/potok/webhook", "events": ["invoice.paid", "invoice.partially_paid"]}'

В ответе 201 — secret (whsec_…). Он показывается один раз: сохраните его в приёмнике. Потеряли — POST /webhooks/:id/rotate-secret, старый перестанет подходить сразу.

Что приходит

POST на ваш адрес с JSON-телом и заголовками:

Тело — всегда один конверт: id события, type, createdAt и data.

{
  "id": "evt_4f0c…",
  "type": "invoice.paid",
  "createdAt": "2026-09-11T10:15:00.000Z",
  "data": { "invoice": { "id": "…", "number": "26050001", "status": "paid", "paidKopecks": 150000, "…": "…" } }
}

Что лежит в data:

Событие data
invoice.issued invoice — счёт целиком, как в GET /invoices/:id, уже с номером и status: "issued"
invoice.paid invoice — счёт после привязки оплаты: status: "paid", paidKopecks, paidAt, payments[]
invoice.partially_paid invoice — status: "partially_paid"; сколько пришло — paidKopecks, сколько осталось — dueKopecks
invoice.payment_removed invoice — счёт уже после отвязки: статус вернулся к issued или partially_paid, отвязанного платежа в payments[] нет
invoice.cancelled invoice — status: "cancelled"; оплата, если была, остаётся за счётом
payment.received payment — id, bankAccountId, date, amountKopecks, counterpartyName, counterpartyInn, purpose
document.issued document — документ целиком, как в GET /sales-documents/:id: kind (act, waybill, upd), number, issuedOn, invoiceId, items[], суммы
document.cancelled document — status: "cancelled", cancelledAt и cancelReason; номер и позиции остаются прежними
automation.task.ready task — id задачи в GET /automation/tasks/:id, kind (payroll_register/filing/payment_order/notification), naturalKey, status (needs_review/draft/awaiting_signature)
automation.task.failed task — id задачи, kind, naturalKey и error — причина сбоя (без секретов и содержимого файлов)
ping message — только для проверки приёмника (POST /webhooks/:id/test); подписываться на него не нужно

invoice — тот же объект, что отдаёт GET /invoices/:id (см. раздел «Выставление счёта» → «Ответ»), поэтому состояние счёта можно брать прямо из события, не запрашивая его заново.

Одно событие может прийти дважды (например, если ваш ответ не дошёл до нас) — отбрасывайте повторы по id события.

Проверка подписи

Подпись — HMAC-SHA256 секретом от строки <t>.<тело запроса как есть>. Проверяйте её до разбора JSON и отбрасывайте запросы старше нескольких минут — так перехваченный запрос нельзя прислать повторно:

import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyPotokSignature(secret: string, header: string, rawBody: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // старше 5 минут
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const got = Buffer.from(String(parts.v1 ?? ''), 'hex');
  return got.length === 32 && timingSafeEqual(got, Buffer.from(expected, 'hex'));
}

Ответ и повторы

Ответьте 2xx быстро — за 10 секунд, работу делайте после ответа. Всё остальное (другой код, таймаут, обрыв) — неуспех: «Поток» повторит через минуту, 5 минут, полчаса, 2, 6, 12 и 24 часа — около двух суток. Переадресации (3xx) не выполняются — укажите окончательный адрес. Журнал доставок с кодами ответов — GET /webhooks/:id/deliveries и в кабинете; проверить приёмник — POST /webhooks/:id/test (событие ping).

Адрес — только https, и не во внутреннюю сеть (частные, loopback и link-local адреса запрещены).


Неразнесённые поступления

GET /api/invoices/payments/unmatched?page=1&limit=20

Входящие платежи, которые автосверка не привязала ни к одному счёту. Это очередь на решение человека, и попасть в неё платёж может по двум причинам: счёт есть, но сопоставить не удалось, — или счёта на эти деньги не было вовсе.

Из очереди платёж уходит двумя способами:

  1. Привязкой к счёту — POST /api/invoices/:id/payments с transactionId.
  2. Указанием вида платежа, которым нельзя оплатить счёт. Взнос учредителя, заём, возврат займа, возврат от поставщика и перевод между своими счетами счетами не оплачиваются — счёт на них никто не выставлял. Такой платёж разбирается проставлением вида (PATCH /api/transactions/:id с kindId) и исчезает из очереди.

Выручка, эквайринг и поступление от агента из очереди не уходят до привязки: за выручкой счёт стоять обязан. За это отвечает признак canSettleInvoice у вида платежа (GET /api/payment-kinds).

Печать

GET /api/invoices/:id/print   → text/html — печатная форма под А4
GET /api/invoices/:id/pdf     → application/pdf

Коды ошибок

Ошибка — всегда JSON с полем error: текст по-русски, его можно показывать человеку как есть. Иногда рядом есть поля для программы:

Код Когда Что делать
400 Тело не JSON; не прошли zod-схемы (нет обязательного поля, projectCode не 2 цифры, отрицательная цена…); неизвестный counterpartyId/seriesId/bankAccountId; кривые параметры фильтров; sendToBank без issue Проверить запрос по тексту ошибки, не ретраить вслепую
401 Нет заголовка; ключ неизвестен; ключ отозван Проверить ключ (ooo_sk_…), выпустить новый, если старый потерян/отозван
402 Не оплачен тариф «Потока»: синхронизация платного расчётного счёта после 7 дней просрочки; во время просрочки — новые ключи, вебхуки, счета и приглашения Оплатить счёт за тариф (он в кабинете: «Настройки» → «Банковские счета»); выставлять счета и проверять оплату можно и без этого
403 Право (scope) не выдано ключу; компания не работает (code: "tenant_inactive"); нет доступа к компании (code: "tenant_access") Для scope — попросить админа добавить его ключу; для компании — прочитать текст ошибки
404 Объект по :id не найден (в т.ч. кривой формат id); неизвестный путь ({"error":"Не найдено"}) Проверить идентификатор
409 Конфликт состояния: счёт уже выставлен; дата в закрытом периоде нумерации; не заполнены реквизиты для выставления (название/ИНН компании, счёт/БИК); изменение выставленного счёта; удаление не-черновика; контрагент со счетами; send-to-bank для черновика/не-issued/параллельной отправки (busy) Прочитать текст — обычно это состояние домена, а не сбой; для busy повторить через минуту
422 Счёт не отправляется в Т-Банк в текущем виде (rejected): номер не из цифр, цены без НДС с ненулевой ставкой, >100 позиций и т.п. Исправить счёт (в черновике) или доставлять печатной формой
429 Слишком часто: регистрации и заявки с одного адреса за сутки, повторное письмо подтверждения почты чаще раза в минуту Подождать — время указано в тексте
502 Банк недоступен или вернул ошибку: отправка (bank_error), подключение счёта, синхронизация выписки Ретрай с паузой; состояние счетов не портится
503 Сервис не настроен: нет ключа шифрования токенов банка; регистрация компаний закрыта, пока не настроена почта Сообщить владельцам инстанса
500 Внутренняя ошибка сервера ({"error":"Внутренняя ошибка сервера"}) Ретрай позже; если повторяется — сообщить владельцам инстанса

Пример разбора в коде: 409 на POST /invoices с текстом «Счёт нельзя выставить: не заполнены реквизиты — …» означает, что администратор не завершил шаги 2–3 раздела «С чего начать».


Полный сценарий: от ключа до оплаты

Один блок, выполняется подряд (нужны curl и jq). Рассчитан на чистую базу: только на ней открыта регистрация, и первый пользователь станет администратором.

BASE=https://app.example.ru/api   # домен вашего кабинета

# --- 1. Администратор (первый пользователь) и его JWT ---
ADMIN_TOKEN=$(curl -s -X POST "$BASE/auth/register" \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@romashka.ru","password":"secret123","name":"Иван Иванов"}' \
  | jq -r .token)

# --- 2. Реквизиты компании (без них выставление даст 409) ---
curl -s -X PATCH "$BASE/settings" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"companyName":"ООО «Ромашка»","inn":"7701234567","kpp":"770101001",
       "ogrn":"1157746123456","legalAddress":"127006, Москва, ул. Тверская, 1"}' > /dev/null

# --- 3. Подключение расчётного счёта Т-Банка ---
curl -s -X POST "$BASE/bank-accounts" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"accountNumber":"40802810800000012345",
       "tbankToken":"ВАШ_ТОКЕН_Т_БИЗНЕСА",
       "corrAccount":"30101810100000000100"}' | jq .

# --- 4. Ключ приложения. ПОКАЗЫВАЕТСЯ ОДИН РАЗ — сохраните ---
KEY=$(curl -s -X POST "$BASE/api-clients" \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Моё приложение",
       "scopes":["invoices:read","invoices:write","counterparties:read","counterparties:write"]}' \
  | jq -r .key)
echo "KEY=$KEY"

# Дальше работаем только с ключом приложения.

# --- 5. Контрагент (повтор с тем же ИНН вернёт его же, код 200) ---
CP_ID=$(curl -s -X POST "$BASE/counterparties" \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"ООО «Покупатель»","kind":"legal","inn":"7704123456","kpp":"770401001"}' \
  | jq -r .id)

# --- 6. Счёт: сразу выставить и отправить в Т-Банк.
#     externalId делает ретраи безопасными: повтор запроса вернёт этот же счёт (200).
INVOICE=$(curl -s -X POST "$BASE/invoices" \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d "{
        \"counterpartyId\":\"$CP_ID\",
        \"externalId\":\"order-2026-000123\",
        \"projectCode\":\"01\",
        \"issue\":true,
        \"sendToBank\":true,
        \"pricesIncludeVat\":true,
        \"basis\":\"Договор № 5 от 01.09.2026\",
        \"items\":[
          {\"name\":\"Разработка мобильного приложения\",\"unit\":\"услуга\",
           \"quantityMilli\":1000,\"priceKopecks\":3500000,\"vatRate\":\"vat_22\"},
          {\"name\":\"Техподдержка, месяц\",\"unit\":\"мес.\",
           \"quantityMilli\":1500,\"priceKopecks\":500000,\"vatRate\":\"none\"}
        ]
      }")
echo "$INVOICE" | jq '{id, number, status, totalKopecks, tbank}'
INVOICE_ID=$(echo "$INVOICE" | jq -r .id)

# --- 7. Контрагенту не уходило письмо — досылаем явно (внешнее действие!) ---
curl -s -X POST "$BASE/invoices/$INVOICE_ID/send-to-bank" \
  -H "Authorization: Bearer $KEY" \
  -H 'Content-Type: application/json' \
  -d '{"contacts":["buh@pokupatel.ru"]}' | jq '{alreadySent, tbank: .invoice.tbank}'

# --- 8. Опрос оплаты: можно хоть каждую секунду, банк спрашивается раз в cooldown ---
curl -s "$BASE/invoices/$INVOICE_ID/payment-status" \
  -H "Authorization: Bearer $KEY" | jq .

# --- 9. PDF себе (прямой ссылкой не открыть — нужен заголовок) ---
curl -s "$BASE/invoices/$INVOICE_ID/pdf" \
  -H "Authorization: Bearer $KEY" -o invoice.pdf && ls -l invoice.pdf

Что должно получиться: шаг 6 — счёт с номером вида 26010001, status: "issued" и заполненным tbank (в песочнице — тестовые ссылки); шаг 8 — "status":"issued", "paidKopecks":0, "refreshed":true/false.