# Company requisites for document generation

**Difficulty:** medium | **Scopes:** crm, documentgenerator | **Stack:** cURL / JavaScript

Collect the client's legal details for a deal — the company requisite, the bank account and the legal address — and pass the collected values into a document template. The result is a document in your Bitrix24 account with links to the file.

## What you need

- A Vibecode API key with the `crm` and `documentgenerator` scopes
- A deal with a company filled in, and a company that has requisites
- A document template in your Bitrix24 account
- Node.js 18 or newer

In all examples `$VIBE_URL` is the base address `https://vibecode.bitrix24.com` and `$VIBE_API_KEY` is your API key.

## How the solution works

1. From the deal, find the company and which of its requisites is selected for this deal.
2. Read the requisite: the company name and the VAT ID.
3. Take the requisite's bank account: current account, BIC, correspondent account.
4. Pick the legal one among the requisite's addresses.
5. Pass the collected values into the template and get a document with links to the file.

The step examples show individual calls. The ready-to-run script is in the "Full code" section.

## Step 1. Deal: company and the selected requisite

[`GET /v1/deals/:id`](/docs/entities/deals/get) returns the deal's company in the `companyId` field.

### 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": "Equipment supply", "companyId": 42 } }
```

A company can have several requisites, and the requisite link determines which of them applies to this deal. [`GET /v1/requisite-links/:entityTypeId/:entityId`](/docs/entities/requisite-links/get) returns it by a pair of values: `2` — deal, then the deal identifier.

### 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
  }
}
```

A `0` in any of the four identifiers means there is no binding. The `requisiteId` and `bankDetailId` fields describe the client side, `mcRequisiteId` and `mcBankDetailId` describe your own company. Both sets of identifiers arrive in one call, and the requisites themselves are read in the steps below — each by its own identifier.

If the deal has no link, the call returns `404`. Branch on the response status rather than on the code: this `404` has two codes — `ENTITY_NOT_FOUND` when Bitrix24 returned an error, and `NOT_FOUND` when it returned an empty result.

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

This is an expected state, not a failure: no requisite has been selected for the deal yet. In that case take the company requisites from step 2 and pick the first one.

## Step 2. Company requisite

When the link carries a `requisiteId`, the requisite is read by identifier — [`GET /v1/requisites/:id`](/docs/entities/requisites/get). When there is no link, the company requisites are selected by [`POST /v1/requisites/search`](/docs/entities/requisites/search) with a pair of conditions: `entityTypeId` equals `4` — company, `entityId` — the company identifier from step 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": "Main requisite",
      "presetId": 1,
      "rqCompanyName": "Acme",
      "rqCompanyFullName": null,
      "rqVatId": "GB123456789",
      "rqInn": null,
      "rqKpp": null,
      "rqOgrn": null,
      "rqDirector": null
    }
  ],
  "meta": { "total": 1, "hasMore": false, "durationMs": 360 }
}
```

The set of filled fields is defined by the requisite preset — `presetId`. Which presets exist depends on the account country: an international Bitrix24 account has two built-in ones, `Company` and `Person`. The `Company` preset carries the company name and the VAT ID, the `Person` preset carries identity-document fields, and any field a preset does not define arrives empty. The field set of a specific preset is returned by [`GET /v1/requisite-presets/:presetId/fields`](/docs/entities/requisite-presets/preset-fields/list).

## Step 3. Bank account

A bank account belongs to a requisite, not to a company: in the filter, `entityId` is the requisite identifier from step 2. A bank account always has an owner of a single type, so the pair of values that an address filter needs is not required here. Accounts are selected by [`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": "Main account",
      "rqBankName": "First Bank",
      "rqBik": "123456789",
      "rqAccNum": "12345678901234567890",
      "rqCorAccNum": "09876543210987654321"
    }
  ],
  "meta": { "total": 1, "hasMore": false, "durationMs": 583 }
}
```

There may be no bank account at all — then `data` comes back as an empty array and `meta.total` as zero. That is enough for a certificate or a power of attorney, but not for an invoice, so check the array before substituting the values into the template.

## Step 4. Legal address

The requisite's addresses are returned by [`GET /v1/addresses`](/docs/entities/addresses/list) with a filter: `entityTypeId` equals `8` — requisite, `entityId` — the requisite identifier.

### 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": "123 Main St, floor 1",
      "address2": "Suite 2",
      "city": "Springfield",
      "postalCode": "62701",
      "region": null,
      "province": "Illinois",
      "country": "United States",
      "countryCode": null,
      "locAddrId": 465,
      "anchorTypeId": 4,
      "anchorId": 42
    }
  ],
  "meta": { "total": 1, "hasMore": false }
}
```

The address type is set by `typeId`. For a document take the legal one — `6` — and when it is missing, the actual one — `1`. Which codes are available depends on the account country, so pick the type from what actually arrived in the response. The full code reference is in [Get address](/docs/entities/addresses/get).

Assemble the string for the template from the response fields in the order you need: `postalCode`, `country`, `province`, `city`, `address1`, `address2`. Unfilled fields arrive as `null`, so drop empty values and duplicates before joining: `city` and `province` sometimes hold the same value, and without deduplication the city would appear in the string twice.

## Step 5. Document from a template

[`POST /v1/documents`](/docs/entities/documents/create) builds the file from a template. The template identifier comes from [`GET /v1/doc-templates`](/docs/entities/doc-templates), the collected values are passed in `values`, and `value` is your external identifier of the source object.

### 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": "Acme",
      "CompanyInn": "1234567890",
      "CompanyAddress": "62701, United States, Illinois, Springfield, 123 Main St, floor 1, Suite 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": "Supply contract A003//08/2026",
    "number": "A003//08/2026",
    "templateId": 237,
    "provider": "Bitrix\\DocumentGenerator\\DataProvider\\Rest",
    "value": "DEAL-5289",
    "values": {
      "productsTableVariant": "",
      "_creationMethod": "rest",
      "CompanyName": "Acme",
      "CompanyInn": "1234567890",
      "CompanyAddress": "62701, United States, Illinois, Springfield, 123 Main St, floor 1, Suite 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=..."
  }
}
```

Links with the `Machine` suffix are meant for an application and work without an employee session. The other links open in the browser of the signed-in user. `<portal>` is the Bitrix24 account domain.

## Limitations

**The link picks the requisite, not the record order.** A company can have several requisites — an old one and the current one, a head office and a branch. The first one in the output is not necessarily the right one, so the deal link in step 1 comes before the search by company and overrides its result.

**The PDF link does not appear immediately.** The creation response carries `downloadUrl`, while `pdfUrl` and `imageUrl` arrive after the file has been converted. You get them with a repeat call to [`GET /v1/documents/:id`](/docs/entities/documents/get). The same response carries `isTransformationError` — the flag that marks a failed conversion.

**A repeat run creates a new document.** The creation endpoint has no separate marker for "a document for this deal has already been built". The script below checks for a document by its own `value` through [`GET /v1/documents`](/docs/entities/documents/list) with a filter and does not build a second one. The check protects against a repeat run, but not against two simultaneous ones: both copies see an empty output and each creates a document.

**Refusals on document creation.** An unknown `templateId` gives `404` with the `ENTITY_NOT_FOUND` code and a message that the template was not found. If the data provider did not parse `value`, a `422` with the `BITRIX_ERROR` code arrives. The script stops on both and prints the message returned by Bitrix24.

**The template defines the label names.** The keys in `values` are the labels of a specific document template, not the requisite field names. Before the first run, match the label set against the template named in `templateId`.

**The preset decides which requisite fields carry data.** On an international Bitrix24 account the built-in `Company` preset defines the company name, the VAT ID (`rqVatId`), the address, the signature and the stamp. Fields belonging to another country's preset stay in the entity schema and can be requested, but come back empty. Read the actual set for your account with [`GET /v1/requisite-presets/:presetId/fields`](/docs/entities/requisite-presets/preset-fields/list) and keep in `values` only what the preset fills.

**Account queue.** A Bitrix24 account runs a limited number of API requests at a time. A chain of calls per deal — one per step plus the check for an already created document — in a loop over a large list receives `429` with the `QUEUE_OVERFLOW` or `QUEUE_TIMEOUT` code. The recommended pause arrives in the `Retry-After` header and is duplicated in `error.retryAfter`. The full list of codes is in [Errors](/docs/errors).

## Full code

Run: `VIBE_API_KEY=<key> DEAL_ID=<deal id> TEMPLATE_ID=<template id> node deal-document.js`

```javascript
// deal-document.js — collects the client requisites for a deal and builds the document
const VIBE_URL = process.env.VIBE_URL ?? 'https://vibecode.bitrix24.com'
const VIBE_API_KEY = process.env.VIBE_API_KEY
if (!VIBE_API_KEY) throw new Error('The VIBE_API_KEY environment variable is not set')

const DEAL_ID = Number(process.env.DEAL_ID)
const TEMPLATE_ID = Number(process.env.TEMPLATE_ID)
if (!DEAL_ID || !TEMPLATE_ID) throw new Error('Set DEAL_ID and TEMPLATE_ID')

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

// A 429 refusal means the request never reached Bitrix24 — repeating it is safe.
// How long to wait is stated by the Retry-After header, and in its absence by error.retryAfter.
// A random fraction of a second is added to the pause so that parallel copies of the script
// do not retry at the same moment.
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)
      // The spread is proportional to the pause so that parallel copies of the script do not
      // retry at the same moment. The 300-second ceiling is the maximum Bitrix24 reports.
      const wait = Math.min(base, 300) * (0.75 + Math.random() * 0.5)
      console.warn(`Account queue is busy, retrying in ${Math.round(wait)} s`)
      await new Promise(resolve => setTimeout(resolve, wait * 1000))
      continue
    }

    if (!body?.success) throw new Error(body?.error?.message ?? `request rejected (${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(`deal ${dealId} has no company filled in`)

  // The link knows which of the company requisites is selected for this deal.
  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(`  requisite ${link.requisiteId} from the link was not found — taking the company requisites`)
  }

  // There is no link — take the company requisites and pick the first one.
  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(`company ${deal.companyId} has no requisites`)
  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(`  account ${preferredId} from the link was not found — using ${list[0].id} instead`)
  return chosen ?? list[0]
}

// The legal address is typeId 6; the fallback when it is missing is the actual one, 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() {
  // The value is stable for a deal, so the check goes FIRST: for an already
  // processed deal that is one call to Bitrix24 instead of five.
  const value = `DEAL-${DEAL_ID}`
  const existing = await findExistingDocument(value)
  if (existing) {
    console.log(`A document for the deal has already been built: ${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 ?? '',
    CompanyVatId: requisite.rqVatId ?? '',
    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 ${deal.id}: requisite ${requisite.id}, account ${account?.id ?? 'not set up'}`)
  // For a certificate or a power of attorney empty account fields are enough. For an invoice
  // they are not: if you build a payment document, exit here instead of warning.
  if (!account) console.warn('  bank account not found — the account fields will stay empty')
  if (!address) console.warn('  address not found — the address field will stay empty')

  const document = await apiCall(`${VIBE_URL}/v1/documents`, {
    method: 'POST',
    body: JSON.stringify({ templateId: TEMPLATE_ID, providerClassName: PROVIDER, value, values }),
  })
  console.log(`Document ${document.id} created: ${document.title}`)
  // The downloadUrlMachine link from the response carries an access token — keep it out of the log.
  console.log(`Link for the user: ${document.downloadUrl}`)
}

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

## See also

- [Requisites](/docs/entities/requisites)
- [Bank details](/docs/entities/bank-details)
- [Addresses](/docs/entities/addresses)
- [Requisite links](/docs/entities/requisite-links)
- [Documents](/docs/entities/documents)
- [Document templates](/docs/entities/doc-templates)
