How to Retrieve a Customer Address from the CRM
Scope:
crmWho can execute the methods:
- crm.requisite.list — a user with permission to read contacts or companies
- crm.address.list — a user with permission to read contacts, companies, and leads simultaneously
- crm.contact.userfield.list — an administrator
- crm.contact.get — a user with permission to read a contact
If you are developing integrations for Bitrix24 using AI tools (Codex, Claude Code, Cursor), connect to the MCP server so that the assistant can utilize the official REST documentation.
A customer address is stored in Bitrix24 in two independent ways.
- In the Company details of contacts and companies. This is the standard method. In the customer card, the address is displayed as a separate Company details field. A single customer may have multiple Company details, and within a single Company details entry, there can be several addresses of different types. Leads do not have Company details; their address is linked directly to the lead itself.
- In a custom field of type
address. An administrator creates such a field separately for a specific CRM object type, and the value is stored as a string within the object itself.
These methods are not linked. An address from Company details does not populate a custom field, and an address from a custom field is not visible to crm.address.* methods. If it is unknown where a specific customer's address is filled in, check both methods.
This tutorial covers both. The primary scenario is retrieving the address from Company details, which consists of two steps.
- Retrieve the customer's Company details identifiers using the crm.requisite.list method.
- Retrieve the addresses for these Company details using the crm.address.list method.
The second method is described in the Address From a Custom Field section.
Prepare the Data
The following are required for the scenario:
- An incoming webhook with
crmpermission — the examples use it for authorization. Store the webhook URL in an environment variable rather than in the code. - A customer identifier. The examples use a contact with
ID2429. You can retrieve the identifier using the crm.contact.list method with a filter on any known contact field, or for a company, using the crm.company.list method. If only a phone number or Webmail is known, use the “Search for Duplicates by Phone Number” tutorial.
1. Retrieve Company Details Linked to a Contact
An address is not linked directly to a contact or company — it is linked to a Company details entry. Therefore, first retrieve the customer's Company details identifiers.
To do this, use the crm.requisite.list method with a filter:
- Specify the value
3inENTITY_TYPE_ID— the identifier for the contact type. For the company type, use the identifier4. - Specify the contact identifier in
ENTITY_ID; in the example, this is2429.
How to Use Examples in Documentation
The example steps follow one another. The SDK is initialized once here, and the existing instance is used thereafter.
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 result = await $b24.actions.v2.call.make({
method: 'crm.requisite.list',
params: {
filter: {
ENTITY_TYPE_ID: 3,
ENTITY_ID: 2429,
},
select: [
'ID',
'ENTITY_TYPE_ID',
'ENTITY_ID',
],
}
});
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$sb = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook(getenv('B24_HOOK'));
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
$requisites = $sb->getCRMScope()->requisite()->list(
[],
[
'ENTITY_TYPE_ID' => 3,
'ENTITY_ID' => 2429,
],
[
'ID',
'ENTITY_TYPE_ID',
'ENTITY_ID',
]
)->getRequisites();
print_r($requisites);
import os
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError
client = Client(
BitrixWebhook(
domain=os.environ["B24_DOMAIN"],
webhook_token=os.environ["B24_WEBHOOK_TOKEN"],
)
)
# B24_DOMAIN = 'your-domain.bitrix24.com'
# B24_WEBHOOK_TOKEN = 'user_id/webhook_key'
result = client.crm.requisite.list(
filter={
"ENTITY_TYPE_ID": 3,
"ENTITY_ID": 2429,
},
select=[
"ID",
"ENTITY_TYPE_ID",
"ENTITY_ID",
],
).response.result
// The address is bound not to the contact but to its REQUISITE, so you first need
// the requisite IDs.
res, err := core.Call(ctx, "crm.requisite.list", b24.Params{
"filter": b24.Params{"ENTITY_TYPE_ID": typeContact, "ENTITY_ID": contactID},
"select": []string{"ID", "ENTITY_TYPE_ID", "ENTITY_ID"},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.requisite.list: %w", err)
}
// Here the IDs arrive AS STRINGS ("361"), although crm.enum.* returns them
// as numbers. b24.ID parses both spellings, a plain int does not.
var requisites []struct {
ID b24.ID `json:"ID"`
}
if err := json.Unmarshal(res.Result, &requisites); err != nil {
return fmt.Errorf("parse requisites: %w", err)
}
if len(requisites) == 0 {
return fmt.Errorf("contact %d has no requisites, there is nowhere to store the address", contactID)
}
The response will contain a list of the contact's Company details. In the example, there is one Company details entry, and its ID is 361. This is the value required for the next request.
{
"result": [
{
"ID": "361",
"ENTITY_TYPE_ID": "3",
"ENTITY_ID": "2429"
}
],
"total": 1
}
If there are multiple Company details in the response, you must request addresses for each ID from result.
2. Retrieve Company Details Addresses
To retrieve addresses, use the crm.address.list method with the following filter:
- specify the value
8inENTITY_TYPE_ID— the identifier for the company details type - specify the company details identifier from step 1 in
ENTITY_ID, which is361in the example
Without a type filter, the method returns all company details addresses. This allows you to obtain the complete list of customer addresses.
The lead address is retrieved using the same method, but without step 1. Specify 1 in ENTITY_TYPE_ID — the identifier for the lead type, and ENTITY_ID — the identifier of the lead itself.
const result = await $b24.actions.v2.call.make({
method: 'crm.address.list',
params: {
filter: {
ENTITY_TYPE_ID: 8,
ENTITY_ID: 361,
},
}
});
$addresses = $sb->getCRMScope()->address()->list(
[],
[
'ENTITY_TYPE_ID' => 8,
'ENTITY_ID' => 361,
],
[]
)->getAddresses();
print_r($addresses);
result = client.crm.address.list(
filter={
"ENTITY_TYPE_ID": 8,
"ENTITY_ID": 361,
}
).response.result
// Without a filter by type, the method returns all addresses of the requisite.
res, err := core.Call(ctx, "crm.address.list", b24.Params{
"filter": b24.Params{"ENTITY_TYPE_ID": typeRequisite, "ENTITY_ID": r.ID},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.address.list: %w", err)
}
var addresses []address
if err := json.Unmarshal(res.Result, &addresses); err != nil {
return fmt.Errorf("parse addresses: %w", err)
}
The response contains all company details addresses. In the example, there are two — the street address and the delivery address.
{
"result": [
{
"TYPE_ID": "1",
"ENTITY_TYPE_ID": "8",
"ENTITY_ID": "361",
"ADDRESS_1": "Tverskaya Street, 7",
"ADDRESS_2": null,
"CITY": "Berlin",
"POSTAL_CODE": "125009",
"REGION": null,
"PROVINCE": "Berlin",
"COUNTRY": "Germany",
"COUNTRY_CODE": null,
"LOC_ADDR_ID": "569",
"ANCHOR_TYPE_ID": "3",
"ANCHOR_ID": "2429"
},
{
"TYPE_ID": "11",
"ENTITY_TYPE_ID": "8",
"ENTITY_ID": "361",
"ADDRESS_1": "Granatny Lane, 10",
"ADDRESS_2": null,
"CITY": "Berlin",
"POSTAL_CODE": "123001",
"REGION": "Presnensky District",
"PROVINCE": "Berlin",
"COUNTRY": "Germany",
"COUNTRY_CODE": null,
"LOC_ADDR_ID": "571",
"ANCHOR_TYPE_ID": "3",
"ANCHOR_ID": "2429"
}
],
"total": 2
}
Key response fields:
TYPE_ID— address type. In the example,1is the street address and11is the delivery address. The crm.enum.addresstype method returns the full list of types.ADDRESS_1,ADDRESS_2,CITY,POSTAL_CODE,REGION,PROVINCE,COUNTRY— the address components. You must construct the address string from these components, as the method does not provide a single field with a ready-made string. Unfilled components are returned asnull, which is normal even for a completed address.ANCHOR_TYPE_IDandANCHOR_ID— the type and identifier of the customer to whom the company details belong. In the example,3and2429refer to the original contact. Use this pair to verify that the address belongs to the correct customer.
To retrieve only one type of address, add TYPE_ID to the filter. For example, for a delivery address:
const result = await $b24.actions.v2.call.make({
method: 'crm.address.list',
params: {
filter: {
ENTITY_TYPE_ID: 8,
ENTITY_ID: 361,
TYPE_ID: 11,
},
}
});
$addresses = $sb->getCRMScope()->address()->list(
[],
[
'ENTITY_TYPE_ID' => 8,
'ENTITY_ID' => 361,
'TYPE_ID' => 11,
],
[]
)->getAddresses();
print_r($addresses);
result = client.crm.address.list(
filter={
"ENTITY_TYPE_ID": 8,
"ENTITY_ID": 361,
"TYPE_ID": 11,
}
).response.result
res, err = core.Call(ctx, "crm.address.list", b24.Params{
"filter": b24.Params{
"ENTITY_TYPE_ID": typeRequisite,
"ENTITY_ID": r.ID,
"TYPE_ID": 11, // 11 — delivery address
},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.address.list by type: %w", err)
}
var delivery []address
if err := json.Unmarshal(res.Result, &delivery); err != nil {
return fmt.Errorf("parse delivery addresses: %w", err)
}
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/'
// Customer ID, which can be obtained using the crm.contact.list method
const contactId = 2429;
// CRM object type: 3 — contact, 4 — company
const entityTypeId = 3;
// Address type from crm.enum.addresstype, for example 11 — delivery address.
// Leave null to get all customer addresses
const addressTypeId = null;
// Getting customer requisition IDs
const requisiteResult = await $b24.actions.v2.call.make({
method: 'crm.requisite.list',
params: {
filter: {
ENTITY_TYPE_ID: entityTypeId,
ENTITY_ID: contactId
},
select: ["ID"]
}
});
if (!requisiteResult.isSuccess) {
console.error(requisiteResult.getErrorMessages().join('; '));
} else {
const requisites = requisiteResult.getData().result;
if (requisites.length === 0) {
console.log("The customer has no requisitions, there is nowhere to store the address.");
} else {
const rows = [];
// The customer may have several requisitions, we iterate through each one
for (const requisite of requisites) {
const filter = {
ENTITY_TYPE_ID: 8,
ENTITY_ID: requisite.ID
};
if (addressTypeId !== null) {
filter.TYPE_ID = addressTypeId;
}
const addressResult = await $b24.actions.v2.call.make({
method: 'crm.address.list',
params: { filter: filter }
});
if (!addressResult.isSuccess) {
console.error(addressResult.getErrorMessages().join('; '));
continue;
}
for (const address of addressResult.getData().result) {
rows.push({
"Requisition": requisite.ID,
"Address type": address.TYPE_ID,
"Address": address.ADDRESS_1 || "Not specified",
"City": address.CITY || "Not specified",
"Postal code": address.POSTAL_CODE || "Not specified",
"Country": address.COUNTRY || "Not specified"
});
}
}
if (rows.length === 0) {
console.log("Customer requisitions have no addresses.");
} else {
console.table(rows);
}
}
}
<?php
// composer require bitrix24/b24phpsdk:"^3.0"
require_once 'vendor/autoload.php';
use Bitrix24\SDK\Services\ServiceBuilderFactory;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
$log = new Logger('b24');
$log->pushHandler(new StreamHandler('php://stdout'));
$sb = (new ServiceBuilderFactory(new EventDispatcher(), $log))
->initFromWebhook(getenv('B24_HOOK'));
// B24_HOOK = 'https://your-domain.bitrix24.com/rest/USER_ID/TOKEN/'
// Customer ID, which can be obtained using the crm.contact.list method
$contactId = 2429;
// CRM object type: 3 — contact, 4 — company
$entityTypeId = 3;
// Address type from crm.enum.addresstype, for example 11 — delivery address.
// Leave null to get all customer addresses
$addressTypeId = null;
try {
// Getting customer requisition IDs
$requisites = $sb->getCRMScope()->requisite()->list(
[],
[
'ENTITY_TYPE_ID' => $entityTypeId,
'ENTITY_ID' => $contactId
],
['ID']
)->getRequisites();
if (count($requisites) === 0) {
echo 'The customer has no requisitions, there is nowhere to store the address.';
return;
}
$rows = [];
// The customer may have several requisitions, we iterate through each one
foreach ($requisites as $requisite) {
$filter = [
'ENTITY_TYPE_ID' => 8,
'ENTITY_ID' => $requisite->ID
];
if ($addressTypeId !== null) {
$filter['TYPE_ID'] = $addressTypeId;
}
$addresses = $sb->getCRMScope()->address()->list(
[],
$filter,
[]
)->getAddresses();
foreach ($addresses as $address) {
$rows[] = [
'requisiteId' => $requisite->ID,
'typeId' => $address->TYPE_ID,
'address' => $address->ADDRESS_1 ?? 'Not specified',
'city' => $address->CITY ?? 'Not specified',
'postalCode' => $address->POSTAL_CODE ?? 'Not specified',
'country' => $address->COUNTRY ?? 'Not specified'
];
}
}
if (count($rows) === 0) {
echo 'Customer requisitions have no addresses.';
return;
}
echo '<table border="1">';
echo '<tr><th>Requisition</th><th>Address type</th><th>Address</th><th>City</th><th>Postal code</th><th>Country</th></tr>';
foreach ($rows as $row) {
echo '<tr>';
foreach ($row as $value) {
echo '<td>' . htmlspecialchars((string)$value) . '</td>';
}
echo '</tr>';
}
echo '</table>';
} catch (\Throwable $e) {
echo 'Error: ' . $e->getMessage();
}
import os
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError
client = Client(
BitrixWebhook(
domain=os.environ["B24_DOMAIN"],
webhook_token=os.environ["B24_WEBHOOK_TOKEN"],
)
)
# B24_DOMAIN = 'your-domain.bitrix24.com'
# B24_WEBHOOK_TOKEN = 'user_id/webhook_key'
# Customer ID, which can be obtained using the crm.contact.list method
contact_id = 2429
# CRM object type: 3 — contact, 4 — company
entity_type_id = 3
# Address type from crm.enum.addresstype, for example 11 — delivery address.
# Leave None to get all customer addresses
address_type_id = None
try:
requisites = client.crm.requisite.list(
filter={
"ENTITY_TYPE_ID": entity_type_id,
"ENTITY_ID": contact_id,
},
select=["ID"],
).response.result
except BitrixAPIError as error:
print(f"Error: {error}")
else:
if not requisites:
print("The customer has no requisitions, there is nowhere to store the address.")
else:
rows = []
# The customer may have several requisitions, we iterate through each one
for requisite in requisites:
address_filter = {
"ENTITY_TYPE_ID": 8,
"ENTITY_ID": requisite["ID"],
}
if address_type_id is not None:
address_filter["TYPE_ID"] = address_type_id
try:
addresses = client.crm.address.list(
filter=address_filter,
).response.result
except BitrixAPIError as error:
print(f"Error: {error}")
continue
for address in addresses:
rows.append(
[
str(requisite["ID"]),
str(address.get("TYPE_ID") or "Not specified"),
str(address.get("ADDRESS_1") or "Not specified"),
str(address.get("CITY") or "Not specified"),
str(address.get("POSTAL_CODE") or "Not specified"),
str(address.get("COUNTRY") or "Not specified"),
]
)
if not rows:
print("Customer requisitions have no addresses.")
else:
print("Requisition\tAddress type\tAddress\tCity\tPostal code\tCountry")
for row in rows:
print("\t".join(row))
// Setup 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-portal.bitrix24.com/rest/1/token/' && go run .
//
// The example is self-contained: it creates a contact with a requisite and two addresses,
// finds the addresses the way the page describes and cleans up after itself. It runs
// on any portal, nothing needs to be edited.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
b24 "github.com/bitrix24/b24gosdk"
)
// The IDs of CRM object types: crm.enum.ownertype returns the full list.
const (
typeContact = 3 // a contact; a company is 4
typeRequisite = 8 // a requisite
)
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
// The webhook path is a secret, so it comes from the environment, not from the code.
core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()
// --- setup: a contact, its requisite, and two addresses
contactID, err := createClient(ctx, core)
if err != nil {
return err
}
// Deleting a contact also removes its requisites and the addresses of those requisites.
defer del(ctx, core, "crm.contact.delete", b24.Params{"id": contactID})
// --- step 1: the client requisites
// The address is bound not to the contact but to its REQUISITE, so you first need
// the requisite IDs.
res, err := core.Call(ctx, "crm.requisite.list", b24.Params{
"filter": b24.Params{"ENTITY_TYPE_ID": typeContact, "ENTITY_ID": contactID},
"select": []string{"ID", "ENTITY_TYPE_ID", "ENTITY_ID"},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.requisite.list: %w", err)
}
// Here the IDs arrive AS STRINGS ("361"), although crm.enum.* returns them
// as numbers. b24.ID parses both spellings, a plain int does not.
var requisites []struct {
ID b24.ID `json:"ID"`
}
if err := json.Unmarshal(res.Result, &requisites); err != nil {
return fmt.Errorf("parse requisites: %w", err)
}
if len(requisites) == 0 {
return fmt.Errorf("contact %d has no requisites, there is nowhere to store the address", contactID)
}
fmt.Printf("requisites of contact %d: %d\n", contactID, len(requisites))
// --- step 2: the addresses of each requisite
for _, r := range requisites {
// Without a filter by type, the method returns all addresses of the requisite.
res, err := core.Call(ctx, "crm.address.list", b24.Params{
"filter": b24.Params{"ENTITY_TYPE_ID": typeRequisite, "ENTITY_ID": r.ID},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.address.list: %w", err)
}
var addresses []address
if err := json.Unmarshal(res.Result, &addresses); err != nil {
return fmt.Errorf("parse addresses: %w", err)
}
for _, a := range addresses {
fmt.Printf(" requisite %d, type %d: %s\n", r.ID, a.TypeID, a.String())
}
// To retrieve an address of only one type, add TYPE_ID to the filter.
res, err = core.Call(ctx, "crm.address.list", b24.Params{
"filter": b24.Params{
"ENTITY_TYPE_ID": typeRequisite,
"ENTITY_ID": r.ID,
"TYPE_ID": 11, // 11 — delivery address
},
}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.address.list by type: %w", err)
}
var delivery []address
if err := json.Unmarshal(res.Result, &delivery); err != nil {
return fmt.Errorf("parse delivery addresses: %w", err)
}
fmt.Printf(" of them delivery addresses: %d\n", len(delivery))
}
// --- the second storage option: a custom field of the address type
return userFieldAddresses(ctx, core, contactID)
}
// address is a single row of the crm.address.list response. The method has no ready-made address string
// at all: it is assembled from parts, and the unfilled parts arrive as null — this is
// normal even for a filled-in address.
type address struct {
TypeID b24.ID `json:"TYPE_ID"`
Address1 string `json:"ADDRESS_1"`
City string `json:"CITY"`
PostalCode string `json:"POSTAL_CODE"`
Country string `json:"COUNTRY"`
// This pair is used to verify that the address belongs to the right client.
AnchorTypeID b24.ID `json:"ANCHOR_TYPE_ID"`
AnchorID b24.ID `json:"ANCHOR_ID"`
}
func (a address) String() string {
out := ""
for _, part := range []string{a.PostalCode, a.Country, a.City, a.Address1} {
if part == "" {
continue
}
if out != "" {
out += ", "
}
out += part
}
if out == "" {
return "not specified"
}
return out
}
// userFieldAddresses reads the address stored not in the requisite but directly in
// the contact — in a custom field of the address type. To the crm.address.* methods such an
// address is not visible, this is an independent storage option.
func userFieldAddresses(ctx context.Context, core *b24.Core, contactID b24.ID) error {
res, err := core.Call(ctx, "crm.contact.userfield.list", b24.Params{
"filter": b24.Params{"USER_TYPE_ID": "address"},
}, b24.WithIdempotent())
if err != nil {
// The method is available only to an administrator — this is no reason to abort the scenario
// with the addresses from the requisites, it has already run.
fmt.Fprintf(os.Stderr, "crm.contact.userfield.list: %v\n", err)
return nil
}
var fields []struct {
FieldName string `json:"FIELD_NAME"`
Multiple string `json:"MULTIPLE"`
}
if err := json.Unmarshal(res.Result, &fields); err != nil {
return fmt.Errorf("parse custom fields: %w", err)
}
res, err = core.Call(ctx, "crm.contact.get",
b24.Params{"id": contactID}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.contact.get: %w", err)
}
for _, f := range fields {
// The shape of the value depends on MULTIPLE: for a multiple field it is an array.
raw, ok := b24.Unwrap(res.Result, f.FieldName)
if !ok || b24.IsEmpty(raw) {
fmt.Printf(" field %s (MULTIPLE=%s): not filled in\n", f.FieldName, f.Multiple)
continue
}
fmt.Printf(" field %s (MULTIPLE=%s): %s\n", f.FieldName, f.Multiple, raw)
}
if len(fields) == 0 {
fmt.Println(" contacts on this portal have no fields of the address type")
}
return nil
}
// --- helpers: data setup and cleanup
// createClient creates a contact, its requisite, and two addresses in ONE linked
// batch: 4 commands cost one call to the portal instead of four.
func createClient(ctx context.Context, core *b24.Core) (b24.ID, error) {
presetID, err := firstPreset(ctx, core)
if err != nil {
return 0, err
}
b := b24.NewBatch()
// Halt is MANDATORY for a linked batch. Without it, a failed producer is not
// an error for the server: it will substitute the placeholder text itself for the consumer
// as a value, and the requisite would be bound to a "contact" named "$result[...]".
b.Halt = true
if err := b.AddAs("contact", "crm.contact.add", b24.Params{
"fields": b24.Params{"NAME": "Klaus", "LAST_NAME": "Weber"},
}); err != nil {
return 0, err
}
// There is no path: crm.contact.add responds with a bare ID.
contactRef, err := b24.Ref("contact")
if err != nil {
return 0, err
}
if err := b.AddAs("requisite", "crm.requisite.add", b24.Params{
"fields": b24.Params{
"ENTITY_TYPE_ID": typeContact,
"ENTITY_ID": contactRef,
"PRESET_ID": presetID,
"NAME": "Primary requisite",
"ACTIVE": "Y",
},
}); err != nil {
return 0, err
}
requisiteRef, err := b24.Ref("requisite")
if err != nil {
return 0, err
}
// A single requisite can have several addresses, but no more than one
// of each type: a second crm.address.add with the same TYPE_ID will not go through.
// The address types are listed by crm.enum.addresstype: 1 — actual,
// 11 — a delivery address.
addresses := []struct {
cmd b24.CmdID
fields b24.Params
}{
{"address_actual", b24.Params{"TYPE_ID": 1, "ADDRESS_1": "Tverskaya Street, 7",
"CITY": "Berlin", "POSTAL_CODE": "125009", "COUNTRY": "Germany"}},
{"address_delivery", b24.Params{"TYPE_ID": 11, "ADDRESS_1": "Granatny Lane, 10",
"CITY": "Berlin", "POSTAL_CODE": "123001", "COUNTRY": "Germany"}},
}
for _, a := range addresses {
a.fields["ENTITY_TYPE_ID"] = typeRequisite
a.fields["ENTITY_ID"] = requisiteRef
if err := b.AddAs(a.cmd, "crm.address.add", b24.Params{"fields": a.fields}); err != nil {
return 0, err
}
}
// The commands are executed in the order they were ADDED, whatever their names are —
// this is exactly what makes the chain work.
res, err := core.CallBatch(ctx, b)
if err != nil {
return 0, fmt.Errorf("prepare the data in a batch: %w", err)
}
raw, err := res.Get("contact")
if err != nil {
return 0, err
}
var contactID b24.ID
return contactID, json.Unmarshal(raw, &contactID)
}
func firstPreset(ctx context.Context, core *b24.Core) (b24.ID, error) {
res, err := core.Call(ctx, "crm.requisite.preset.list", b24.Params{
"select": []string{"ID", "NAME"}, "order": b24.Params{"ID": "ASC"},
}, b24.WithIdempotent())
if err != nil {
return 0, fmt.Errorf("crm.requisite.preset.list: %w", err)
}
var presets []struct {
ID b24.ID `json:"ID"`
}
if err := json.Unmarshal(res.Result, &presets); err != nil {
return 0, err
}
if len(presets) == 0 {
return 0, fmt.Errorf("the portal has no requisite templates")
}
return presets[0].ID, nil
}
// del removes what was created. A cleanup error is printed but not returned: it must not
// mask the real error of the scenario.
func del(ctx context.Context, core *b24.Core, method string, params b24.Params) {
if _, err := core.Call(ctx, method, params); err != nil {
fmt.Fprintf(os.Stderr, "cleanup, %s: %v
Address from Custom Field
If a Bitrix24 administrator has created a custom field of type address for a contact, the address is stored directly in the contact and is not visible via methods crm.address.*. Such an address is retrieved in two steps.
- Find the field code using the crm.contact.userfield.list method with the filter
USER_TYPE_ID=address. This method is available only to an administrator. - Read the field value using the crm.contact.get method — it returns custom fields along with standard fields.
For a company, use crm.company.userfield.list and crm.company.get; for a lead, use crm.lead.userfield.list and crm.lead.get.
// 1. Searching for custom fields of type "address"
const fieldsResult = await $b24.actions.v2.call.make({
method: 'crm.contact.userfield.list',
params: {
filter: { USER_TYPE_ID: 'address' }
}
});
if (!fieldsResult.isSuccess) {
// Method is available only to the administrator
console.error(fieldsResult.getErrorMessages().join('; '));
} else {
const addressFields = fieldsResult.getData().result;
// 2. Reading the values of these fields for the contact
const contactResult = await $b24.actions.v2.call.make({
method: 'crm.contact.get',
params: { id: 2429 }
});
if (!contactResult.isSuccess) {
console.error(contactResult.getErrorMessages().join('; '));
} else {
const contact = contactResult.getData().result;
for (const field of addressFields) {
console.log(field.FIELD_NAME, contact[field.FIELD_NAME]);
}
}
}
// B24PhpSDK does not have a typed wrapper for crm.contact.userfield.list,
// therefore we call the method via the SDK core
try {
$fields = $sb->core->call(
'crm.contact.userfield.list',
[
'filter' => ['USER_TYPE_ID' => 'address']
]
)->getResponseData()->getResult();
$contact = $sb->core->call(
'crm.contact.get',
['id' => 2429]
)->getResponseData()->getResult();
foreach ($fields as $field) {
echo $field['FIELD_NAME'] . ': ' . print_r($contact[$field['FIELD_NAME']] ?? null, true) . PHP_EOL;
}
} catch (\Throwable $e) {
// crm.contact.userfield.list is available only to the administrator
echo 'Error: ' . $e->getMessage();
}
try:
fields = client.crm.contact.userfield.list(
filter={"USER_TYPE_ID": "address"},
).response.result
contact = client.crm.contact.get(bitrix_id=2429).response.result
except BitrixAPIError as error:
# crm.contact.userfield.list is available only to the administrator
print(f"Error: {error}")
else:
for field in fields:
print(field["FIELD_NAME"], contact.get(field["FIELD_NAME"]))
res, err := core.Call(ctx, "crm.contact.userfield.list", b24.Params{
"filter": b24.Params{"USER_TYPE_ID": "address"},
}, b24.WithIdempotent())
if err != nil {
// The method is available only to an administrator — this is no reason to abort the scenario
// with the addresses from the requisites, it has already run.
fmt.Fprintf(os.Stderr, "crm.contact.userfield.list: %v\n", err)
return nil
}
var fields []struct {
FieldName string `json:"FIELD_NAME"`
Multiple string `json:"MULTIPLE"`
}
if err := json.Unmarshal(res.Result, &fields); err != nil {
return fmt.Errorf("parse custom fields: %w", err)
}
res, err = core.Call(ctx, "crm.contact.get",
b24.Params{"id": contactID}, b24.WithIdempotent())
if err != nil {
return fmt.Errorf("crm.contact.get: %w", err)
}
for _, f := range fields {
// The shape of the value depends on MULTIPLE: for a multiple field it is an array.
raw, ok := b24.Unwrap(res.Result, f.FieldName)
if !ok || b24.IsEmpty(raw) {
fmt.Printf(" field %s (MULTIPLE=%s): not filled in\n", f.FieldName, f.Multiple)
continue
}
fmt.Printf(" field %s (MULTIPLE=%s): %s\n", f.FieldName, f.Multiple, raw)
}
In the response of the first request, we are interested in FIELD_NAME — the field code where the value is stored in the contact. Note MULTIPLE: the value format depends on it.
{
"result": [
{
"ID": "474",
"ENTITY_ID": "CRM_CONTACT",
"FIELD_NAME": "UF_CRM_1724412832",
"USER_TYPE_ID": "address",
"MULTIPLE": "N",
"MANDATORY": "N"
},
{
"ID": "475",
"ENTITY_ID": "CRM_CONTACT",
"FIELD_NAME": "UF_CRM_1724412960",
"USER_TYPE_ID": "address",
"MULTIPLE": "Y",
"MANDATORY": "N"
}
],
"total": 2
}
The value of such a field is a string consisting of three parts separated by the | character: the text address, coordinates via ;, and the address identifier in the location module. If MULTIPLE = Y, an array of such strings will be returned.
{
"result": {
"ID": "2429",
"UF_CRM_1724412832": "Granatny Lane, 10, Berlin, Berlin, Germany, 123001|55.761234;37.591234|571",
"UF_CRM_1724412960": [
"Tverskaya Street, 7, , Berlin, Berlin, Germany|;|575",
", , Berlin, Berlin, Germany|;|577"
]
}
}
The text part is constructed from the same components as a company details address, so unfilled components result in consecutive commas. Coordinates can also be empty — in that case, a single separator ; remains. Parse the string by | and do not assume that all three parts are filled.
Verify the Result
The scenario is successful if:
- in the
crm.requisite.listresponse, thetotalfield is greater than zero and there is at least oneIDinresult - in the
crm.address.listresponse, thetotalfield is greater than zero - in every address,
ANCHOR_TYPE_IDandANCHOR_IDmatch the type and identifier of the original customer — in the example,3and2429
Do not consider null in individual address fields as an error: unfilled components arrive empty even for a correctly created address.
You can verify the data in the interface. Open the contact or company card and expand the "Company details" field. The addresses from the method response should match the addresses in the card's company details. The address from the custom field is displayed in the card as a separate field, not within the company details.
Errors and Diagnostics
If the method returns an error or an empty result, check the request data.
Access denied.incrm.requisite.list— the user does not have permission to read the object specified inENTITY_TYPE_ID. Check the permissions to read contacts and companies in the CRM settings.Access denied.incrm.requisite.listwith valueENTITY_TYPE_ID1— Company details only exist for contacts and companies; step 1 is skipped for a lead.Access denied.incrm.address.list— the method requires permissions to read contacts, companies, and leads simultaneously. The error will appear even if a specific detail can be read.Access denied.incrm.contact.userfield.list— the method was called by someone other than an administrator. This does not mean the address does not exist — the field code is retrieved as an administrator once, and thereaftercrm.contact.getadministrator is not required.resultis empty incrm.requisite.list— the customer has no details; check the custom field. Also, ensure thatENTITY_TYPE_IDcorresponds to the object fromENTITY_ID. With type3and a company identifier, the method will return an empty list without an error.resultis empty incrm.address.list— the detail has no addresses, or addresses exist but of a different type. Repeat step 2 without theTYPE_IDfilter and ensure that8is passed inENTITY_TYPE_ID, and inENTITY_IDthe detail identifier from step 1 is passed, rather than the contact identifier.
Key Considerations
- The address is linked to a detail, not directly to a contact or a company. A single customer may have multiple details, so iterate through the entire list from step 1 instead of just the first item.
- A single detail may contain multiple addresses of different types. Without the
TYPE_IDfilter, the method will return all of them. - The method does not return a formatted address string. Construct it from the fields
ADDRESS_1,ADDRESS_2,CITY,POSTAL_CODE,REGION,PROVINCE, andCOUNTRY. - The field
COUNTRY_CODEis kept for backward compatibility and is not populated. - The fields
ANCHOR_TYPE_IDandANCHOR_IDare service fields; they are populated automatically when an address is added. - Linking an address directly to a contact or a company, bypassing the detail, is only possible in cases where the old address management mode was enabled by technical support. Do not rely on this link in new integrations.
- The two storage methods do not synchronize with each other. If an address is stored in a custom field, it will not appear in the details.