Add a Contact with Requisites via Web Form

Scope: crm

Who can execute the methods: to complete the entire scenario, the strictest of the listed permissions is required — "Add|Import" for contacts

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

You can place a form on your website to collect client data and requisites. When a client fills out the form, the data is sent to a handler. The handler script creates objects in the CRM via the REST API.

As a result of the scenario, three linked objects appear in the CRM: a contact, its requisite, and the requisite address.

The setup consists of two stages.

  1. Prepare the fields and place the web form on the page. The set of form fields is taken from the crm.address.fields and crm.requisite.preset.list methods

  2. Create a handler file that sequentially calls the crm.contact.add, crm.requisite.add, and crm.address.add methods

The order of the calls is set by the links between the objects: the requisite is created for an existing contact, and the address for an existing requisite.

Before You Start

  • At least one requisite template is configured in Bitrix24. If there are no templates, the crm.requisite.preset.list method returns an empty list and there is nothing to build the form from

  • The webhook is created on behalf of a user who has the "Add|Import" permission for contacts

  • You have a server that serves the page with the form and accepts the form data using the POST method. In the examples, this is Express for JS, a PHP script, Flask for Python, and net/http for Go

  • The webhook URL is stored in the environment, not in the page code. The page with the form is public, and the secret must not end up in it

1. Create the Web Form

To generate the fields, we use two methods:

  • crm.address.fields — retrieves a list of address fields. Save the result in the $arAddressFields array

  • crm.requisite.preset.list — retrieves a list of requisite templates based on the ID and NAME fields. Save the result in the $arPresets array

How to Use Examples in Documentation

const arAddressFields = (await $b24.actions.v2.call.make({
            method: 'crm.address.fields', params: {}, requestId: 'address-fields'
        })).getData().result
        const arPresets = (await $b24.actions.v2.call.make({
            method: 'crm.requisite.preset.list', params: { select: ['ID', 'NAME'] }, requestId: 'preset-list'
        })).getData().result
        
ar_address_fields = client.crm.address.fields().result
        ar_presets = client.crm.requisite.preset.list(select=["ID", "NAME"]).result
        
$arAddressFields = $sb->getCRMScope()->address()->fields()->getFieldsDescription();
        $arPresets = $sb->getCRMScope()->requisitePreset()->list(
            order: [], filter: [], select: ["ID", "NAME"]
        )->getRequisitePresets();
        
res, err := core.Call(ctx, "crm.address.fields", nil, b24.WithIdempotent())
        if err != nil {
        	return fmt.Errorf("crm.address.fields: %w", err)
        }
        
        // The response is not a list but an object "field name -> description", hence a map.
        var addressFields map[string]struct {
        	Type       string `json:"type"`
        	Title      string `json:"title"`
        	IsReadOnly bool   `json:"isReadOnly"`
        }
        if err := json.Unmarshal(res.Result, &addressFields); err != nil {
        	return fmt.Errorf("parsing address fields: %w", err)
        }
        
        res, err = core.Call(ctx, "crm.requisite.preset.list", b24.Params{
        	"select": []string{"ID", "NAME"},
        }, b24.WithIdempotent())
        if err != nil {
        	return fmt.Errorf("crm.requisite.preset.list: %w", err)
        }
        
        // Here the identifier arrives as a STRING ("1"), whereas crm.enum.* returns
        // numbers. b24.ID parses both notations.
        var presets []struct {
        	ID   b24.ID `json:"ID"`
        	Name string `json:"NAME"`
        }
        if err := json.Unmarshal(res.Result, &presets); err != nil {
        	return fmt.Errorf("parsing requisite templates: %w", err)
        }
        if len(presets) == 0 {
        	return fmt.Errorf("there are no requisite templates in Bitrix24")
        }
        

The crm.requisite.preset.list method returns an array of objects, not identifier-name pairs. For the drop-down list, iterate over this array and take ID and NAME from each object.

{
            "result": [
                { "ID": "1", "NAME": "Organization" },
                { "ID": "3", "NAME": "Sole Proprietorship" },
                { "ID": "5", "NAME": "Individual" }
            ]
        }
        

The crm.address.fields method returns an object where the key is the field code and the value is its description with the isRequired mandatory flag and the title name.

{
            "result": {
                "TYPE_ID": {
                    "type": "integer",
                    "isRequired": true,
                    "isReadOnly": false,
                    "isImmutable": true,
                    "isMultiple": false,
                    "isDynamic": false,
                    "title": "TYPE_ID"
                },
                "ADDRESS_1": {
                    "type": "string",
                    "isRequired": false,
                    "isReadOnly": false,
                    "isImmutable": false,
                    "isMultiple": false,
                    "isDynamic": false,
                    "title": "Street, house, building, structure"
                },
                "CITY": {
                    "type": "string",
                    "isRequired": false,
                    "isReadOnly": false,
                    "isImmutable": false,
                    "isMultiple": false,
                    "isDynamic": false,
                    "title": "City"
                }
            }
        }
        

Remove unnecessary address fields from the $arAddressFields array so they are not displayed in the form. Three of them — TYPE_ID, ENTITY_TYPE_ID, and ENTITY_ID — are mandatory system fields that the client does not fill in; the handler substitutes them itself.

for (const f of ['TYPE_ID', 'ENTITY_TYPE_ID', 'ENTITY_ID', 'COUNTRY_CODE', 'ANCHOR_TYPE_ID', 'ANCHOR_ID']) {
            delete arAddressFields[f]
        }
        
for f in ("TYPE_ID", "ENTITY_TYPE_ID", "ENTITY_ID", "COUNTRY_CODE", "ANCHOR_TYPE_ID", "ANCHOR_ID"):
            ar_address_fields.pop(f, None)
        
foreach (['TYPE_ID', 'ENTITY_TYPE_ID', 'ENTITY_ID', 'COUNTRY_CODE', 'ANCHOR_TYPE_ID', 'ANCHOR_ID'] as $field) {
            unset($arAddressFields[$field]);
        }
        
// The form takes only the string fields that are available for writing: TYPE_ID,
        // ENTITY_ID, and ENTITY_TYPE_ID also arrive in this response, but the handler
        // substitutes them itself. Map keys in Go are unordered — sort them, otherwise
        // the form fields will jump around from run to run.
        var addressNames []string
        for name, f := range addressFields {
        	if f.Type == "string" && !f.IsReadOnly {
        		addressNames = append(addressNames, name)
        	}
        }
        sort.Strings(addressNames)
        

Create an HTML form with the following fields:

  • REQ_TYPE — a drop-down list with requisite templates from the $arPresets array. Mandatory field

  • NAME — contact first name. Mandatory field

  • LAST_NAME — contact last name

  • PHONE — phone number

  • ADDRESS — address fields are created dynamically from $arAddressFields. If the field is mandatory, add the attribute required

The form collects the data and sends it to the handler using the POST method. The form markup is shown below — the requisite drop-down list and the address fields are populated from the retrieved data.

// assemble the form string from the received data and insert it into the server response
        const options = arPresets.map(p => `<option value="${p.ID}">${p.NAME}</option>`).join('')
        const addressInputs = Object.entries(arAddressFields).map(([key, field]) =>
            `<input type="text" name="ADDRESS[${key}]" placeholder="${field.title}" ${field.isRequired ? 'required' : ''}>`
        ).join('')
        
        const formHtml = `
            <form id="form_to_crm">
                <select name="REQ_TYPE" required>
                    <option value="" disabled selected>Select a requisite type</option>
                    ${options}
                </select>
                <input type="text" name="NAME" placeholder="First name" required>
                <input type="text" name="LAST_NAME" placeholder="Last name">
                <input type="text" name="PHONE" placeholder="Phone">
                ${addressInputs}
                <input type="submit" value="Submit">
            </form>`
        
# assemble the form string from the received data and insert it into the server response
        from markupsafe import escape
        
        options = "".join(
            f'<option value="{escape(preset["ID"])}">{escape(preset["NAME"])}</option>'
            for preset in ar_presets
        )
        address_inputs = "".join(
            f'<input type="text" name="ADDRESS[{escape(key)}]" '
            f'placeholder="{escape(field["title"])}" '
            f'{"required" if field["isRequired"] else ""}>'
            for key, field in ar_address_fields.items()
        )
        
        form_html = f"""
            <form id="form_to_crm">
                <select name="REQ_TYPE" required>
                    <option value="" disabled selected>Select a requisite type</option>
                    {options}
                </select>
                <input type="text" name="NAME" placeholder="First name" required>
                <input type="text" name="LAST_NAME" placeholder="Last name">
                <input type="text" name="PHONE" placeholder="Phone">
                {address_inputs}
                <input type="submit" value="Submit">
            </form>"""
        
<form id="form_to_crm">
            <select name="REQ_TYPE" required>
                <option value="" disabled selected>Select a requisite type</option>
                <?php foreach($arPresets as $preset):?>
                    <option value="<?=$preset->ID?>"><?=$preset->NAME?></option>
                <?php endforeach;?>
            </select>
            <input type="text" name="NAME" placeholder="First name" required>
            <input type="text" name="LAST_NAME" placeholder="Last name">
            <input type="text" name="PHONE" placeholder="Phone">
            <?php if(is_array($arAddressFields)):?>
                <?php foreach($arAddressFields as $key=>$arField):?>
                    <input type="text" name="ADDRESS[<?=$key?>]" placeholder="<?=$arField['title']?>" <?=($arField['isRequired'])?'required':'';?>>
                <?php endforeach;?>
            <?php endif;?>
            <input type="submit" value="Submit">
        </form>
        
var form strings.Builder
        form.WriteString(`<!doctype html>
        <meta charset="utf-8">
        <title>Request</title>
        <form method="post" action="/form">
        <p><label>Requisite type*<br><select name="REQ_TYPE" required>`)
        for _, p := range presets {
        	fmt.Fprintf(&form, `<option value="%d">%s</option>`, p.ID, html.EscapeString(p.Name))
        }
        form.WriteString(`</select></label></p>
        <p><label>First name*<br><input name="NAME" required></label></p>
        <p><label>Last name<br><input name="LAST_NAME"></label></p>
        <p><label>Phone<br><input name="PHONE" type="tel"></label></p>`)
        // The address fields are created dynamically: their set is defined by Bitrix24,
        // not by the code. Names such as ADDRESS[CITY] — the handler parses them back.
        for _, name := range addressNames {
        	fmt.Fprintf(&form, "<p><label>%s<br><input name=\"ADDRESS[%s]\"></label></p>\n",
        		html.EscapeString(addressFields[name].Title), name)
        }
        form.WriteString(`<p><button type="submit">Submit</button></p>
        </form>`)
        page := form.String()
        

Full Code Example of the Form Page

import express from 'express'
        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 app = express()
        
        // Form page: retrieve data from Bitrix24 and render HTML
        app.get('/', async (req, res) => {
            const arAddressFields = (await $b24.actions.v2.call.make({
                method: 'crm.address.fields', params: {}, requestId: 'address-fields'
            })).getData().result
            const arPresets = (await $b24.actions.v2.call.make({
                method: 'crm.requisite.preset.list', params: { select: ['ID', 'NAME'] }, requestId: 'preset-list'
            })).getData().result
        
            if (!arPresets.length) {
                res.send('<p>No requisite types available.</p>')
                return
            }
        
            // Remove system and unused address fields
            for (const f of ['TYPE_ID', 'ENTITY_TYPE_ID', 'ENTITY_ID', 'COUNTRY_CODE', 'ANCHOR_TYPE_ID', 'ANCHOR_ID']) {
                delete arAddressFields[f]
            }
        
            // Assemble the requisite drop-down list and the address fields
            const options = arPresets.map(p => `<option value="${p.ID}">${p.NAME}</option>`).join('')
            const addressInputs = Object.entries(arAddressFields).map(([key, field]) =>
                `<input type="text" name="ADDRESS[${key}]" placeholder="${field.title}" ${field.isRequired ? 'required' : ''}>`
            ).join('')
        
            res.send(`
                <form id="form_to_crm">
                    <select name="REQ_TYPE" required>
                        <option value="" disabled selected>Select a requisite type</option>
                        ${options}
                    </select>
                    <input type="text" name="NAME" placeholder="First name" required>
                    <input type="text" name="LAST_NAME" placeholder="Last name">
                    <input type="text" name="PHONE" placeholder="Phone">
                    ${addressInputs}
                    <input type="submit" value="Submit">
                </form>
                <script>
                    document.getElementById('form_to_crm').addEventListener('submit', async (el) => {
                        el.preventDefault()
                        const formData = Object.fromEntries(new FormData(el.currentTarget).entries())
                        const response = await fetch('/form', {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify(formData),
                        })
                        alert((await response.json()).message)
                    })
                <\/script>
            `)
        })
        
        app.listen(3000)
        
# pip install b24pysdk flask
        import os
        
        from flask import Flask
        from markupsafe import escape
        from b24pysdk import BitrixWebhook, Client
        
        app = Flask(__name__)
        
        client = Client(BitrixWebhook(
            domain=os.environ["B24_DOMAIN"],  # your-domain.bitrix24.com
            webhook_token=os.environ["B24_TOKEN"],  # user_id/token only, without https://
        ))
        
        # Page template: %(options)s and %(address_inputs)s are substituted from Python
        PAGE = """
            <form id="form_to_crm">
                <select name="REQ_TYPE" required>
                    <option value="" disabled selected>Select a requisite type</option>
                    %(options)s
                </select>
                <input type="text" name="NAME" placeholder="First name" required>
                <input type="text" name="LAST_NAME" placeholder="Last name">
                <input type="text" name="PHONE" placeholder="Phone">
                %(address_inputs)s
                <input type="submit" value="Submit">
            </form>
            <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
            <script>
            $(document).ready(function() {
                $('#form_to_crm').on('submit', function(el) {
                    el.preventDefault();
                    $.ajax({
                        method: 'POST', dataType: 'json', url: '/form',
                        data: $(this).serialize(),
                        success: function(data) { alert(data.message); }
                    });
                });
            });
            </script>
        """
        
        EMPTY_PAGE = "<p>No requisite types available.</p>"
        
        
        @app.route("/")
        def form_page():
            # Retrieve the list of address fields and requisite templates
            ar_address_fields = client.crm.address.fields().result
            ar_presets = client.crm.requisite.preset.list(select=["ID", "NAME"]).result
        
            if not ar_presets:
                return EMPTY_PAGE
        
            # Remove system and unused address fields
            for f in ("TYPE_ID", "ENTITY_TYPE_ID", "ENTITY_ID", "COUNTRY_CODE", "ANCHOR_TYPE_ID", "ANCHOR_ID"):
                ar_address_fields.pop(f, None)
        
            # Assemble the requisite drop-down list and the address fields
            options = "".join(
                f'<option value="{escape(preset["ID"])}">{escape(preset["NAME"])}</option>'
                for preset in ar_presets
            )
            address_inputs = "".join(
                f'<input type="text" name="ADDRESS[{escape(key)}]" '
                f'placeholder="{escape(field["title"])}" '
                f'{"required" if field["isRequired"] else ""}>'
                for key, field in ar_address_fields.items()
            )
        
            return PAGE % {"options": options, "address_inputs": address_inputs}
        
<?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(getenv('B24_HOOK'));
        // B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
        
        // Retrieve the list of address fields and requisite templates
        $arAddressFields = $sb->getCRMScope()->address()->fields()->getFieldsDescription();
        $arPresets = $sb->getCRMScope()->requisitePreset()->list(
            order: [], filter: [], select: ["ID", "NAME"]
        )->getRequisitePresets();
        
        if (!empty($arPresets)):
            // Remove system and unused address fields
            $excludeFields = ['TYPE_ID', 'ENTITY_TYPE_ID', 'ENTITY_ID', 'COUNTRY_CODE', 'ANCHOR_TYPE_ID', 'ANCHOR_ID'];
            foreach ($excludeFields as $field) {
                unset($arAddressFields[$field]);
            }
        ?>
            <form id="form_to_crm">
                <select name="REQ_TYPE" required>
                    <option value="" disabled selected>Select a requisite type</option>
                    <?php foreach ($arPresets as $preset): ?>
                        <option value="<?=$preset->ID?>"><?=$preset->NAME?></option>
                    <?php endforeach; ?>
                </select>
                <input type="text" name="NAME" placeholder="First name" required>
                <input type="text" name="LAST_NAME" placeholder="Last name">
                <input type="text" name="PHONE" placeholder="Phone">
                <?php foreach ($arAddressFields as $key => $arField): ?>
                    <input type="text" name="ADDRESS[<?=$key?>]"
                           placeholder="<?=$arField['title']?>"
                           <?=$arField['isRequired'] ? 'required' : ''?>>
                <?php endforeach; ?>
                <input type="submit" value="Submit">
            </form>
        <?php else: ?>
            <p>No requisite types available.</p>
        <?php endif; ?>
        
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
        <script>
        $(document).ready(function() {
            $('#form_to_crm').on('submit', function(el) {
                el.preventDefault();
                $.ajax({
                    method: 'POST',
                    dataType: 'json',
                    url: 'form.php', // handler file from step 2
                    data: $(this).serialize(),
                    success: function(data) {
                        alert(data.message);
                    }
                });
            });
        });
        </script>
        
// The full code of the page and the handler is in the example below, in step 2:
        // the same program assembles and serves the page, there is no separate file for
        // the form.
        mux := http.NewServeMux()
        mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        	w.Header().Set("Content-Type", "text/html; charset=utf-8")
        	fmt.Fprint(w, page)
        })
        mux.HandleFunc("/form", func(w http.ResponseWriter, r *http.Request) {
        	if r.Method != http.MethodPost {
        		reply(w, http.StatusMethodNotAllowed, "POST required", 0)
        		return
        	}
        	handleForm(w, r, core)
        })
        
        log.Println("form and handler: http://localhost:3000/")
        return http.ListenAndServe(":3000", mux)
        

2. Create a Form Handler

Create a file that accepts the form data and retains it in the CRM. In the PHP examples this is form.php; in the others it is the /form route handler.

Retrieve Data

Retrieve and process the data from the form.

const iRequisitePresetID = parseInt(req.body.REQ_TYPE, 10)
        const sName = String(req.body.NAME ?? '')
        const sLastName = String(req.body.LAST_NAME ?? '')
        const sPhone = String(req.body.PHONE ?? '')
        const arAddress = {}
        for (const [key, val] of Object.entries(req.body.ADDRESS ?? {})) {
            arAddress[key] = String(val)
        }
        
i_requisite_preset_id = int(request.form.get("REQ_TYPE", 0))
        s_name = request.form.get("NAME", "")
        s_last_name = request.form.get("LAST_NAME", "")
        s_phone = request.form.get("PHONE", "")
        ar_address = {k[len("ADDRESS["):-1]: v for k, v in request.form.to_dict().items()
                      if k.startswith("ADDRESS[")}
        
$iRequisitePresetID = intval($_POST["REQ_TYPE"] ?? 0);
        $sName = htmlspecialchars($_POST["NAME"] ?? '');
        $sLastName = htmlspecialchars($_POST["LAST_NAME"] ?? '');
        $sPhone = htmlspecialchars($_POST["PHONE"] ?? '');
        $arAddress = [];
        foreach (($_POST["ADDRESS"] ?? []) as $key => $val) {
            $arAddress[$key] = htmlspecialchars($val);
        }
        
// The requisite type is converted to a number, the rest is cleared of HTML tags.
        // The tags are CUT OUT rather than escaped: escaping is needed when rendering to
        // the page, and because of it the CRM would receive "Weber &amp; Son" instead of
        // "Weber & Son".
        presetID, _ := strconv.Atoi(r.PostFormValue("REQ_TYPE"))
        name := stripTags(r.PostFormValue("NAME"))
        lastName := stripTags(r.PostFormValue("LAST_NAME"))
        phone := stripTags(r.PostFormValue("PHONE"))
        
        if presetID == 0 || name == "" {
        	reply(w, http.StatusBadRequest, "Fill in the requisite type and the first name", 0)
        	return
        }
        
        // The address fields arrived with names such as ADDRESS[CITY] — parse them back.
        address := b24.Params{}
        for key, values := range r.PostForm {
        	if inner, ok := addressKey(key); ok && len(values) > 0 && values[0] != "" {
        		address[inner] = stripTags(values[0])
        	}
        }
        
  • $iRequisitePresetID — convert the requisite template identifier REQ_TYPE to an integer

  • $sName, $sLastName, $sPhone — safely process the data from NAME, LAST_NAME, PHONE to prevent XSS attacks

  • $arAddress — save the data from the array containing address fields ADDRESS

Prepare Data

Add two mandatory system fields to the $arAddress array.

The third mandatory field, ENTITY_ID, is substituted later: it is the requisite identifier, and the requisite does not exist yet.

arAddress.TYPE_ID = 1
        arAddress.ENTITY_TYPE_ID = 8
        
ar_address["TYPE_ID"] = 1
        ar_address["ENTITY_TYPE_ID"] = 8
        
$arAddress['TYPE_ID'] = 1;
        $arAddress['ENTITY_TYPE_ID'] = 8;
        
// The handler substitutes the address type and the owner type itself: they are not in the form.
        address["TYPE_ID"] = addressTypeActual
        address["ENTITY_TYPE_ID"] = typeRequisite
        

The system retains the phone number as a crm_multifield array of objects, so the $sPhone value must be converted to an array format:

  • in the first item VALUE, we write $sPhone

  • in the second item VALUE_TYPE, we pass, for example, WORK

If the $sPhone variable has no value, specify an empty array.

const arPhone = sPhone ? [{ VALUE: sPhone, VALUE_TYPE: 'WORK' }] : []
        
ar_phone = [{"VALUE": s_phone, "VALUE_TYPE": "WORK"}] if s_phone else []
        
$arPhone = !empty($sPhone) ? [['VALUE' => $sPhone, 'VALUE_TYPE' => 'WORK']] : [];
        
// The phone number is retained as a multifield — a list of objects, even when
        // there is a single number. A row WITHOUT an ID adds a value; MultifieldAdd
        // assembles it for you.
        phones := []map[string]any{}
        if phone != "" {
        	phones = append(phones, b24.MultifieldAdd(phone, "WORK"))
        }
        

Add a Contact

To add a contact, use the crm.contact.add method. You must pass the following data to it:

  • NAME — contact first name. We pass $sName, which was retrieved from the form

  • LAST_NAME — contact last name. We pass $sLastName, which was retrieved from the form

  • PHONE — an array containing the phone number $arPhone retrieved from the form

Check which mandatory fields are configured for contacts in your Bitrix24. All mandatory fields must be passed to the crm.contact.add method.

const contactResponse = await $b24.actions.v2.call.make({
            method: 'crm.contact.add',
            params: { fields: { NAME: sName, LAST_NAME: sLastName, PHONE: arPhone } },
            requestId: 'contact-add'
        })
        const iContactID = contactResponse.getData()?.result
        
i_contact_id = client.crm.contact.add(fields={
            "NAME": s_name,
            "LAST_NAME": s_last_name,
            "PHONE": ar_phone,
        }).result
        
$iContactID = $sb->getCRMScope()->contact()->add([
            'NAME' => $sName,
            'LAST_NAME' => $sLastName,
            'PHONE' => $arPhone,
        ])->getId();
        
res, err := core.Call(ctx, "crm.contact.add", b24.Params{
        	"fields": b24.Params{
        		"NAME":      name,
        		"LAST_NAME": lastName,
        		"PHONE":     phones,
        	},
        }) // without WithIdempotent: a retry would create a second contact
        if err != nil {
        	// The details go to the server log, they are not shown to the visitor.
        	log.Println("crm.contact.add:", err)
        	reply(w, http.StatusBadGateway, "Failed to create the contact", 0)
        	return
        }
        
        // There is no wrapper: result is the identifier of the new contact right away.
        var contactID b24.ID
        if err := json.Unmarshal(res.Result, &contactID); err != nil {
        	log.Println("parsing the contact identifier:", err)
        	reply(w, http.StatusBadGateway, "Failed to create the contact", 0)
        	return
        }
        

If the contact is successfully created, the method returns its identifier in $iContactID. Retain the value: the requisite needs it.

{
            "result": 23
        }
        

Add Requisites to the Contact

To add requisites, use the crm.requisite.add method. You must pass the following data to it:

  • ENTITY_TYPE_IDCRM object type identifier. We pass 3 — contact

  • ENTITY_ID — contact identifier. We pass $iContactID, which was obtained during contact creation

  • PRESET_ID — requisite template identifier. We specify $iRequisitePresetID, which was retrieved from the form

  • NAME — requisite name. We assemble it from the contact first and last name

  • ACTIVE — an activity flag; we will specify Y

const requisiteResponse = await $b24.actions.v2.call.make({
            method: 'crm.requisite.add',
            params: {
                fields: {
                    ENTITY_TYPE_ID: 3,
                    ENTITY_ID: iContactID,
                    PRESET_ID: iRequisitePresetID,
                    ACTIVE: 'Y',
                    NAME: [sName, sLastName].join(' ').trim(),
                }
            },
            requestId: 'requisite-add'
        })
        const iRequisiteID = requisiteResponse.getData()?.result
        
i_requisite_id = client.crm.requisite.add(fields={
            "ENTITY_TYPE_ID": 3,
            "ENTITY_ID": i_contact_id,
            "PRESET_ID": i_requisite_preset_id,
            "ACTIVE": "Y",
            "NAME": " ".join([s_name, s_last_name]).strip(),
        }).result
        
$iRequisiteID = $sb->getCRMScope()->requisite()->add(
            entityId: $iContactID,
            entityTypeId: 3,
            requisitePresetId: $iRequisitePresetID,
            requisiteName: trim(implode(' ', [$sName, $sLastName])),
            fields: ['ACTIVE' => 'Y']
        )->getId();
        
res, err = core.Call(ctx, "crm.requisite.add", b24.Params{
        	"fields": b24.Params{
        		"ENTITY_TYPE_ID": typeContact,
        		"ENTITY_ID":      contactID,
        		"PRESET_ID":      presetID,
        		"ACTIVE":         "Y",
        		"NAME":           strings.TrimSpace(name + " " + lastName),
        	},
        })
        if err != nil {
        	// The contact has already been created, so this is no reason to answer
        	// "nothing worked": report that the requisites were not added and return the
        	// identifier.
        	log.Println("crm.requisite.add:", err)
        	reply(w, http.StatusOK, "The contact was created, the requisites could not be added", contactID)
        	return
        }
        var requisiteID b24.ID
        if err := json.Unmarshal(res.Result, &requisiteID); err != nil {
        	log.Println("parsing the requisite identifier:", err)
        	reply(w, http.StatusOK, "The contact was created, the requisites could not be added", contactID)
        	return
        }
        

If the requisites are successfully added, the method returns the record identifier in $iRequisiteID.

{
            "result": 34
        }
        

The method does not check whether a template with the passed PRESET_ID exists. With a nonexistent identifier, the requisite is still created but remains without the template fields. Take PRESET_ID from the crm.requisite.preset.list response instead of substituting an arbitrary number.

Add an Address to the Requisite

  1. Add the ENTITY_ID field — the requisite identifier — to the $arAddress array. Pass the $iRequisiteID obtained during the creation of the requisite

    arAddress.ENTITY_ID = iRequisiteID
            
    ar_address["ENTITY_ID"] = i_requisite_id
            
    $arAddress['ENTITY_ID'] = $iRequisiteID;
            
    address["ENTITY_ID"] = requisiteID
            
  2. Use the crm.address.add method. You must pass the $arAddress array to it

    const bAddressAdded = (await $b24.actions.v2.call.make({
                method: 'crm.address.add', params: { fields: arAddress }, requestId: 'address-add'
            })).getData().result
            
    b_address_added = client.crm.address.add(fields=ar_address).result
            
    $bAddressAdded = $sb->getCRMScope()->address()->add($arAddress)->isSuccess();
            
    // The address is linked to the REQUISITE, not to the contact, so ENTITY_ID
            // is filled in only now — the requisite identifier did not exist earlier.
            if _, err := core.Call(ctx, "crm.address.add", b24.Params{"fields": address}); err != nil {
            	log.Println("crm.address.add:", err)
            	reply(w, http.StatusOK, "The contact and the requisites were created, the address could not be added", contactID)
            	return
            }
            

The method returns one of the following values in the $bAddressAdded variable:

  • true — the address was added

  • false — the address was not added

{
            "result": true
        }
        

Full Handler Code Example

import { B24Hook } from '@bitrix24/b24jssdk'
        
        const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
        // B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
        
        export async function handler(req, res) {
            const iRequisitePresetID = parseInt(req.body.REQ_TYPE, 10)
            const sName = String(req.body.NAME ?? '')
            const sLastName = String(req.body.LAST_NAME ?? '')
            const sPhone = String(req.body.PHONE ?? '')
        
            const arAddress = {}
            for (const [key, val] of Object.entries(req.body.ADDRESS ?? {})) {
                arAddress[key] = String(val)
            }
            arAddress.TYPE_ID = 1 // 1 — actual address (crm.enum.addresstype)
            arAddress.ENTITY_TYPE_ID = 8 // 8 — requisite (crm.enum.ownertype)
        
            const arPhone = sPhone ? [{ VALUE: sPhone, VALUE_TYPE: 'WORK' }] : []
        
            try {
                const contactResponse = await $b24.actions.v2.call.make({
                    method: 'crm.contact.add',
                    params: { fields: { NAME: sName, LAST_NAME: sLastName, PHONE: arPhone } },
                    requestId: 'contact-add'
                })
                const iContactID = contactResponse.getData()?.result
                if (!iContactID) {
                    res.json({ message: 'Error: ' + contactResponse.getErrorMessages().join('; ') })
                    return
                }
        
                const requisiteResponse = await $b24.actions.v2.call.make({
                    method: 'crm.requisite.add',
                    params: {
                        fields: {
                            ENTITY_TYPE_ID: 3, // 3 — contact (crm.enum.ownertype)
                            ENTITY_ID: iContactID,
                            PRESET_ID: iRequisitePresetID,
                            ACTIVE: 'Y',
                            NAME: [sName, sLastName].join(' ').trim(),
                        }
                    },
                    requestId: 'requisite-add'
                })
                const iRequisiteID = requisiteResponse.getData()?.result
        
                if (iRequisiteID) {
                    arAddress.ENTITY_ID = iRequisiteID
                    await $b24.actions.v2.call.make({
                        method: 'crm.address.add', params: { fields: arAddress }, requestId: 'address-add'
                    })
                }
        
                res.json({ message: 'The contact has been added' })
            } catch (e) {
                res.json({ message: 'Error: ' + e.message })
            }
        }
        
        // Attach the handler to the server from step 1. Without express.json()
        // the request body is not parsed and req.body stays empty
        // app.use(express.json())
        // app.post('/form', handler)
        
# pip install b24pysdk flask
        import os
        
        from flask import Flask, request, jsonify
        from b24pysdk import BitrixWebhook, Client
        
        app = Flask(__name__)
        
        client = Client(BitrixWebhook(
            domain=os.environ["B24_DOMAIN"],  # your-domain.bitrix24.com
            webhook_token=os.environ["B24_TOKEN"],  # user_id/token only, without https://
        ))
        
        
        @app.route("/form", methods=["POST"])
        def handle_form():
            # Retrieve and sanitize the form data
            i_requisite_preset_id = int(request.form.get("REQ_TYPE", 0))
            s_name = request.form.get("NAME", "")
            s_last_name = request.form.get("LAST_NAME", "")
            s_phone = request.form.get("PHONE", "")
        
            # Prepare the address
            ar_address = {k[len("ADDRESS["):-1]: v for k, v in request.form.to_dict().items()
                          if k.startswith("ADDRESS[")}
            ar_address["TYPE_ID"] = 1  # 1 — actual address (crm.enum.addresstype)
            ar_address["ENTITY_TYPE_ID"] = 8  # 8 — requisite (crm.enum.ownertype)
        
            # Format the phone number into the crm_multifield format
            ar_phone = [{"VALUE": s_phone, "VALUE_TYPE": "WORK"}] if s_phone else []
        
            try:
                i_contact_id = client.crm.contact.add(fields={
                    "NAME": s_name,
                    "LAST_NAME": s_last_name,
                    "PHONE": ar_phone,
                }).result
        
                i_requisite_id = client.crm.requisite.add(fields={
                    "ENTITY_TYPE_ID": 3,  # 3 — contact (crm.enum.ownertype)
                    "ENTITY_ID": i_contact_id,
                    "PRESET_ID": i_requisite_preset_id,
                    "ACTIVE": "Y",
                    "NAME": " ".join([s_name, s_last_name]).strip(),
                }).result
        
                if i_requisite_id:
                    ar_address["ENTITY_ID"] = i_requisite_id
                    client.crm.address.add(fields=ar_address)
        
                return jsonify({"message": "The contact has been added"})
            except Exception as e:
                return jsonify({"message": f"Error: {e}"})
        
<?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(getenv('B24_HOOK'));
        // B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
        $crm = $sb->getCRMScope();
        
        // Retrieve and sanitize the form data
        $iRequisitePresetID = intval($_POST["REQ_TYPE"] ?? 0);
        $sName = htmlspecialchars($_POST["NAME"] ?? '');
        $sLastName = htmlspecialchars($_POST["LAST_NAME"] ?? '');
        $sPhone = htmlspecialchars($_POST["PHONE"] ?? '');
        
        // Prepare the address
        $arAddress = [];
        foreach (($_POST["ADDRESS"] ?? []) as $key => $val) {
            $arAddress[$key] = htmlspecialchars($val);
        }
        $arAddress['TYPE_ID'] = 1; // 1 — actual address (crm.enum.addresstype)
        $arAddress['ENTITY_TYPE_ID'] = 8; // 8 — requisite (crm.enum.ownertype)
        
        // Format the phone number into the crm_multifield format
        $arPhone = !empty($sPhone) ? [['VALUE' => $sPhone, 'VALUE_TYPE' => 'WORK']] : [];
        
        try {
            $iContactID = $crm->contact()->add([
                'NAME' => $sName,
                'LAST_NAME' => $sLastName,
                'PHONE' => $arPhone,
            ])->getId();
        
            $iRequisiteID = $crm->requisite()->add(
                entityId: $iContactID,
                entityTypeId: 3, // 3contact (crm.enum.ownertype)
                requisitePresetId: $iRequisitePresetID,
                requisiteName: trim(implode(' ', [$sName, $sLastName])),
                fields: ['ACTIVE' => 'Y']
            )->getId();
        
            if (!empty($iRequisiteID)) {
                $arAddress['ENTITY_ID'] = $iRequisiteID;
                $crm->address()->add($arAddress);
            }
        
            echo json_encode(['message' => 'The contact has been added']);
        } catch (\Throwable $e) {
            echo json_encode(['message' => 'Error: ' . $e->getMessage()]);
        }
        
// Preparation in an empty directory — go get will not work without go mod init:
        //
        //	go mod init example && go get github.com/bitrix24/b24gosdk
        //
        // Run:
        //
        //	export B24_WEBHOOK_URL='https://your-domain.bitrix24.com/rest/1/token/' && go run .
        //
        // A separate file with the form is not needed: the same program assembles and
        // serves the page — it takes the address fields and the list of requisite
        // templates from Bitrix24. Open http://localhost:3000/
        package main
        
        import (
        	"context"
        	"encoding/json"
        	"fmt"
        	"html"
        	"log"
        	"net/http"
        	"os"
        	"regexp"
        	"sort"
        	"strconv"
        	"strings"
        
        	b24 "github.com/bitrix24/b24gosdk"
        )
        
        // CRM object type identifiers from crm.enum.ownertype.
        const (
        	typeContact   = 3
        	typeRequisite = 8
        )
        
        // addressTypeActual — actual address; the full list of types is returned by
        // crm.enum.addresstype.
        const addressTypeActual = 1
        
        func main() {
        	if err := run(context.Background()); err != nil {
        		log.Fatal(err)
        	}
        }
        
        func run(ctx context.Context) error {
        	// The webhook URL is a secret: it comes from the environment, not from the
        	// code, and never ends up on the public page with the form. The client is
        	// built ONCE per Bitrix24: http.Server calls the handler from many goroutines.
        	core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()
        
        	// --- assemble the form from the Bitrix24 settings
        	res, err := core.Call(ctx, "crm.address.fields", nil, b24.WithIdempotent())
        	if err != nil {
        		return fmt.Errorf("crm.address.fields: %w", err)
        	}
        
        	// The response is not a list but an object "field name -> description", hence a map.
        	var addressFields map[string]struct {
        		Type       string `json:"type"`
        		Title      string `json:"title"`
        		IsReadOnly bool   `json:"isReadOnly"`
        	}
        	if err := json.Unmarshal(res.Result, &addressFields); err != nil {
        		return fmt.Errorf("parsing address fields: %w", err)
        	}
        
        	// The form takes only the string fields that are available for writing: TYPE_ID,
        	// ENTITY_ID, and ENTITY_TYPE_ID also arrive in this response, but the handler
        	// substitutes them itself. Map keys in Go are unordered — sort them, otherwise
        	// the form fields will jump around from run to run.
        	var addressNames []string
        	for name, f := range addressFields {
        		if f.Type == "string" && !f.IsReadOnly {
        			addressNames = append(addressNames, name)
        		}
        	}
        	sort.Strings(addressNames)
        	res, err = core.Call(ctx, "crm.requisite.preset.list", b24.Params{
        		"select": []string{"ID", "NAME"},
        	}, b24.WithIdempotent())
        	if err != nil {
        		return fmt.Errorf("crm.requisite.preset.list: %w", err)
        	}
        
        	// Here the identifier arrives as a STRING ("1"), whereas crm.enum.* returns
        	// numbers. b24.ID parses both notations.
        	var presets []struct {
        		ID   b24.ID `json:"ID"`
        		Name string `json:"NAME"`
        	}
        	if err := json.Unmarshal(res.Result, &presets); err != nil {
        		return fmt.Errorf("parsing requisite templates: %w", err)
        	}
        	if len(presets) == 0 {
        		return fmt.Errorf("there are no requisite templates in Bitrix24")
        	}
        	// --- the page with the form
        	var form strings.Builder
        	form.WriteString(`<!doctype html>
        <meta charset="utf-8">
        <title>Request</title>
        <form method="post" action="/form">
        <p><label>Requisite type*<br><select name="REQ_TYPE" required>`)
        	for _, p := range presets {
        		fmt.Fprintf(&form, `<option value="%d">%s</option>`, p.ID, html.EscapeString(p.Name))
        	}
        	form.WriteString(`</select></label></p>
        <p><label>First name*<br><input name="NAME" required></label></p>
        <p><label>Last name<br><input name="LAST_NAME"></label></p>
        <p><label>Phone<br><input name="PHONE" type="tel"></label></p>`)
        	// The address fields are created dynamically: their set is defined by Bitrix24,
        	// not by the code. Names such as ADDRESS[CITY] — the handler parses them back.
        	for _, name := range addressNames {
        		fmt.Fprintf(&form, "<p><label>%s<br><input name=\"ADDRESS[%s]\"></label></p>\n",
        			html.EscapeString(addressFields[name].Title), name)
        	}
        	form.WriteString(`<p><button type="submit">Submit</button></p>
        </form>`)
        	page := form.String()
        	mux := http.NewServeMux()
        	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        		w.Header().Set("Content-Type", "text/html; charset=utf-8")
        		fmt.Fprint(w, page)
        	})
        	mux.HandleFunc("/form", func(w http.ResponseWriter, r *http.Request) {
        		if r.Method != http.MethodPost {
        			reply(w, http.StatusMethodNotAllowed, "POST required", 0)
        			return
        		}
        		handleForm(w, r, core)
        	})
        
        	log.Println("form and handler: http://localhost:3000/")
        	return http.ListenAndServe(":3000", mux)
        }
        
        func handleForm(w http.ResponseWriter, r *http.Request, core *b24.Core) {
        	ctx := r.Context()
        	if err := r.ParseForm(); err != nil {
        		reply(w, http.StatusBadRequest, "Failed to parse the form", 0)
        		return
        	}
        	// The requisite type is converted to a number, the rest is cleared of HTML tags.
        	// The tags are CUT OUT rather than escaped: escaping is needed when rendering to
        	// the page, and because of it the CRM would receive "Weber &amp; Son" instead of
        	// "Weber & Son".
        	presetID, _ := strconv.Atoi(r.PostFormValue("REQ_TYPE"))
        	name := stripTags(r.PostFormValue("NAME"))
        	lastName := stripTags(r.PostFormValue("LAST_NAME"))
        	phone := stripTags(r.PostFormValue("PHONE"))
        
        	if presetID == 0 || name == "" {
        		reply(w, http.StatusBadRequest, "Fill in the requisite type and the first name", 0)
        		return
        	}
        	// The address fields arrived with names such as ADDRESS[CITY] — parse them back.
        	address := b24.Params{}
        	for key, values := range r.PostForm {
        		if inner, ok := addressKey(key); ok && len(values) > 0 && values[0] != "" {
        			address[inner] = stripTags(values[0])
        		}
        	}
        	// The handler substitutes the address type and the owner type itself: they are not in the form.
        	address["TYPE_ID"] = addressTypeActual
        	address["ENTITY_TYPE_ID"] = typeRequisite
        	// The phone number is retained as a multifield — a list of objects, even when
        	// there is a single number. A row WITHOUT an ID adds a value; MultifieldAdd
        	// assembles it for you.
        	phones := []map[string]any{}
        	if phone != "" {
        		phones = append(phones, b24.MultifieldAdd(phone, "WORK"))
        	}
        	res, err := core.Call(ctx, "crm.contact.add", b24.Params{
        		"fields": b24.Params{
        			"NAME":      name,
        			"LAST_NAME": lastName,
        			"PHONE":     phones,
        		},
        	}) // without WithIdempotent: a retry would create a second contact
        	if err != nil {
        		// The details go to the server log, they are not shown to the visitor.
        		log.Println("crm.contact.add:", err)
        		reply(w, http.StatusBadGateway, "Failed to create the contact", 0)
        		return
        	}
        
        	// There is no wrapper: result is the identifier of the new contact right away.
        	var contactID b24.ID
        	if err := json.Unmarshal(res.Result, &contactID); err != nil {
        		log.Println("parsing the contact identifier:", err)
        		reply(w, http.StatusBadGateway, "Failed to create the contact", 0)
        		return
        	}
        	res, err = core.Call(ctx, "crm.requisite.add", b24.Params{
        		"fields": b24.Params{
        			"ENTITY_TYPE_ID": typeContact,
        			"ENTITY_ID":      contactID,
        			"PRESET_ID":      presetID,
        			"ACTIVE":         "Y",
        			"NAME":           strings.TrimSpace(name + " " + lastName),
        		},
        	})
        	if err != nil {
        		// The contact has already been created, so this is no reason to answer
        		// "nothing worked": report that the requisites were not added and return the
        		// identifier.
        		log.Println("crm.requisite.add:", err)
        		reply(w, http.StatusOK, "The contact was created, the requisites could not be added", contactID)
        		return
        	}
        	var requisiteID b24.ID
        	if err := json.Unmarshal(res.Result, &requisiteID); err != nil {
        		log.Println("parsing the requisite identifier:", err)
        		reply(w, http.StatusOK, "The contact was created, the requisites could not be added", contactID)
        		return
        	}
        	// The address is linked to the REQUISITE, not to the contact, so ENTITY_ID
        	// is filled in only now — the requisite identifier did not exist earlier.
        	if requisiteID != 0 {
        		address["ENTITY_ID"] = requisiteID
        		if _, err := core.Call(ctx, "crm.address.add", b24.Params{"fields": address}); err != nil {
        			log.Println("crm.address.add:", err)
        			reply(w, http.StatusOK, "The contact and the requisites were created, the address could not be added", contactID)
        			return
        		}
        	}
        	log.Printf("created contact %d, requisite %d", contactID, requisiteID)
        	reply(w, http.StatusOK, "The contact with requisites has been created", contactID)
        }
        
        // tagPattern cuts HTML tags out of a form value.
        var tagPattern = regexp.MustCompile(`<[^>]*>`)
        
        func stripTags(s string) string {
        	return strings.TrimSpace(tagPattern.ReplaceAllString(s, ""))
        }
        
        // addressKey extracts CITY from the ADDRESS[CITY] field name.
        func addressKey(key string) (string, bool) {
        	if strings.HasPrefix(key, "ADDRESS[") && strings.HasSuffix(key, "]") {
        		return key[len("ADDRESS[") : len(key)-1], true
        	}
        	return "", false
        }
        
        // reply answers the page with the same JSON as the handlers in the other languages.
        func reply(w http.ResponseWriter, status int, message string, id b24.ID) {
        	w.Header().Set("Content-Type", "application/json; charset=utf-8")
        	w.WriteHeader(status)
        	body := map[string]any{"message": message}
        	if id != 0 {
        		body["id"] = id
        	}
        	_ = json.NewEncoder(w).Encode(body)
        }
        

Verify the Result

Open the created contact in Bitrix24. On the "Requisites" tab, the requisite with the address from the form is displayed.

Through REST, the result is verified with two methods:

  • crm.requisite.list with a filter by ENTITY_TYPE_ID: 3 and ENTITY_ID — the identifier of the created contact

  • crm.address.list with a filter by ENTITY_TYPE_ID: 8 and ENTITY_ID — the identifier of the created requisite

const requisites = (await $b24.actions.v2.call.make({
            method: 'crm.requisite.list',
            params: {
                filter: { ENTITY_TYPE_ID: 3, ENTITY_ID: iContactID },
                select: ['ID', 'ENTITY_TYPE_ID', 'ENTITY_ID', 'NAME']
            },
            requestId: 'requisite-list'
        })).getData().result
        
        const addresses = (await $b24.actions.v2.call.make({
            method: 'crm.address.list',
            params: { filter: { ENTITY_TYPE_ID: 8, ENTITY_ID: iRequisiteID } },
            requestId: 'address-list'
        })).getData().result
        
        console.dir({ requisites, addresses })
        
requisites = client.crm.requisite.list(
            filter={"ENTITY_TYPE_ID": 3, "ENTITY_ID": i_contact_id},
            select=["ID", "ENTITY_TYPE_ID", "ENTITY_ID", "NAME"],
        ).result
        
        addresses = client.crm.address.list(
            filter={"ENTITY_TYPE_ID": 8, "ENTITY_ID": i_requisite_id},
        ).result
        
        print(requisites)
        print(addresses)
        
$requisites = $sb->getCRMScope()->requisite()->list(
            [],
            ['ENTITY_TYPE_ID' => 3, 'ENTITY_ID' => $iContactID],
            ['ID', 'ENTITY_TYPE_ID', 'ENTITY_ID', 'NAME']
        )->getRequisites();
        
        $addresses = $sb->getCRMScope()->address()->list(
            [],
            ['ENTITY_TYPE_ID' => 8, 'ENTITY_ID' => $iRequisiteID],
            []
        )->getAddresses();
        
        print_r($requisites);
        print_r($addresses);
        
res, err := core.Call(ctx, "crm.requisite.list", b24.Params{
        	"filter": b24.Params{"ENTITY_TYPE_ID": 3, "ENTITY_ID": contactID},
        	"select": []string{"ID", "ENTITY_TYPE_ID", "ENTITY_ID", "NAME"},
        }, b24.WithIdempotent())
        if err != nil {
        	return fmt.Errorf("crm.requisite.list: %w", err)
        }
        log.Println("contact requisites:", string(res.Result))
        
        res, err = core.Call(ctx, "crm.address.list", b24.Params{
        	"filter": b24.Params{"ENTITY_TYPE_ID": 8, "ENTITY_ID": requisiteID},
        }, b24.WithIdempotent())
        if err != nil {
        	return fmt.Errorf("crm.address.list: %w", err)
        }
        log.Println("requisite addresses:", string(res.Result))
        

The scenario is complete if crm.requisite.list returned the requisite with the ID from the "Add Requisites to the Contact" step, and crm.address.list returned the address with the same ENTITY_ID.

{
            "result": [
                {
                    "ENTITY_TYPE_ID": "3",
                    "ENTITY_ID": "23",
                    "ID": "34",
                    "NAME": "Klaus Weber"
                }
            ],
            "total": 1
        }
        
{
            "result": [
                {
                    "TYPE_ID": "1",
                    "ENTITY_TYPE_ID": "8",
                    "ENTITY_ID": "34",
                    "ADDRESS_1": "Tiergartenstraße 17",
                    "CITY": "Berlin",
                    "POSTAL_CODE": "10785"
                }
            ],
            "total": 1
        }
        

Errors and Diagnostics

If the method returns an error, check the request data. The requisite and address methods return errors with an empty code, so rely on the text in error_description.

Error text

Reason and action

Entity not found.

An ENTITY_ID of a nonexistent contact was passed to crm.requisite.add. Take the identifier from the crm.contact.add response

ENTITY_TYPE_ID is not defined or invalid.

The owner type was not passed or is incorrect. A contact requisite requires 3, a requisite address requires 8

ENTITY_ID is not defined or invalid.

The owner identifier was not passed. In crm.address.add, this is the requisite identifier, not the contact identifier

TYPE_ID is not defined or invalid.

The address type was not passed to crm.address.add. The list of values is returned by the crm.enum.addresstype method

TypeAddress exists.

The requisite already has an address of this type. One requisite retains one address of each type — modify the existing one with the crm.address.update method or pass a different TYPE_ID

Access denied.

The user does not have permission to add or import contacts. Check which user the webhook was created on behalf of

The scenario creates three objects in a row, and an error at any step leaves the previous objects in the CRM. Repeat the step that failed rather than the whole handler:

  • An error in crm.contact.add — nothing has been created in the CRM, you can repeat the entire handler

  • An error in crm.requisite.add — the contact has already been created. Running the handler again creates a duplicate of it, so pass the existing ENTITY_ID

  • An error in crm.address.add — the contact and the requisite have already been created. Add the address with a separate call using the ENTITY_ID of the existing requisite

Key Considerations

  • An address has no identifier of its own: it is recognized by the ENTITY_TYPE_ID and ENTITY_ID pair plus TYPE_ID

  • The set of requisite fields depends on the template. For a contact, the individual template is usually selected, and it has no organization fields. The set of template fields is returned by the crm.requisite.preset.field.list method. The value of a field that is not in the template is retained and returned by crm.requisite.get, but it is not displayed on the requisite card

  • Submitting the form again with the same data creates a new contact and a new requisite. Duplicates are not filtered out

Continue Learning