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

**Сложность:** средний | **Скоупы:** 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`](/docs/entities/deals/get) отдаёт компанию сделки в поле `companyId`.

### cURL

```bash
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`](/docs/entities/requisite-links/get) возвращает её по паре значений: `2` — сделка, дальше идентификатор сделки.

### cURL

```bash
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`](/docs/entities/requisites/get). Когда связи нет, реквизиты компании отбирает [`POST /v1/requisites/search`](/docs/entities/requisites/search) по паре условий: `entityTypeId` равен `4` — компания, `entityId` — идентификатор компании из шага 1.

### cURL

```bash
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`](/docs/entities/requisite-presets/preset-fields/list).

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

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

### cURL

```bash
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`](/docs/entities/addresses/list) по фильтру: `entityTypeId` равен `8` — реквизит, `entityId` — идентификатор реквизита.

### cURL

```bash
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`. Какие коды доступны порталу, зависит от его страновой зоны, поэтому выбирайте тип из того, что реально пришло в ответе. Полный справочник кодов — [Получить адрес](/docs/entities/addresses/get).

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

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

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

### cURL

```bash
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`](/docs/entities/documents/get). В том же ответе приходит `isTransformationError` — признак того, что преобразование не удалось.

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

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

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

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

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

## Полный код

Запуск: `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
})
```

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

- [Реквизиты](/docs/entities/requisites)
- [Банковские реквизиты](/docs/entities/bank-details)
- [Адреса](/docs/entities/addresses)
- [Связи реквизитов](/docs/entities/requisite-links)
- [Документы](/docs/entities/documents)
- [Шаблоны документов](/docs/entities/doc-templates)
