How to Add a Template and Create a Document Based on It
Scope:
crmWho can execute the methods: to complete the entire scenario, both permissions are required — to modify document generator templates and to modify document generator documents
- crm.documentgenerator.numerator.add and crm.documentgenerator.template.add — a user with permission to modify document generator templates
- crm.documentgenerator.document.add — a user with permission to modify document generator documents
- crm.documentgenerator.document.get — a user with permission to view document generator documents
Choose a tool for developing with an AI agent:
- use Alaio Vibecode to build an app for Bitrix24 from a task description without knowing any programming language. The agent writes the code and deploys the app to a server, with no manual hosting setup
- use the MCP server to develop a REST API integration in your own project. The agent refers to the official REST documentation
The document generator builds a printable form from a .docx template and the data of a CRM object. Through REST, all three steps — creating a numerator, uploading a template, and generating a document — can be performed by a script.
The numerator defines how document numbers are counted, so it is created first: without it, the template is not retained. The template holds the file and the list of objects it is available for. The document is created from a ready template for a single object, so it comes last.
As a result of the scenario, a document with a number from the numerator appears in the deal card, and the method returns a link to download it.
The scenario consists of three steps. Steps 1 and 2 configure the generator and are performed once, step 3 is repeated for every new deal with the templateId that is already available.
- Create the numerator using the crm.documentgenerator.numerator.add method and retrieve its
id - Upload the template using the crm.documentgenerator.template.add method, passing the numerator
idand the file in Base64, and retrieve the templateid - Generate the document using the crm.documentgenerator.document.add method, passing the template
idand the deal identifier
Before You Start
-
The webhook is created on behalf of a user who has permissions to modify document generator templates and documents. Verifying the result additionally requires the permission to view documents
-
The
crmscope is selected in the webhook permissions -
The webhook URL grants full access within its scope. Retain the URL in an environment variable and never publish it in open code
-
A
.docxtemplate file with document generator fields is located on the disk next to the script -
Bitrix24 contains the deal the document is created for, and you know its
id. The deal can be found using the crm.item.list method withentityTypeId:2 -
The document generator module is available in Bitrix24, and the plan allows creating documents
Fields in the template file are written in curly braces, for example {DocumentNumber} — the document number, {DocumentCreateTime} — the generation date, {TotalSum} — the total amount. A file without fields is uploaded successfully, but the document built from it comes out empty.
The examples use three values. Replace them with your own.
-
templatePath— the path to the template file,template.docxin the example -
templateName— the template name,Demonstration product implementationin the example -
dealId— the deal identifier,8287in the example
1. Create the Numerator
Use the crm.documentgenerator.numerator.add method. The method accepts a fields object with the following parameters:
-
name— the numerator name, a required parameter. SpecifyNumerator from REST -
template— the number template, a required parameter. Specify{NUMBER}— the generator replaces this variable with the sequential number of the document. Variables can be combined in the number template, for example{DAY}— the current day,{CLIENT_ID}— the client identifier,{RANDOM}— a random number
How to Use Examples in Documentation
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
const templatePath = 'template.docx'; // path to the template file
const templateName = 'Demonstration product implementation'; // template name
const dealId = 8287; // deal identifier
const resNum = await $b24.actions.v2.call.make({
method: 'crm.documentgenerator.numerator.add',
params: {
fields: {
name: 'Numerator from REST', // Numerator name
template: '{NUMBER}' // Document number template
}
},
requestId: 'numerator-add'
});
const numeratorId = resNum.getData().result.numerator.id;
from b24pysdk import BitrixWebhook, Client
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
template_path = "template.docx" # path to the template file
template_name = "Demonstration product implementation" # template name
deal_id = 8287 # deal identifier
numerator = client.crm.documentgenerator.numerator.add(
fields={
"name": "Numerator from REST", # Numerator name
"template": "{NUMBER}", # Document number template
},
).response.result["numerator"]
numerator_id = numerator["id"]
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Psr\Log\NullLogger;
$sb = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
->initFromWebhook('https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/');
$templatePath = __DIR__ . '/template.docx'; // path to the template file
$templateName = 'Demonstration product implementation'; // template name
$dealId = 8287; // deal identifier
$resNum = $sb->getCRMScope()->documentgeneratorNumerator()->add(
[
'name' => 'Numerator from REST', // Numerator name
'template' => '{NUMBER}', // Document number template
]
);
$numeratorId = $resNum->getId();
In the response, the method returns a numerator object. Retain the id — it has to be passed to step 2. In the example, id: 1095.
{
"result": {
"numerator": {
"name": "Numerator from REST",
"template": "{NUMBER}",
"id": 1095,
"code": null,
"settings": {
"Bitrix_Main_Numerator_Generator_SequentNumberGenerator": {
"start": 1,
"step": 1,
"length": 0,
"padString": "0",
"periodicBy": null,
"timezone": null,
"isDirectNumeration": false
}
}
}
}
}
2. Upload the Document Template
Use the crm.documentgenerator.template.add method.
The content of the template file has to be converted to the Base64 format.
The method accepts a fields object with the following parameters:
-
name— the template name, a required parameter. Pass thetemplateNamevalue -
numeratorId— the numerator identifier, a required parameter. Take theidfrom the step 1 response,1095in the example -
region— the template region, a required parameter. It affects localization, such as the currency and date format. Specifyde. The list of regions available in your Bitrix24 is returned by the documentgenerator.region.list method — it requires the separatedocumentgeneratorscope -
entityTypeId— an array of CRM object type identifiers the template is available for, a required parameter. Specify2— a deal. The full list of types is returned by the crm.enum.ownertype method -
file— the template file in the format["file_name.docx", "base64-content"], a required parameter. The first element of the array sets the name the file is retained under in Bitrix24, the second one holds the file content in Base64. Take the name fromtemplatePathso that it does not diverge from the name of the uploaded file -
users— an array of access permission codes, an optional parameter. It defines who sees the template and can use it. SpecifyUA— the access code for all authorized users. To narrow the access down, pass the codes of specific users or groups instead ofUA
const fileContent = (await readFile(templatePath)).toString('base64');
const resTemplate = await $b24.actions.v2.call.make({
method: 'crm.documentgenerator.template.add',
params: {
fields: {
name: templateName, // Template name
numeratorId: numeratorId, // Numerator identifier from step 1
region: 'de', // Template region
users: ['UA'], // Access permissions: all authorized users
entityTypeId: ['2'], // 2 — deal
file: [basename(templatePath), fileContent] // File name and content in Base64
}
},
requestId: 'template-add'
});
const templateId = resTemplate.getData().result.template.id;
import base64
from pathlib import Path
with open(template_path, "rb") as file:
file_content = base64.b64encode(file.read()).decode("ascii")
template = client.crm.documentgenerator.template.add(
fields={
"name": template_name, # Template name
"numeratorId": numerator_id, # Numerator identifier from step 1
"region": "de", # Template region
"users": ["UA"], # Access permissions: all authorized users
"entityTypeId": ["2"], # 2 — deal
"file": [Path(template_path).name, file_content], # File name and content in Base64
},
).response.result["template"]
template_id = int(template["id"])
$fileContent = base64_encode(file_get_contents($templatePath));
$resTemplate = $sb->getCRMScope()->documentgeneratorTemplate()->add(
[
'name' => $templateName, // Template name
'numeratorId' => $numeratorId, // Numerator identifier from step 1
'region' => 'de', // Template region
'users' => ['UA'], // Access permissions: all authorized users
'entityTypeId' => ['2'], // 2 — deal
'file' => [basename($templatePath), $fileContent] // File name and content in Base64
]
);
$templateId = $resTemplate->getId();
In the response, the method returns a template object. Retain the id — it has to be passed to step 3. In the example, id: 249.
{
"result": {
"template": {
"id": "249",
"name": "Demonstration product implementation",
"region": "de",
"code": null,
"download": "https://your-domain.bitrix24.com/bitrix/services/main/ajax.php?action=crm.documentgenerator.template.download&SITE_ID=s1&id=249",
"active": "Y",
"moduleId": "crm",
"numeratorId": "1095",
"withStamps": "N",
"users": {
"UA": "UA"
},
"isDeleted": "N",
"sort": "500",
"createTime": "2026-08-19T14:55:23+03:00",
"updateTime": "2026-08-19T14:55:23+03:00",
"entityTypeId": [
"2"
],
"downloadMachine": "https://your-domain.bitrix24.com/rest/crm.documentgenerator.template.download.json?..."
}
}
}
The numerator identifier came back in the numeratorId field as a string — this is the same numerator that was created in step 1. The value types in the response differ:
-
the template's own
idalso arrives as a string: it can be passed totemplateIdin step 3 as is or converted to a number -
the template's
entityTypeIdarray holds strings, while the document in step 3 accepts a number
The names of the fields that the template substitutes into the document are returned by the crm.documentgenerator.template.getfields method for the id from this response. Compare that list with the fields in the .docx file if the document comes out empty.
3. Generate the Document
Build the document from the template and the deal data using the crm.documentgenerator.document.add method with the following parameters:
-
templateId— the template identifier, a required parameter. Take theidfrom the step 2 response,249in the example -
entityTypeId— the CRM object type identifier, a required parameter. Specify2— a deal. The value has to be included in the template'sentityTypeIdarray, otherwise the document is not created -
entityId— the object identifier, a required parameter. Pass thedealIdvalue — the document is built from the data of this deal
The method does not check whether the object exists. If the entityId of a non-existent deal is passed, the document is still created: there is no error, but the deal fields in it remain empty and the numerator value is spent. Make sure the deal exists before the call.
const resDoc = await $b24.actions.v2.call.make({
method: 'crm.documentgenerator.document.add',
params: {
templateId: templateId, // Template identifier from step 2
entityTypeId: 2, // 2 — deal
entityId: dealId // Deal identifier
},
requestId: 'document-add'
});
const documentId = resDoc.getData().result.document.id;
document = client.crm.documentgenerator.document.add(
template_id=template_id, # Template identifier from step 2
entity_type_id=2, # 2 — deal
entity_id=deal_id, # Deal identifier
).response.result["document"]
document_id = document["id"]
$resDoc = $sb->getCRMScope()->documentgeneratorDocument()->add(
$templateId, // Template identifier from step 2
2, // 2 — deal
$dealId // Deal identifier
);
$documentId = $resDoc->getId();
In the response, the method returns a document object. The response is shortened, showing the fields that confirm the result.
{
"result": {
"document": {
"id": 1919,
"title": "Demonstration product implementation 1",
"number": "1",
"templateId": "249",
"entityTypeId": "2",
"entityId": 8287,
"createTime": "2026-08-19T14:55:42+03:00",
"createdBy": 1,
"publicUrl": null,
"downloadUrl": "https://your-domain.bitrix24.com/bitrix/services/main/ajax.php?action=crm.documentgenerator.document.download&SITE_ID=s1&id=1919",
"downloadUrlMachine": "https://your-domain.bitrix24.com/rest/crm.documentgenerator.document.download.json?...",
"products": {
"currencyId": "EUR",
"totalSum": "0.00",
"totalRows": 0
}
}
}
}
The title field is built from the template name and the number 1 issued by the numerator. The downloadUrl link opens the document in a browser, downloadUrlMachine returns the file when it is downloaded from an application. The publicUrl field is empty: the public link is enabled by the separate crm.documentgenerator.document.enablepublicurl method.
Verify the Result
Open the deal card in Bitrix24 — the new document appears in the deal document list under the name from the title field. From there it can be downloaded or sent to the client.
Through REST, the document is returned by the crm.documentgenerator.document.get method for the identifier from step 3.
const checkResult = await $b24.actions.v2.call.make({
method: 'crm.documentgenerator.document.get',
params: { id: documentId },
requestId: 'document-get'
});
console.dir(checkResult.getData().result.document);
check_result = client.crm.documentgenerator.document.get(
int(document_id),
).response.result["document"]
print(check_result)
$checkResult = $sb->getCRMScope()->documentgeneratorDocument()->get($documentId);
print_r($checkResult->document());
The scenario is complete if the response contains non-empty id and downloadUrl fields and the templateId matches the template identifier from step 2. The number field shows the number issued by the numerator: for the first document it is 1, for the next one 2.
Errors and Diagnostics
If the method returns an error, check the request data.
|
Error text |
Reason and action |
|
|
The |
|
|
The required fields of the |
|
|
The file content was not passed in |
|
|
The template file was not retained. Make sure that the whole |
|
|
The webhook user does not have permission to modify document generator templates |
|
|
The |
|
|
The |
|
|
The |
|
|
The document was not built from the template. A common reason is that the file is not in the |
|
The document is created, but the deal fields are empty |
This is not an error. Either a non-existent deal was passed in |
|
|
The webhook user does not have permission to modify document generator documents |
|
|
The document limit of the plan has been reached |
|
|
The document generator module is not available in Bitrix24 |
The steps run as a chain, so repeat only the failed step and the ones that follow it. If step 2 returned the error, the numerator is already created — take its id from the step 1 response instead of creating a new one. If step 3 returned the error, the template is already uploaded and steps 1 and 2 do not need to be repeated.
Key Considerations
-
Running all three steps repeatedly accumulates duplicate numerators and templates in the CRM settings. The existing ones are returned by the crm.documentgenerator.numerator.list and crm.documentgenerator.template.list methods, and the extra templates are deleted by crm.documentgenerator.template.delete
-
The numerator counts numbers with a continuous counter. Documents created from different templates with the same numerator continue the shared numbering
-
To build documents for invoices or smart processes, add their identifiers to the
entityTypeIdarray when uploading the template, or update the template using the crm.documentgenerator.template.update method
Code Example
The script sequentially creates the numerator, uploads the template, and builds the document for the deal. Each following call runs only after the previous one has succeeded.
import { readFile } from 'node:fs/promises'
import { basename } from 'node:path'
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
const templatePath = 'template.docx'; // path to the template file
const templateName = 'Demonstration product implementation'; // template name
const dealId = 8287; // deal identifier
async function createDocument() {
try {
const fileContent = (await readFile(templatePath)).toString('base64');
const resNum = await $b24.actions.v2.call.make({
method: 'crm.documentgenerator.numerator.add',
params: {
fields: { name: 'Numerator from REST', template: '{NUMBER}' }
},
requestId: 'numerator-add'
});
const numeratorId = resNum.getData().result.numerator.id;
const resTemplate = await $b24.actions.v2.call.make({
method: 'crm.documentgenerator.template.add',
params: {
fields: {
name: templateName,
numeratorId: numeratorId,
region: 'de',
users: ['UA'],
entityTypeId: ['2'],
file: [basename(templatePath), fileContent]
}
},
requestId: 'template-add'
});
const templateId = resTemplate.getData().result.template.id;
const resDoc = await $b24.actions.v2.call.make({
method: 'crm.documentgenerator.document.add',
params: {
templateId: templateId,
entityTypeId: 2,
entityId: dealId
},
requestId: 'document-add'
});
const document = resDoc.getData().result.document;
console.log('Document created:', document.title, document.downloadUrl);
} catch (error) {
console.error('Document not created:', error.message);
}
}
createDocument();
import base64
from pathlib import Path
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
template_path = "template.docx" # path to the template file
template_name = "Demonstration product implementation" # template name
deal_id = 8287 # deal identifier
try:
with open(template_path, "rb") as file:
file_content = base64.b64encode(file.read()).decode("ascii")
except OSError as error:
print(f"Template file not read: {error}")
else:
try:
numerator = client.crm.documentgenerator.numerator.add(
fields={
"name": "Numerator from REST",
"template": "{NUMBER}",
},
).response.result["numerator"]
template = client.crm.documentgenerator.template.add(
fields={
"name": template_name,
"numeratorId": numerator["id"],
"region": "de",
"users": ["UA"],
"entityTypeId": ["2"],
"file": [Path(template_path).name, file_content],
},
).response.result["template"]
document = client.crm.documentgenerator.document.add(
template_id=int(template["id"]),
entity_type_id=2,
entity_id=deal_id,
).response.result["document"]
except BitrixAPIError as error:
print(f"Document not created: {error}")
else:
print(f"Document created: {document['title']} {document['downloadUrl']}")
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Psr\Log\NullLogger;
$sb = (new ServiceBuilderFactory(new EventDispatcher(), new NullLogger()))
->initFromWebhook('https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/');
$templatePath = __DIR__ . '/template.docx'; // path to the template file
$templateName = 'Demonstration product implementation'; // template name
$dealId = 8287; // deal identifier
try {
if (!is_readable($templatePath)) {
throw new \RuntimeException('Template file not found: ' . $templatePath);
}
$fileContent = base64_encode(file_get_contents($templatePath));
$resNum = $sb->getCRMScope()->documentgeneratorNumerator()->add(
[
'name' => 'Numerator from REST',
'template' => '{NUMBER}',
]
);
$numeratorId = $resNum->getId();
$resTemplate = $sb->getCRMScope()->documentgeneratorTemplate()->add(
[
'name' => $templateName,
'numeratorId' => $numeratorId,
'region' => 'de',
'users' => ['UA'],
'entityTypeId' => ['2'],
'file' => [basename($templatePath), $fileContent]
]
);
$templateId = $resTemplate->getId();
$resDoc = $sb->getCRMScope()->documentgeneratorDocument()->add(
$templateId,
2,
$dealId
);
echo 'Document created: ' . $sb->getCRMScope()
->documentgeneratorDocument()
->get($resDoc->getId())
->document()
->title;
} catch (\Throwable $e) {
echo 'Document not created: ' . $e->getMessage();
}
Continue Learning
- Add a New Numerator crm.documentgenerator.numerator.add
- Get the List of Numerators crm.documentgenerator.numerator.list
- Add a New Template crm.documentgenerator.template.add
- Get a List of Document Templates crm.documentgenerator.template.list
- Get crm.documentgenerator.template.getfields Document Template Fields
- Get the List of Regions documentgenerator.region.list
- Create a New Document crm.documentgenerator.document.add
- Get Document crm.documentgenerator.document.get
- Enable or Disable Public Link for Document crm.documentgenerator.document.enablepublicurl
- How to Upload Files