REST API

Apiin построен по принципу API-first: любое действие в кабинете — это вызов публичного REST API. Тот же контракт доступен внешним интеграциям и MCP-серверу для нейросетей.

Аутентификация

Ключи создаются в кабинете, в разделе API и MCP, — программно ключ выпустить нельзя. Каждому ключу выдаётся набор прав (scopes) и срок действия (постоянный или до даты). Полный ключ имеет вид apiin_live_… и передаётся в заголовке:

Authorization: Bearer apiin_live_xxxxxxxx

Один ключ может работать с несколькими организациями (набор задаётся при создании). Активных ключей у организации может быть до 25 (отозванные не считаются), а если секрет скомпрометирован — ключ перевыпускается в кабинете: меняется только секрет, а имя, права, срок и набор организаций остаются, поэтому в интеграции достаточно подменить строку ключа. Базовый адрес — https://api.apiin.ru. Лимит — 600 запросов в минуту на ключ; при превышении приходит 429. Ключ можно отозвать в любой момент.

Права (scopes)

Действие требует соответствующего права; * — все права:

  • counterparties:read / counterparties:write — контрагенты;
  • documents:read / documents:write — документы, PDF, оплаты, подписи, связки;
  • orders:read / orders:write — заказы;
  • public_links:write — публичные ссылки;
  • send:write — отправка на email;
  • templates:read / templates:write, assets:write, offers:write;
  • organizations:read / organizations:write;
  • webhooks:write — настройка вебхуков.

Организации

Все ресурсы скоупятся по организации: /api/organizations/{org_id}/…. Доступные ключу организации возвращает GET /api/whoami:

GET https://api.apiin.ru/api/whoami
Authorization: Bearer apiin_live_xxxxxxxx

→ { "organization_id": "<первая>", "organization_ids": ["<uuid>", …] }

Банковские счета и платёжный QR

Счета организации — GET/POST/PATCH/DELETE /api/organizations/{org_id}/bank-accounts (organizations:read / organizations:write). Платёжный QR (ГОСТ Р 56042-2014) печатается в счёте, когда у счёта по умолчанию заполнены расчётный счёт, БИК и корр. счёт; ИНН для QR не обязателен. В каждом счёте организации приходит готовность:

GET https://api.apiin.ru/api/organizations/{org_id}/bank-accounts

→ [ { "id": "…", "bank_name": "АО «АЛЬФА-БАНК»", "bik": "044525593",
      "account_number": "…", "corr_account": "…", "is_default": true,
      "payment_qr": { "ready": true, "missing": [], "personal_account": true } } ]

missing — коды незаполненных полей (account_number, bik, corr_account); personal_account — счёт физлица (40817/40820): такой QR принимают не все банки плательщиков. Сводка по организации — GET …/payment-qr: те же поля плюс source (bank_account | legacy | none) и bank_name.

Оплата для физлиц (самозанятые)

Самозанятый (тип self_employed или налоговый режим npd) может показать на публичной карточке реквизитов способы перевода для плательщиков-физлиц: телефоны для СБП с банками-получателями, карты и ссылку на оплату от банка (по ней рисуется QR). Настройки хранятся целиком и заменяются одним запросом; сервер приводит телефон к +79XXXXXXXXX, карту — к цифрам с проверкой по алгоритму Луна, ссылку принимает только https://. Лимиты: 5 телефонов, 5 карт, 10 банков у номера.

PUT https://api.apiin.ru/api/organizations/{org_id}/pay-options
Content-Type: application/json

{
  "enabled": true,
  "recipient_name": "Анна Павловна С.",
  "sbp":   { "enabled": true,  "phones": [ { "phone": "8 900 123-45-67", "banks": ["Сбер", "Альфа-Банк"] } ] },
  "cards": { "enabled": true,  "items":  [ { "number": "2200 1234 5678 9019", "bank": "" } ] },
  "link":  { "enabled": false, "url": "" }
}

→ 200 нормализованный объект · 400 { "code": "pay_options_invalid", "message": "Телефон 1: …" }

GET …/pay-options возвращает текущие настройки. Публично они видны в GET /p/r/{slug} полем pay_options (только у самозанятого и только включённые способы с данными, иначе null); QR ссылки — GET /p/r/{slug}/pay-link-qr.png.

Контрагент

Документ выставляется существующему контрагенту, поэтому его создают заранее. Обязательны kind (ooo|ip|self_employed|individual) и name; остальные реквизиты — по желанию. Реквизиты по ИНН подставляются двумя способами: одним запросом с флагом enrich_by_inn (см. ниже) либо двухшагово — сначала GET /api/dadata/party?inn=… (вернёт название, тип, КПП, ОГРН, адрес), затем подставьте нужные поля в POST …/counterparties. Банк по БИК — GET /api/dadata/bank?bic=…. Без флага сервер ничего не дозаполняет: вы явно контролируете, какие данные записываются.

POST https://api.apiin.ru/api/organizations/{org_id}/counterparties
Authorization: Bearer apiin_live_xxxxxxxx
Content-Type: application/json

{
  "kind": "ooo",
  "name": "ООО «Ромашка»",
  "inn": "7707083893",
  "kpp": "770701001"
}

В ответе — контрагент с полем id (UUID); его подставляют в счёт.

Чтобы не делать два запроса, можно попросить сервер заполнить реквизиты самому — флаг enrich_by_inn:

POST …/counterparties
{ "enrich_by_inn": true, "inn": "7707083893" }

→ наименование, тип, КПП, ОГРН и юридический адрес подставлены из ЕГРЮЛ/ЕГРИП
  • Заполняются только пустые поля: присланные вами значения важнее справочника и не перезаписываются.
  • При enrich_by_inn поля kind и name можно не присылать — они придут из справочника.
  • Ошибки: 400 inn_required (нет ИНН), 400 bad_inn (не 10/12 цифр), 400 enrich_failed (организация не найдена — контрагент не создаётся), 503 unavailable (справочник временно недоступен).
  • Если предпочитаете контролировать данные вручную, остаётся двухшаговый путь: GET /api/dadata/party?inn=…, затем создание с нужными полями.

Счёт

Цены — целое число в копейках (45 000 ₽ → 4500000). Обязательны kind, counterparty_id и хотя бы одна позиция. Номер присваивается автоматически по формату нумерации организации.

POST https://api.apiin.ru/api/organizations/{org_id}/documents
Authorization: Bearer apiin_live_xxxxxxxx
Content-Type: application/json

{
  "kind": "invoice",
  "counterparty_id": "<uuid контрагента>",
  "vat_mode": "none",
  "with_qr": true,
  "items": [
    { "name": "Консультация", "quantity": 1, "price": 4500000 }
  ]
}

vat_mode: none (без НДС) | usn (УСН, без выделения НДС) | osno (НДС «в том числе» — выделяется из цены по ставке каждой позиции vat_rate: 0/5/7/10/20/22, основная с 2026 года — 22). Поле manual_vat_amount (копейки, опционально) переопределяет отображаемую сумму НДС при ОСНО, не меняя total; null возвращает авторасчёт. У счёта-фактуры НДС считается «сверху» (цены без налога) — своя модель, manual_vat_amount не действует. Виды документов (kind): invoice (счёт), act (акт), invoice_contract (счёт-договор), invoice_factura (счёт-фактура), contract (договор), report (отчёт о выполненных работах), agent_report (отчёт агента), upd (универсальный передаточный документ, статус 1 или 2 в поле upd_status). В ответе — документ с id, номером и рассчитанными суммами (subtotal, vat_amount, total).

Собственная нумерация

Если у вас своя сквозная нумерация, передайте number прямо при создании — он будет использован вместо номера по формату организации:

POST …/documents
{ "kind": "invoice", "counterparty_id": "…", "number": "CR-2026/08-0042", "items": [ … ] }

POST …/documents/{id}/create-act
{ "number": "ACT-2026/08-0042" }        // тело необязательное
  • Внутренний счётчик нумерации сдвигается в любом случае — так следующий автоматический номер не столкнётся с уже выданным вручную.
  • Номер обязан быть свободным. Попытка выставить документ с номером, который уже занят, возвращает 409 с кодом number_taken — и при создании, и при PATCH. Пустой номер — 400 bad_number.
  • Уникальность считается в пределах вида документа, серии нумерации и года выпуска, среди действующих документов. Серия — это то, на каком уровне задан формат номера: аккаунт, контрагент или заказ. Поэтому счёт № 1 и акт № 1 не конфликтуют; у двух контрагентов с собственной нумерацией «1» тоже свои серии; а после сброса нумерации по году номер повторяется законно.
  • Аннулированный документ освобождает номер — типовой сценарий «ошиблись → аннулировали → выставили заново тем же номером».
  • Если номер не задан, сервис выдаёт следующий по формату и пропускает занятые вручную значения, поэтому автоматическая нумерация не спотыкается о ваши номера.
  • Сетевой ретрай с тем же Idempotency-Key конфликта не вызывает: повтор вернёт уже созданный документ (200), а не 409.
  • Номер можно изменить и позже — PATCH …/documents/{id} с полем number.

Оформление документа — сразу при создании

Все настройки вида документа принимаются тем же запросом POST …/documents, второй вызов PATCH не нужен:

{
  "kind": "invoice",
  "counterparty_id": "<uuid контрагента>",

  "with_qr": true,             // платёжный QR (по умолчанию true)
  "with_signatures": true,     // факсимиле подписи и печати (по умолчанию true)
  "with_sign_field": true,     // поле «подпись/печать» под документом; false — для ЭДО

  "hide_logo": true,           // не печатать логотип В ЭТОМ документе
  "hide_stamp": false,         // не печатать печать
  "hide_signature": false,     // не печатать подпись

  "logo_asset_id": null,       // взять ДРУГОЙ ассет вместо дефолта организации
  "stamp_asset_id": null,
  "signature_asset_id": null,

  "invoice_style": "individual",
  "accent_color": "#2563EB",
  "tag_ids": ["<uuid тега>"],
  "items": [ … ]
}
  • hide_logo / hide_stamp / hide_signature — «явно без ассета» для конкретного документа: сильнее и дефолта организации, и *_asset_id. Флаги независимы, поэтому «счёт без логотипа, но с печатью и подписью» — это hide_logo: true при with_signatures: true.
  • with_signatures: false убирает подпись и печать разом (общий выключатель), hide_* — точечно.
  • tag_ids проставляются при создании. Все теги должны принадлежать организации, иначе 400 с кодом tag_not_found (документ не создаётся) — берите UUID из GET …/tags. Изменить набор позже: PUT …/documents/{id}/tags.
  • Опущенные logo_asset_id / stamp_asset_id / signature_asset_id, invoice_style, accent_color берутся из настроек организации. У остальных флагов дефолт фиксированный и от организации не зависит: with_qr / with_signatures / with_sign_fieldtrue, hide_*false.
  • Те же поля принимает PATCH …/documents/{id}, если оформление меняется потом. Неизвестные поля тела запрос не отклоняют — они молча игнорируются, поэтому опечатка в имени поля не даст ошибки: сверяйте результат по ответу (201 возвращает документ целиком).
  • Копия (POST …/documents/{id}/copy) и производный документ (…/create-act) наследуют оформление, скрытые ассеты и теги источника.

PDF, Word и публичная ссылка

  • PDF: GET …/documents/{id}/pdf (добавьте ?download=1 для вложения);
  • Word: GET …/documents/{id}/docx;
  • Публичная ссылка на счёт: POST …/public-links с телом { "target": "document", "target_ref_id": "<id счёта>" } — в ответе url неугадываемой страницы (с QR и реквизитами). Вместо target_ref_id принимается алиас document_id — оба варианта равнозначны.

Оплата, акт, подпись

  • Отметить оплату полностью: POST …/documents/{id}/mark-paid — идемпотентен (повтор возвращает 200 и не рождает повторного события вебхука); документ получает paid_at (момент перехода в paid, сбрасывается при mark-unpaid);
  • Для бухгалтерской сверки сумм рекомендуем вместо метки записывать платёж: POST …/documents/{id}/payments с { "amount": <копейки>, "paid_at": "ГГГГ-ММ-ДД" } — остаётся след в истории оплат, статус пересчитывается автоматически;
  • Акт на основе счёта одним вызовом: POST …/documents/{id}/create-act — позиции и контрагент копируются из счёта, основание проставляется «По счёту № … от …», связка «счёт ↔ акт» создаётся автоматически. Повторный вызов вернёт уже созданный акт (без дублей);
  • Подписать акт: POST …/documents/{id}/sign. Связка закрывается сама, когда счёт оплачен, а акт подписан;
  • Аннулировать документ: POST …/documents/{id}/cancel (идемпотентно) — документ получает cancelled_at, в PDF появляется пометка «АННУЛИРОВАН», платёжный QR скрывается, а оплата/подпись/отправка отвечают 400 с кодом document_cancelled. Восстановление — POST …/uncancel. События вебхука: document.cancelled / document.uncancelled. Нюанс: если ссылка онлайн-оплаты была создана ДО аннулирования и покупатель по ней заплатил, платёж фиксируется (деньги поступили — запись нужна для сверки); владелец видит противоречие и решает — вернуть платёж или восстановить документ.

Язык документа

Поле languageru или en. Не передано — берётся язык организации (PATCH …/organizations/{id}, поле language). Неподдерживаемый код — 400 с language_not_supported.

POST …/documents
{ "kind": "invoice", "counterparty_id": "…", "language": "en", "currency": "USD",
  "items": [ { "name": "Software development, hours", "quantity": 150, "price": 10000 } ] }
  • Переводятся системные надписи: заголовок, стороны, графы таблицы, итоги, подпись, футер, штамп электронной подписи. Даты печатаются как 17 August 2026 (месяц словом — 08/17 и 17/08 читаются по-разному в США и Европе), суммы как 1,234.56 USD (код валюты, а не символ: $ неоднозначен), сумма прописью — по-английски.
  • Введённые вами данные не переводятся: наименования позиций, примечание, тело договора, реквизиты и названия сторон печатаются как есть.
  • Счёт на иностранном языке печатается международной формой Invoice — с блоком платёжных реквизитов вместо банковской «шапки». НДС и платёжный QR при этом сохраняются, если применимы: рублёвый счёт на английском несёт и то, и другое.
  • Счёт-фактура и УПД печатаются по-русски всегда — это утверждённые формы (постановление 1137, письмо ФНС ММВ-20-3/96@): переведённая графа перестаёт быть этой формой.
  • В PATCH …/documents/{id} язык меняется в любой момент; пустая строка "" возвращает документ к языку организации. Копия и производный документ (акт по счёту) наследуют язык источника.
  • Язык применяется и к онлайн-версии документа по публичной ссылке, к DOCX, к имени файла и к письму контрагенту при отправке.

Валюта документа

Поле currency принимает коды ISO 4217: RUB, USD, EUR, CNY, GBP, CHF, AED, TRY, INR, KZT, BYN, AMD, AZN, GEL, UZS, KGS, HKD, SGD, THB. Не передано — валюта по умолчанию из организации (default_currency, изначально RUB). Неподдерживаемый код — 400 с currency_not_supported.

  • Валюта фиксируется при создании и не меняется у существующего документа: суммы позиций уже записаны в её минорных единицах. Копия и производный документ наследуют валюту источника.
  • Если язык документа не задан явно, не рублёвый счёт печатается на английском — так работало до появления настройки языка, и поведение сохранено. Явный language это правило перекрывает.
  • Валют без разменной единицы (JPY, KRW, VND) в справочнике нет: суммы хранятся в 1/100 единицы, и печатать иены с копейками было бы неверно.
  • Платёжный QR существует только для рублёвых платежей (ГОСТ Р 56042-2014), поэтому у валютного счёта его нет. Акты, договоры, счета-фактуры и УПД остаются на российских формах независимо от валюты расчётов.

Срок оплаты и напоминания

У счёта есть срок оплаты due_date (YYYY-MM-DD) и вычисляемый флаг overdue — срок прошёл, счёт не оплачен и не аннулирован. Просрочка не отдельный payment_status: статус остаётся unpaid|partial|paid, а overdue приходит рядом с ним в реестре, карточке, вебхуках и на публичной странице.

POST …/documents
{ "kind": "invoice", "counterparty_id": "…", "due_date": "2026-09-01", "items": […] }

GET …/documents?overdue=true       // только просроченные
POST …/documents/{id}/remind    // напомнить письмом прямо сейчас
  • Поле опущено — срок подставится из настройки организации default_payment_days (дней от даты выставления), если она задана. Правило работает во всех путях создания: обычный POST, копия, «создать на основе», акт по счёту и повторяющиеся счета. Явный null — счёт без срока даже при включённой настройке. Срок принимают только invoice и invoice_contract: по акту и отчёту не платят, а у счёта-фактуры форма 1137 строгая. Попытка задать due_date другому виду — 400 с кодом due_date_not_applicable; по такому документу и просрочка не считается.
  • PATCH …/documents/{id} меняет срок (null — снять). Смена срока обнуляет журнал напоминаний — у нового срока свой цикл.
  • Автонапоминания включаются у организации: reminders_enabled, reminder_days_before, reminder_days_after. Раз в сутки уходит до трёх писем на счёт — за N дней до срока, в день срока и через N дней после. Письмо идёт на contact_email контрагента; без него напоминание пропускается.
  • POST …/documents/{id}/remind — ручное напоминание (право send:write). Ошибки: 400 due_date_missing (срок не задан), 400 document_paid, 400 document_cancelled, 400 contact_email_missing, 400 document_not_payable (не счёт), 409 reminder_already_sent — повторно можно после смены срока.
  • Событие вебхука document.overdue отправляется один раз, вместе с первым напоминанием о просрочке.

В PDF и Word счёта печатается строка «Оплатить до ДД.ММ.ГГГГ».

Подписание документа простой электронной подписью

Акты, отчёты и договоры подписываются получателем прямо по публичной ссылке. Правовая рамка — соглашение об электронном документообороте (ст. 9 Федерального закона № 63-ФЗ): подписант соглашается с ним перед запросом кода. Счета-фактуры и УПД так подписать нельзя — для них нужна усиленная квалифицированная подпись через оператора ЭДО.

PATCH …/public-links/{id}
{ "allow_esign": true }        // владелец разрешает подписание по ссылке

POST /p/i/{slug}/sign/request  // публично, без авторизации
{ "name": "Иванов Иван Иванович", "email": "ivan@example.com", "accept_terms": true }
→ { "sent_to": "iv***@example.com", "expires_in_minutes": 15 }

POST /p/i/{slug}/sign/confirm
{ "email": "ivan@example.com", "code": "123456" }
→ { "signed_at": "2026-08-12T14:12:18Z", "signer_name": "Иванов Иван Иванович",
     "document_hash": "28a84806abd84f9a…" }
  • Код действует 15 минут, допускается 5 попыток ввода. В базе хранится только хеш кода.
  • При подтверждении фиксируются ФИО и email подписанта, дата и время, IP, браузер и SHA-256 PDF документа на этот момент: изменение документа после подписания делает подпись несоответствующей новому файлу.
  • Подписанный акт получает sign_status: "signed", уходит событие document.sign_status_changed, а в PDF появляется штамп с ФИО, датой и идентификатором подписи.
  • Подписи возвращаются в публичном представлении документа (GET /p/i/{slug}, поле signatures).
  • Коды ошибок: esign_not_allowed, kind_not_esignable, terms_not_accepted, signer_name_required, code_expired, bad_code, too_many_attempts, already_signed, document_cancelled.

Импорт банковской выписки

Выписку из клиент-банка в формате 1CClientBankExchange можно загрузить и сверить с неоплаченными счетами. Процесс двухшаговый: сначала предпросмотр с предложенными сопоставлениями, затем запись подтверждённых платежей.

POST …/bank-import/preview        // multipart, поле file — файл выписки
→ {
  "account": "40702810900000000001",
  "total": 4, "incoming": 3, "matched": 2,
  "rows": [
    { "index": 0, "number": "125", "date": "12.08.2026", "amount": 4500000,
      "purpose": "Оплата по счету № 1 от 12.08.2026",
      "payer_name": "ООО «Ромашка»", "payer_inn": "7707083893",
      "document_id": "<uuid>", "document_number": "1",
      "confidence": "exact", "outstanding": 4500000 }, …
  ]
}

POST …/bank-import/apply
{ "items": [ { "document_id": "<uuid>", "amount": 4500000,
              "paid_at": "2026-08-12", "note": "Оплата по счету № 1" } ] }
→ { "applied": 1, "failed": 0, "results": [ { "document_id": "…", "status": "ok" } ] }
  • Отбираются только входящие платежи — те, где счёт получателя совпадает с расчётным счётом организации (учитываются и дополнительные счета из справочника). Если расчётный счёт не заполнен, приходит 400 account_missing.
  • confidence показывает, как подобран документ: exact — номер счёта найден в назначении платежа, likely — совпали ИНН плательщика и сумма остатка, weak — совпала только сумма и кандидат единственный. Неоднозначные строки остаются без сопоставления: сервер не угадывает.
  • apply обрабатывает строки независимо — ошибка по одному документу не отменяет остальные; в results у каждой строки свой status (ok, not_found, bad_amount, error). За один вызов принимается до 500 платежей.
  • Записанные платежи ничем не отличаются от внесённых вручную: статус оплаты пересчитывается, событие document.payment_status_changed уходит в вебхук.
  • Файл до 4 МиБ, кодировка windows-1251 или UTF-8 — определяется автоматически.

Теги документов

Теги — цветные метки уровня организации: помечайте документы, созданные вашей интеграцией, и отличайте их от ручных. Справочник: GET/POST …/tags, PATCH/DELETE …/tags/{id} (имя ≤50 символов уникально, цвет #RRGGBB). Присвоение — декларативно: PUT …/documents/{id}/tags с { "tag_ids": […] } (полная замена набора; пустой массив снимает все).

// Пометить автоматический счёт прямо при создании:
POST …/documents
{ "kind": "invoice", "counterparty_id": "…", "tag_ids": ["<uuid тега>"], "items": […] }

// Фильтры списка:
GET …/documents?tag_ids=<uuid>,<uuid>   // с любым из тегов
GET …/documents?untagged=1               // только без тегов

«Создать подобный» и «создать на основе» наследуют теги источника. В payload каждого события вебхука документ несёт tag_ids — маршрутизируйте события по своим меткам. Все связи организации одним запросом: GET …/document-tags.

Предложения правок

Получатель документа может прислать предложение изменений, если владелец включил разрешение на публичной ссылке: PATCH …/public-links/{id} с { "allow_proposals": true }. Предложение — это неизменяемый снапшот содержания плюс база сравнения, поэтому подсветка изменений не «плывёт», даже если документ позже правили. Изменить можно ТОЛЬКО содержание (позиции, дата, описание, основание, примечание, текст договора) — реквизиты сторон, номер и оформление недоступны.

  • Лента предложений документа: GET …/documents/{id}/proposals — каждое несёт готовый diff (список изменений «было → стало», счётчики позиций, итог до и после) и summary;
  • Принять: POST …/proposals/{pid}/accept — снапшот применяется к документу целиком, суммы пересчитывает сервер;
  • Отклонить: POST …/proposals/{pid}/decline (можно с причиной в message);
  • Встречный вариант: POST …/proposals/{pid}/counter с payload — исходное предложение помечается «заменено», а решение переходит к другой стороне (сравниваются два последних варианта).

Флаг outdated в предложении означает, что документ менялся уже после его отправки — стоит пересмотреть сравнение перед принятием. События вебхука: document.proposal_submitted и document.proposal_accepted. Решение принимает пользователь кабинета: API-ключ читает предложения, но не решает за человека.

Вебхуки

Чтобы узнавать об оплате и подписи без опроса, настройте вебхук (право webhooks:write, роль admin или owner). Приёмников может быть до 10 на организацию — например, отдельные адреса для прода, тестового стенда и CRM; у каждого своё имя, подписка и секрет подписи:

POST https://api.apiin.ru/api/organizations/{org_id}/webhooks
Authorization: Bearer apiin_live_xxxxxxxx
Content-Type: application/json

{
  "name": "Прод",
  "url": "https://example.com/apiin-webhook",
  "events": ["document.payment_status_changed", "document.sign_status_changed"]
}

Управление приёмниками:

  • GET …/webhooks — список (id, имя, подписка, статус; БЕЗ секретов);
  • POST …/webhooks — создать (409 webhook_limit_reached — лимит 10);
  • PATCH …/webhooks/{id} — изменить переданные поля (имя, url, события, enabled для паузы);
  • POST …/webhooks/{id}/rotate — перевыпустить секрет (ответ содержит новый);
  • GET …/webhooks/{id}/secret — секрет приёмника отдельным запросом;
  • POST …/webhooks/{id}/test — тестовое событие webhook.test;
  • DELETE …/webhooks/{id} — удалить приёмник;
  • GET …/webhooks/{id}/deliveries?limit= — журнал ОДНОГО приёмника;
  • GET …/webhooks/deliveries?limit=&webhook_id= — сводный журнал организации; в каждой записи есть webhook_id, поэтому видно, чей это отказ.

Прежние singleton-пути GET/PUT/DELETE …/webhook, …/webhook/test и …/webhook/deliveries продолжают работать и относятся к первому вебхуку организации — уже настроенные интеграции менять не нужно.

События (12):

  • document.created — создан документ;
  • document.sent — документ отправлен на email;
  • document.payment_status_changed — статус оплаты (unpaid|partial|paid);
  • document.sign_status_changed — статус подписи акта;
  • document.overdue — срок оплаты прошёл, счёт не оплачен;
  • document.cancelled — документ аннулирован;
  • document.uncancelled — документ восстановлен после аннулирования;
  • document.proposal_submitted — получатель прислал правки;
  • document.proposal_accepted — правки приняты, документ изменён;
  • recurring.generated — сгенерирован повторяющийся счёт;
  • access.requested — запрошен доступ к документу;
  • access.granted — доступ к документу выдан.

Каждое событие — POST на ваш адрес. Пример тела:

{
  "event": "document.payment_status_changed",
  "occurred_at": "2026-08-11T10:32:07Z",
  "organization_id": "<uuid>",
  "document": {
    "id": "<uuid>",
    "kind": "invoice",
    "number": "СЧ-42",
    "counterparty_id": "<uuid>",
    "total": 4500000,
    "payment_status": "paid",
    "paid_at": "2026-08-11T10:32:07Z",
    "due_date": "2026-08-20",
    "overdue": false,
    "sign_status": "not_signed",
    "cancelled_at": null,
    "tag_ids": ["<uuid тега>"]
  }
}

Состав document в payload фиксирован: id, kind, number, counterparty_id, total, payment_status, paid_at, due_date, overdue, sign_status, cancelled_at, tag_ids. Полей достаточно для сверки оплат и долгов без дополнительного запроса документа.

Заголовки запроса:

  • X-Apiin-Event — имя события;
  • X-Apiin-Delivery — UUID доставки (используйте для идемпотентности приёмника);
  • X-Apiin-Signature: sha256=<hex> — HMAC-SHA256 над сырыми байтами тела запроса (проверяйте подпись до парсинга JSON — по исходной строке, а не по пересериализованному объекту). Секрет подписи возвращается при создании (POST …/webhooks), при перевыпуске (POST …/webhooks/{id}/rotate) и по отдельному запросу GET …/webhooks/{id}/secret — в списках его нет, чтобы он не уезжал по сети без необходимости. Проверяйте подпись, прежде чем доверять телу.

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

HMAC-SHA256 считается от сырых байтов тела запроса ключом-секретом вебхука; результат — hex в нижнем регистре, а заголовок содержит его с префиксом: X-Apiin-Signature: sha256=<hex>. Сравнивайте значения константным по времени сравнением и только потом парсите JSON.

// Node.js (Express): важно получить именно сырое тело
import express from 'express';
import crypto from 'node:crypto';

app.post('/apiin-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const received = String(req.get('X-Apiin-Signature') || '');
  const expected =
    'sha256=' + crypto.createHmac('sha256', process.env.APIIN_WEBHOOK_SECRET)
      .update(req.body)             // Buffer с исходными байтами, НЕ JSON.stringify
      .digest('hex');

  const ok = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  // …обработка; ответить 2xx в течение 10 секунд
  res.sendStatus(200);
});
# Python (FastAPI)
import hmac, hashlib
from fastapi import Request, HTTPException

@app.post("/apiin-webhook")
async def apiin_webhook(request: Request):
    raw = await request.body()                     # сырые байты
    expected = "sha256=" + hmac.new(SECRET.encode(), raw, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(request.headers.get("X-Apiin-Signature", ""), expected):
        raise HTTPException(status_code=401)
    event = json.loads(raw)
    return {"ok": True}

Типичная ошибка — считать подпись по пересериализованному объекту: порядок ключей и пробелы изменятся, и подпись не совпадёт. Секрет берётся из ответа создания или перевыпуска либо запрашивается отдельно: GET …/webhooks/{id}/secret. Если приёмников несколько, у каждого свой секрет — проверяйте подпись тем, который соответствует адресу.

Доставка считается успешной при ответе 2xx за 10 секунд. Иначе — повторы с увеличивающейся паузой (до 6 попыток за ~9 часов), после чего доставка помечается несостоявшейся. Журнал: GET …/webhooks/{id}/deliveries (или сводный GET …/webhooks/deliveries); тестовое событие: POST …/webhooks/{id}/test. Событие уходит на КАЖДЫЙ включённый приёмник, подписанный на него, — доставки независимы, отказ одного не влияет на остальные.

Адрес вебхука должен быть публичным https на порту 443. Локальные и приватные адреса отклоняются — для локальной отладки используйте туннель (например, ngrok).

Списки: пагинация и синхронизация

Списки документов и контрагентов принимают необязательные query-параметры. Без них возвращается весь список (как раньше). Для интеграций:

  • ?limit= и ?offset= — пагинация (limit от 1 до 500).
  • ?updated_since= — только изменённые с указанного момента (формат RFC3339, например 2026-07-17T10:00:00Z) — для инкрементальной синхронизации.
  • ?inn= (только контрагенты) — точный поиск по ИНН: удобно проверить, есть ли уже такой контрагент, перед созданием.

Идемпотентность (защита от дублей)

Создающие запросы POST /documents и POST /counterparties принимают заголовок Idempotency-Key: <уникальная строка>. Первый запрос создаёт сущность и запоминает результат на 24 часа; повтор с тем же ключом вернёт ту же сущность (статус 200), а не дубль с новым номером. Если предыдущий запрос ещё выполняется — повтор получит 409. Используйте это при ретраях после сетевого таймаута: сгенерируйте ключ один раз на операцию и отправляйте его при каждой попытке.

Ошибки

Ошибки возвращаются с соответствующим HTTP-статусом (400 — неверные данные, 401 — нет/недействительный ключ, 403 — не хватает прав, 404 — не найдено или чужая организация, 409 — конфликт, 429 — превышен лимит) и телом { "error": "<описание>", "code": "<машинный код>" }. Поле code — стабильный идентификатор (например rate_limited, not_found, document_protected); в коде опирайтесь на него, а не на текст error. При 429 приходит заголовок Retry-After (секунды до повтора) и X-RateLimit-*.

Создайте первый ключ в кабинете. Для управления документами из нейросети — раздел MCP для нейросетей.