Для 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. Сделка: компания и выбранный реквизит
GET /v1/deals/:id отдаёт компанию сделки в поле companyId.
cURL
curl -s -H "X-Api-Key: $VIBE_API_KEY" "$VIBE_URL/v1/deals/5289?select=id,title,companyId"
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()
{ "success": true, "data": { "id": 5289, "title": "Поставка оборудования", "companyId": 42 } }
У компании может быть несколько реквизитов, и какой из них относится к этой сделке, знает связь реквизитов. GET /v1/requisite-links/:entityTypeId/:entityId возвращает её по паре значений: 2 — сделка, дальше идентификатор сделки.
cURL
curl -s -H "X-Api-Key: $VIBE_API_KEY" "$VIBE_URL/v1/requisite-links/2/5289"
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
{
"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, когда он вернул пустой результат.
{ "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
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
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()
{
"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
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
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()
{
"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
curl -s -H "X-Api-Key: $VIBE_API_KEY" \
"$VIBE_URL/v1/addresses?filter[entityTypeId]=8&filter[entityId]=305"
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()
{
"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
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
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()
{
"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 с фильтром и не формирует второй. Проверка защищает от повторного запуска, но не от двух одновременных: обе копии увидят пустую выдачу и создадут по документу.
Отказы на создании документа. Неизвестный templateId — 404 с кодом 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
// 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
})