Для AI-агентов: markdown этой страницы — /docs-content/recipes/document-requisites.md индекс документации — /llms.txt

Реквизиты компании для генерации документа

Сложность: средний | Скоупы: crm, documentgenerator | Стек: cURL / JavaScript

Собираем по сделке юридическую карточку клиента — реквизит компании, банковский счёт и юридический адрес — и передаём собранные значения в шаблон документа. На выходе документ на портале со ссылками на файл.

Что понадобится

  • API-ключ Вайбкод со скоупами crm и documentgenerator
  • Сделка, у которой заполнена компания, а у компании заведены реквизиты
  • Шаблон документа на портале
  • Node.js 18 или новее

Во всех примерах $VIBE_URL — базовый адрес https://vibecode.bitrix24.tech, $VIBE_API_KEY — ваш API-ключ.

Как устроено решение

  1. По сделке узнаём компанию и то, какой её реквизит выбран для этой сделки.
  2. Читаем реквизит: название, ИНН, КПП, ОГРН, руководитель.
  3. Берём банковский счёт реквизита: расчётный счёт, БИК, корреспондентский счёт.
  4. Выбираем среди адресов реквизита юридический.
  5. Передаём собранные значения в шаблон и получаем документ со ссылками на файл.

Примеры шагов показывают отдельные вызовы. Готовый к запуску скрипт — в разделе «Полный код».

Шаг 1. Сделка: компания и выбранный реквизит

GET /v1/deals/:id отдаёт компанию сделки в поле companyId.

cURL

Terminal
curl -s -H "X-Api-Key: $VIBE_API_KEY" "$VIBE_URL/v1/deals/5289?select=id,title,companyId"

JavaScript

javascript
const res = await fetch(`${VIBE_URL}/v1/deals/${dealId}?select=id,title,companyId`, {
  headers: { 'X-Api-Key': VIBE_API_KEY },
})
const { data: deal } = await res.json()
JSON
{ "success": true, "data": { "id": 5289, "title": "Поставка оборудования", "companyId": 42 } }

У компании может быть несколько реквизитов, и какой из них относится к этой сделке, знает связь реквизитов. GET /v1/requisite-links/:entityTypeId/:entityId возвращает её по паре значений: 2 — сделка, дальше идентификатор сделки.

cURL

Terminal
curl -s -H "X-Api-Key: $VIBE_API_KEY" "$VIBE_URL/v1/requisite-links/2/5289"

JavaScript

javascript
const res = await fetch(`${VIBE_URL}/v1/requisite-links/2/${dealId}`, {
  headers: { 'X-Api-Key': VIBE_API_KEY },
})
const link = res.status === 404 ? null : (await res.json()).data
JSON
{
  "success": true,
  "data": {
    "entityTypeId": 2,
    "entityId": 5289,
    "requisiteId": 305,
    "bankDetailId": 0,
    "mcRequisiteId": 3,
    "mcBankDetailId": 3
  }
}

Значение 0 в любом из четырёх идентификаторов означает, что привязки нет. Поля requisiteId и bankDetailId описывают сторону клиента, mcRequisiteId и mcBankDetailId — вашу собственную компанию. Оба набора идентификаторов приходят одним вызовом, а сами реквизиты читаются дальше по шагам — каждый по своему идентификатору.

Если связи у сделки нет, вызов отвечает 404. Проверяйте именно статус ответа: у этого 404 два кода — ENTITY_NOT_FOUND, когда Битрикс24 вернул ошибку, и NOT_FOUND, когда он вернул пустой результат.

JSON
{ "success": false, "error": { "code": "ENTITY_NOT_FOUND", "message": "Not found" } }

Это рабочее состояние, а не сбой: реквизит сделке ещё не выбирали. Тогда берём реквизиты компании из шага 2 и выбираем первый.

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

Когда связь назвала requisiteId, реквизит читается по идентификатору — GET /v1/requisites/:id. Когда связи нет, реквизиты компании отбирает POST /v1/requisites/search по паре условий: entityTypeId равен 4 — компания, entityId — идентификатор компании из шага 1.

cURL

Terminal
curl -s -X POST -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
  "$VIBE_URL/v1/requisites/search" \
  -d '{
    "filter": { "entityTypeId": 4, "entityId": 42 },
    "select": ["id", "name", "presetId", "rqCompanyName", "rqCompanyFullName", "rqVatId", "rqInn", "rqKpp", "rqOgrn", "rqDirector"],
    "limit": 50
  }'

JavaScript

javascript
const res = await fetch(`${VIBE_URL}/v1/requisites/search`, {
  method: 'POST',
  headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    filter: { entityTypeId: 4, entityId: deal.companyId },
    select: ['id', 'name', 'presetId', 'rqCompanyName', 'rqCompanyFullName', 'rqVatId', 'rqInn', 'rqKpp', 'rqOgrn', 'rqDirector'],
    limit: 50,
  }),
})
const { data: requisites } = await res.json()
JSON
{
  "success": true,
  "data": [
    {
      "id": 305,
      "name": "Основной реквизит",
      "presetId": 1,
      "rqCompanyName": "ООО «Ромашка»",
      "rqCompanyFullName": "ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «РОМАШКА»",
      "rqVatId": null,
      "rqInn": "7701234567",
      "rqKpp": "770101001",
      "rqOgrn": "1157746000000",
      "rqDirector": "Соколов Игорь Викторович"
    }
  ],
  "meta": { "total": 1, "hasMore": false, "durationMs": 360 }
}

Набор заполненных полей задаёт шаблон реквизитов — presetId. У шаблона «Организация» это ИНН, КПП, ОГРН и руководитель, у шаблона «Физ. лицо» — паспортные поля, а rqKpp и rqOgrn там останутся пустыми. Состав полей конкретного шаблона отдаёт GET /v1/requisite-presets/:presetId/fields.

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

Банковский счёт принадлежит реквизиту, а не компании: в фильтре entityId — идентификатор реквизита из шага 2. Владелец у счёта всегда одного типа, поэтому пара значений, как у адреса, здесь не нужна. Отбирает счета POST /v1/bank-details/search.

cURL

Terminal
curl -s -X POST -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
  "$VIBE_URL/v1/bank-details/search" \
  -d '{
    "filter": { "entityId": 305 },
    "select": ["id", "name", "rqBankName", "rqBik", "rqAccNum", "rqCorAccNum"],
    "limit": 50
  }'

JavaScript

javascript
const res = await fetch(`${VIBE_URL}/v1/bank-details/search`, {
  method: 'POST',
  headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    filter: { entityId: requisite.id },
    select: ['id', 'name', 'rqBankName', 'rqBik', 'rqAccNum', 'rqCorAccNum'],
    limit: 50,
  }),
})
const { data: accounts } = await res.json()
JSON
{
  "success": true,
  "data": [
    {
      "id": 5,
      "name": "Основной счёт",
      "rqBankName": "АО «Первый банк»",
      "rqBik": "044599999",
      "rqAccNum": "40702810000000000001",
      "rqCorAccNum": "30101810000000000002"
    }
  ],
  "meta": { "total": 1, "hasMore": false, "durationMs": 583 }
}

Счёта может не быть — тогда data придёт пустым массивом, а meta.total нулём. Для акта или доверенности этого достаточно, для счёта на оплату — нет, поэтому проверяйте массив до подстановки в шаблон.

Шаг 4. Юридический адрес

Адреса реквизита отдаёт GET /v1/addresses по фильтру: entityTypeId равен 8 — реквизит, entityId — идентификатор реквизита.

cURL

Terminal
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
  "$VIBE_URL/v1/addresses?filter[entityTypeId]=8&filter[entityId]=305"

JavaScript

javascript
const res = await fetch(
  `${VIBE_URL}/v1/addresses?filter[entityTypeId]=8&filter[entityId]=${requisite.id}`,
  { headers: { 'X-Api-Key': VIBE_API_KEY } },
)
const { data: addresses } = await res.json()
JSON
{
  "success": true,
  "data": [
    {
      "typeId": 6,
      "entityTypeId": 8,
      "entityId": 305,
      "address1": "ул. Полевая, д. 5, этаж 1",
      "address2": "помещ. 2",
      "city": "Москва",
      "postalCode": "101000",
      "region": null,
      "province": "Москва",
      "country": "Россия",
      "countryCode": null,
      "locAddrId": 465,
      "anchorTypeId": 4,
      "anchorId": 42
    }
  ],
  "meta": { "total": 1, "hasMore": false }
}

Тип адреса задаёт typeId. Для документа берём юридический — 6, а когда его нет, фактический — 1. Какие коды доступны порталу, зависит от его страновой зоны, поэтому выбирайте тип из того, что реально пришло в ответе. Полный справочник кодов — Получить адрес.

Строку для шаблона собирайте из полей ответа в нужном вам порядке: postalCode, country, province, city, address1, address2. Незаполненные поля приходят как null, поэтому до склейки отбрасывайте пустые значения и повторы: у городов федерального значения city и province совпадают, и без этого город попадёт в строку дважды.

Шаг 5. Документ по шаблону

POST /v1/documents формирует файл по шаблону. Идентификатор шаблона берётся из GET /v1/doc-templates, собранные значения передаются в values, а value — ваш внешний идентификатор объекта-источника.

cURL

Terminal
curl -s -X POST -H "X-Api-Key: $VIBE_API_KEY" -H "Content-Type: application/json" \
  "$VIBE_URL/v1/documents" \
  -d '{
    "templateId": 237,
    "providerClassName": "Bitrix\\DocumentGenerator\\DataProvider\\Rest",
    "value": "DEAL-5289",
    "values": {
      "CompanyName": "ООО «Ромашка»",
      "CompanyInn": "7701234567",
      "CompanyAddress": "101000, Россия, Москва, ул. Полевая, д. 5, этаж 1, помещ. 2"
    }
  }'

JavaScript

javascript
const res = await fetch(`${VIBE_URL}/v1/documents`, {
  method: 'POST',
  headers: { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    templateId: TEMPLATE_ID,
    providerClassName: 'Bitrix\\DocumentGenerator\\DataProvider\\Rest',
    value: `DEAL-${dealId}`,
    values,
  }),
})
const { data: document } = await res.json()
JSON
{
  "success": true,
  "data": {
    "id": 1895,
    "title": "Договор поставки А003//08/2026",
    "number": "А003//08/2026",
    "templateId": 237,
    "provider": "Bitrix\\DocumentGenerator\\DataProvider\\Rest",
    "value": "DEAL-5289",
    "values": {
      "productsTableVariant": "",
      "_creationMethod": "rest",
      "CompanyName": "ООО «Ромашка»",
      "CompanyInn": "7701234567",
      "CompanyAddress": "101000, Россия, Москва, ул. Полевая, д. 5, этаж 1, помещ. 2"
    },
    "createTime": "2026-08-10T08:35:44.000Z",
    "createdBy": 1,
    "stampsEnabled": false,
    "isTransformationError": false,
    "downloadUrl": "/bitrix/services/main/ajax.php?action=documentgenerator.api.document.getfile&SITE_ID=s1&id=1895",
    "downloadUrlMachine": "https://<portal>/rest/1/<token>/documentgenerator.api.document.getfile/?token=..."
  }
}

Ссылки с суффиксом Machine предназначены приложению и работают без сессии сотрудника, остальные открываются в браузере вошедшего пользователя. <portal> — домен портала.

Ограничения

Реквизит выбирает связь, а не порядок записей. У компании бывает несколько реквизитов — старый и действующий, головная организация и филиал. Первый в выдаче не равен нужному, поэтому связь сделки в шаге 1 идёт раньше поиска по компании и перекрывает его результат.

Ссылка на PDF появляется не сразу. Ответ создания несёт downloadUrl, а pdfUrl и imageUrl приходят после того, как файл преобразован. Получить их можно повторным вызовом GET /v1/documents/:id. В том же ответе приходит isTransformationError — признак того, что преобразование не удалось.

Повторный запуск создаёт новый документ. Отдельного признака «документ по этой сделке уже сформирован» у эндпоинта создания нет. Скрипт ниже проверяет наличие документа по своему же значению value через GET /v1/documents с фильтром и не формирует второй. Проверка защищает от повторного запуска, но не от двух одновременных: обе копии увидят пустую выдачу и создадут по документу.

Отказы на создании документа. Неизвестный templateId404 с кодом ENTITY_NOT_FOUND и сообщением «Шаблон не найден». Если провайдер данных не разобрал value, приходит 422 с кодом BITRIX_ERROR. Скрипт останавливается на обоих и печатает сообщение портала.

Имена меток задаёт шаблон. Ключи в values — это метки конкретного шаблона документа, а не имена полей реквизита. Перед первым запуском сверьте набор меток с тем шаблоном, который указан в templateId.

Какие поля реквизита заполнены, решает шаблон реквизита. Встроенный шаблон «Организация» российского аккаунта заводит название, ИНН, КПП, ОГРН, директора и адрес. Поля из шаблона другой страны остаются в схеме сущности и запрашиваются без ошибки, но приходят пустыми. Фактический набор для своего аккаунта возьмите через GET /v1/requisite-presets/:presetId/fields и оставьте в values только то, что шаблон заполняет.

Очередь портала. Портал выполняет ограниченное число запросов к API одновременно. Цепочка вызовов на каждую сделку — по одному на шаг плюс проверка уже созданного документа — в цикле по большому списку получит 429 с кодом QUEUE_OVERFLOW или QUEUE_TIMEOUT. Рекомендованная пауза приходит в заголовке Retry-After и дублируется в error.retryAfter. Полный перечень кодов — Ошибки.

Полный код

Запуск: VIBE_API_KEY=<ключ> DEAL_ID=<id сделки> TEMPLATE_ID=<id шаблона> node deal-document.js

javascript
// deal-document.js — собирает реквизиты клиента по сделке и формирует документ
const VIBE_URL = process.env.VIBE_URL ?? 'https://vibecode.bitrix24.tech'
const VIBE_API_KEY = process.env.VIBE_API_KEY
if (!VIBE_API_KEY) throw new Error('Переменная окружения VIBE_API_KEY не задана')

const DEAL_ID = Number(process.env.DEAL_ID)
const TEMPLATE_ID = Number(process.env.TEMPLATE_ID)
if (!DEAL_ID || !TEMPLATE_ID) throw new Error('Задайте DEAL_ID и TEMPLATE_ID')

const PROVIDER = 'Bitrix\\DocumentGenerator\\DataProvider\\Rest'
const headers = { 'X-Api-Key': VIBE_API_KEY, 'Content-Type': 'application/json' }
const MAX_RETRIES = 5

// Отказ 429 означает, что запрос до Битрикс24 не дошёл, — повторить его безопасно.
// Сколько ждать, говорит заголовок Retry-After, а при его отсутствии — error.retryAfter.
// К паузе добавляется случайная доля секунды, чтобы параллельные копии скрипта
// не пошли на повтор одновременно.
async function apiCall(url, init = {}, { allow404 = false } = {}) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url, { headers, ...init })
    if (res.status === 404 && allow404) return null
    const body = await res.json().catch(() => null)

    if (res.status === 429 && attempt < MAX_RETRIES) {
      const advised = Number(res.headers.get('Retry-After') || body?.error?.retryAfter)
      const base = advised > 0 ? advised : Math.min(2 ** attempt, 30)
      // Разброс пропорционален паузе, чтобы параллельные копии скрипта не пошли
      // на повтор одновременно. Потолок 300 секунд — максимум, который называет портал.
      const wait = Math.min(base, 300) * (0.75 + Math.random() * 0.5)
      console.warn(`Очередь портала занята, повтор через ${Math.round(wait)} с`)
      await new Promise(resolve => setTimeout(resolve, wait * 1000))
      continue
    }

    if (!body?.success) throw new Error(body?.error?.message ?? `запрос отклонён (${res.status})`)
    return body.data
  }
}

async function resolveRequisite(dealId) {
  const deal = await apiCall(`${VIBE_URL}/v1/deals/${dealId}?select=id,title,companyId`)
  if (!deal?.companyId) throw new Error(`у сделки ${dealId} не заполнена компания`)

  // Связь знает, какой именно реквизит компании выбран для этой сделки.
  const link = await apiCall(`${VIBE_URL}/v1/requisite-links/2/${dealId}`, {}, { allow404: true })
  if (link?.requisiteId) {
    const byLink = await apiCall(`${VIBE_URL}/v1/requisites/${link.requisiteId}`, {}, { allow404: true })
    if (byLink) return { deal, requisite: byLink, bankDetailId: link.bankDetailId || null }
    console.warn(`  реквизит ${link.requisiteId} из связи не найден — берём реквизиты компании`)
  }

  // Связи нет — берём реквизиты компании и выбираем первый.
  const list = await apiCall(`${VIBE_URL}/v1/requisites/search`, {
    method: 'POST',
    body: JSON.stringify({
      filter: { entityTypeId: 4, entityId: deal.companyId },
      select: ['id', 'name', 'presetId', 'rqCompanyName', 'rqCompanyFullName', 'rqVatId', 'rqInn', 'rqKpp', 'rqOgrn', 'rqDirector'],
      limit: 50,
    }),
  })
  if (!list?.length) throw new Error(`у компании ${deal.companyId} нет реквизитов`)
  return { deal, requisite: list[0], bankDetailId: null }
}

async function resolveAccount(requisiteId, preferredId) {
  const list = await apiCall(`${VIBE_URL}/v1/bank-details/search`, {
    method: 'POST',
    body: JSON.stringify({
      filter: { entityId: requisiteId },
      select: ['id', 'name', 'rqBankName', 'rqBik', 'rqAccNum', 'rqCorAccNum'],
      limit: 50,
    }),
  })
  if (!list?.length) return null
  const chosen = list.find(account => account.id === preferredId)
  if (preferredId && !chosen) console.warn(`  счёт ${preferredId} из связи не найден — подставлен ${list[0].id}`)
  return chosen ?? list[0]
}

// Юридический адрес — typeId 6, запасной вариант при его отсутствии — фактический, typeId 1.
async function resolveLegalAddress(requisiteId) {
  const list = await apiCall(
    `${VIBE_URL}/v1/addresses?filter[entityTypeId]=8&filter[entityId]=${requisiteId}`,
  )
  if (!list?.length) return null
  return list.find(a => a.typeId === 6) ?? list.find(a => a.typeId === 1) ?? null
}

function formatAddress(address) {
  if (!address) return ''
  return [
    address.postalCode,
    address.country,
    address.province,
    address.city,
    address.address1,
    address.address2,
  ].filter((part, i, all) => part && all.indexOf(part) === i).join(', ')
}

async function findExistingDocument(value) {
  const query = new URLSearchParams({ 'filter[value]': value, select: 'id,value,number' })
  const list = await apiCall(`${VIBE_URL}/v1/documents?${query}`)
  return list?.find(doc => doc.value === value) ?? null
}

async function main() {
  // Значение value стабильно для сделки, поэтому проверка идёт ПЕРВОЙ: для уже
  // обработанной сделки это один вызов портала вместо пяти.
  const value = `DEAL-${DEAL_ID}`
  const existing = await findExistingDocument(value)
  if (existing) {
    console.log(`Документ по сделке уже сформирован: ${existing.id} (${existing.number})`)
    return
  }

  const { deal, requisite, bankDetailId } = await resolveRequisite(DEAL_ID)
  const account = await resolveAccount(requisite.id, bankDetailId)
  const address = await resolveLegalAddress(requisite.id)

  const values = {
    CompanyName: requisite.rqCompanyName ?? requisite.name ?? '',
    CompanyFullName: requisite.rqCompanyFullName ?? '',
    CompanyInn: requisite.rqInn ?? '',
    CompanyKpp: requisite.rqKpp ?? '',
    CompanyOgrn: requisite.rqOgrn ?? '',
    CompanyDirector: requisite.rqDirector ?? '',
    CompanyAddress: formatAddress(address),
    BankName: account?.rqBankName ?? '',
    BankBik: account?.rqBik ?? '',
    BankAccount: account?.rqAccNum ?? '',
    BankCorAccount: account?.rqCorAccNum ?? '',
  }

  console.log(`Сделка ${deal.id}: реквизит ${requisite.id}, счёт ${account?.id ?? 'не заведён'}`)
  // Для акта или доверенности пустых полей счёта достаточно. Для счёта на оплату — нет:
  // если формируете платёжный документ, здесь нужен выход, а не предупреждение.
  if (!account) console.warn('  банковский счёт не найден — поля счёта останутся пустыми')
  if (!address) console.warn('  адрес не найден — поле адреса останется пустым')

  const document = await apiCall(`${VIBE_URL}/v1/documents`, {
    method: 'POST',
    body: JSON.stringify({ templateId: TEMPLATE_ID, providerClassName: PROVIDER, value, values }),
  })
  console.log(`Документ ${document.id} создан: ${document.title}`)
  // Ссылка downloadUrlMachine из ответа несёт токен доступа — в журнал её не пишем.
  console.log(`Ссылка для пользователя: ${document.downloadUrl}`)
}

main().catch(error => {
  console.error(error.message)
  process.exitCode = 1
})

Смотрите также