For AI agents: markdown of this page — /docs-content-en/recipes/document-requisites.md documentation index — /llms.txt
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
crmanddocumentgeneratorscopes - 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
- From the deal, find the company and which of its requisites is selected for this deal.
- Read the requisite: the company name and the VAT ID.
- Take the requisite's bank account: current account, BIC, correspondent account.
- Pick the legal one among the requisite's addresses.
- 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 returns the deal's company in the companyId field.
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": "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 returns it by a pair of values: 2 — deal, then the deal identifier.
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
}
}
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.
{ "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. When there is no link, the company requisites are selected by POST /v1/requisites/search with a pair of conditions: entityTypeId equals 4 — company, entityId — the company identifier from step 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": "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.
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.
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": "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 with a filter: entityTypeId equals 8 — requisite, entityId — the requisite identifier.
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": "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.
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 builds the file from a template. The template identifier comes from GET /v1/doc-templates, the collected values are passed in values, and value is your external identifier of the source object.
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": "Acme",
"CompanyInn": "1234567890",
"CompanyAddress": "62701, United States, Illinois, Springfield, 123 Main St, floor 1, Suite 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": "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. 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 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 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.
Full code
Run: VIBE_API_KEY=<key> DEAL_ID=<deal id> TEMPLATE_ID=<template id> node deal-document.js
// 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
})