# 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 as `null`. 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. The owner of a bank account is always of the same type, so the pair of values that addresses require is not needed 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 an acceptance 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 -g -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"
  }
}
```

`downloadUrl` is a path relative to the Bitrix24 account address. A user signed in to Bitrix24 opens the file in the browser via this path. A document built from a REST template has no `downloadUrlMachine` field, and its file cannot be downloaded with an API key.

## 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 results 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 result 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 as `null`. 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 comes from the Retry-After header, or from error.retryAfter when it is absent.
// 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 an acceptance 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}`)
  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)
