Добавить событие календаря для работы с клиентами
Scope:
crmКто может выполнять методы: чтобы пройти сценарий целиком, нужно самое строгое из перечисленных прав — «изменение контакта»
- crm.contact.get — пользователь с правом на чтение контактов
- crm.activity.add — пользователь с правом на изменение элемента CRM, для которого добавляется дело
Выберите инструмент для разработки с AI-агентом:
- используйте Битрикс24 Вайбкод, чтобы создать приложение для Битрикс24 по описанию задачи без знания языков программирования. Агент напишет код и разместит приложение на сервере без ручной настройки хостинга
- используйте MCP-сервер, чтобы разрабатывать интеграцию через REST API в своем проекте. Агент будет обращаться к официальной REST-документации
События календаря можно добавлять автоматически, чтобы напомнить сотрудникам о встречах или звонках клиентам. В карточке контакта появится дело типа «встреча», а Битрикс24 продублирует его событием в личном календаре ответственного сотрудника: название события возьмется из SUBJECT, границы — из START_TIME и END_TIME.
Ключевые параметры сценария — OWNER_TYPE_ID и TYPE_ID. OWNER_TYPE_ID определяет, в карточке какого объекта CRM появится дело, TYPE_ID — каким это дело будет. Событие в календаре создает только встреча, поэтому в TYPE_ID передаем 1.
Метод создания дела не принимает данные клиента сам: телефон для COMMUNICATIONS и ответственного для RESPONSIBLE_ID нужно сначала получить из карточки контакта. Поэтому сценарий состоит из двух шагов.
-
Получить телефон и ответственного методом crm.contact.get
-
Создать дело методом crm.activity.add, подставив полученные значения в
COMMUNICATIONSиRESPONSIBLE_ID
В результате метод вернет идентификатор дела, дело появится в таймлайне контакта, а событие — в календаре ответственного.
Что нужно до начала
-
контакт уже создан в Битрикс24, и вы знаете его идентификатор. Идентификатор возвращают методы crm.contact.list и crm.contact.add
-
у контакта заполнен телефон. Без коммуникации метод crm.activity.add дело не создаст и вернет ошибку
The field COMMUNICATIONS is not defined or invalid -
у контакта заполнено поле «Ответственный». Его идентификатор попадет в
RESPONSIBLE_ID, и именно в календаре этого сотрудника появится событие -
вебхук создан от имени пользователя, который может изменять этот контакт. Метод проверяет права не на дело, а на объект CRM, к которому дело привязывается
1. Получим данные клиента
Используем метод crm.contact.get с идентификатором клиента. Замените 1 на идентификатор своего контакта.
Как использовать примеры в документации
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
const contactID = 1;
const response = await $b24.actions.v2.call.make({
method: 'crm.contact.get',
params: { id: contactID },
requestId: 'contact-get'
})
const resultContact = response.getData().result;
from b24pysdk import BitrixWebhook, Client
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
contact_id = 1
result_contact = client.crm.contact.get(
bitrix_id=contact_id,
).response.result
// 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.ru/rest/USER_ID/TOKEN/');
$contactID = 1;
$resultContact = $sb->getCRMScope()->contact()->get($contactID)->contact();
// core, ctx и contactID объявлены в полном примере ниже
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)
}
// Из ответа нужны телефон и ответственный. PHONE — мультиполе: список
// объектов, даже когда номер один, и приходит он только если у контакта
// вообще есть телефоны.
var contact struct {
ID b24.ID `json:"ID"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
AssignedByID b24.ID `json:"ASSIGNED_BY_ID"`
Phone []struct {
ID b24.ID `json:"ID"`
Value string `json:"VALUE"`
ValueType string `json:"VALUE_TYPE"`
} `json:"PHONE"`
}
if err := json.Unmarshal(res.Result, &contact); err != nil {
return fmt.Errorf("разбор контакта: %w", err)
}
if len(contact.Phone) == 0 {
return fmt.Errorf("у контакта %d нет телефона", contactID)
}
В результате получим данные клиента. Для следующего шага сохраните два значения:
-
PHONE[0].VALUE— номер телефона. Это мультиполе: метод возвращает список объектов, даже если номер один, и не возвращает ключPHONEвовсе, если телефонов у контакта нет -
ASSIGNED_BY_ID— идентификатор ответственного сотрудника
Остальные поля ответа сценарию не нужны.
{
"result": {
"ID": "1",
"POST": "Исполнительный директор",
"COMMENTS": null,
"NAME": "Алексей",
"SECOND_NAME": "Кириллович",
"LAST_NAME": "Вронский",
"PHOTO": null,
"LEAD_ID": null,
"TYPE_ID": "SHARE",
"SOURCE_ID": "SELF",
"SOURCE_DESCRIPTION": null,
"COMPANY_ID": "52",
"BIRTHDATE": "",
"EXPORT": "Y",
"HAS_PHONE": "Y",
"HAS_EMAIL": "Y",
"HAS_IMOL": "N",
"DATE_CREATE": "2023-08-18T12:43:42+03:00",
"DATE_MODIFY": "2023-10-17T15:59:13+03:00",
"ASSIGNED_BY_ID": "61",
"CREATED_BY_ID": "57",
"MODIFY_BY_ID": "47",
"OPENED": "N",
"ORIGINATOR_ID": null,
"ORIGIN_ID": null,
"ORIGIN_VERSION": null,
"FACE_ID": null,
"LAST_ACTIVITY_TIME": "2025-03-15T10:38:21+02:00",
"ADDRESS": null,
"ADDRESS_2": null,
"ADDRESS_CITY": null,
"ADDRESS_POSTAL_CODE": null,
"ADDRESS_REGION": null,
"ADDRESS_PROVINCE": null,
"ADDRESS_COUNTRY": null,
"ADDRESS_LOC_ADDR_ID": null,
"UTM_SOURCE": null,
"UTM_MEDIUM": null,
"UTM_CAMPAIGN": null,
"UTM_CONTENT": null,
"UTM_TERM": null,
"LAST_ACTIVITY_BY": "1",
"PHONE": [
{
"ID": "1326",
"VALUE_TYPE": "MOBILE",
"VALUE": "88001001020",
"TYPE_ID": "PHONE"
}
],
"EMAIL": [
{
"ID": "1328",
"VALUE_TYPE": "WORK",
"VALUE": "vronsky@example.ru",
"TYPE_ID": "EMAIL"
}
]
},
"time": {
"start": 1747737934.888428,
"finish": 1747737934.945823,
"duration": 0.057394981384277344,
"processing": 0.029510021209716797,
"date_start": "2025-05-20T13:45:34+03:00",
"date_finish": "2025-05-20T13:45:34+03:00"
}
}
2. Создадим событие календаря
Чтобы создать дело и событие в календаре, используем метод crm.activity.add с параметрами:
-
SUBJECT— название дела, оно же станет названием события в календаре. Укажемcalendar title. Пустую строку метод не принимает -
DESCRIPTION— описание. Например,calendar body -
DESCRIPTION_TYPE— формат текста описания:1— обычный текст,2— HTML-разметка,3— BB-код. Зададим значение3 -
OWNER_ID— идентификатор объекта CRM, в карточке которого появится дело. ПередаемcontactID— идентификатор контакта из шага 1 -
OWNER_TYPE_ID— идентификатор типа объекта CRM. Передаем3— контакт. Полный список типов объектов возвращает метод crm.enum.ownertype -
TYPE_ID— тип дела. Укажем1— встреча. Метод принимает только1— встреча,2— звонок,4— письмо и6— дело внешнего провайдера. На других значениях он вернет ошибку -
COMMUNICATIONS— контактные данные клиента. Для встречи допустима ровно одна коммуникация:-
VALUE— номер телефона, беремPHONE[0].VALUEиз ответа шага 1 -
ENTITY_ID— идентификатор клиента, передаемcontactID -
ENTITY_TYPE_ID— идентификатор типа объекта, передаем3— контакт
-
Ключ TYPE в COMMUNICATIONS для встречи не передаем: Битрикс24 заполняет его автоматически только для звонков и писем, у встречи он остается пустым.
-
START_TIMEиEND_TIME— дата и время начала и окончания в формате ISO 8601. Эти же значения станут границами события в календаре, укажем длительность один час. Замените даты из примера на будущие: прошедшую встречу метод создаст, но напоминания по ней уже не будет -
RESPONSIBLE_ID— идентификатор ответственного, передаемASSIGNED_BY_IDиз ответа шага 1. Событие появится в личном календаре именно этого сотрудника
Поле COMPLETED в этом сценарии не передаем: встреча запланирована, а не завершена.
const contactPhone = resultContact.PHONE[0];
const response = await $b24.actions.v2.call.make({
method: 'crm.activity.add',
params: {
fields: {
"SUBJECT": "calendar title",
"DESCRIPTION": "calendar body",
"DESCRIPTION_TYPE": 3,
"OWNER_ID": contactID,
"OWNER_TYPE_ID": 3,
"TYPE_ID": 1,
"COMMUNICATIONS": [
{
'VALUE': contactPhone.VALUE,
'ENTITY_ID': contactID,
'ENTITY_TYPE_ID': 3
}
],
"START_TIME": "2025-05-20T14:00:00",
"END_TIME": "2025-05-20T15:00:00",
"RESPONSIBLE_ID": resultContact.ASSIGNED_BY_ID
}
},
requestId: 'activity-add'
});
contact_phone = result_contact["PHONE"][0]
response = client.crm.activity.add(
fields={
"SUBJECT": "calendar title",
"DESCRIPTION": "calendar body",
"DESCRIPTION_TYPE": 3,
"OWNER_ID": contact_id,
"OWNER_TYPE_ID": 3,
"TYPE_ID": 1,
"COMMUNICATIONS": [
{
"VALUE": contact_phone["VALUE"],
"ENTITY_ID": contact_id,
"ENTITY_TYPE_ID": 3,
}
],
"START_TIME": "2025-05-20T14:00:00",
"END_TIME": "2025-05-20T15:00:00",
"RESPONSIBLE_ID": result_contact["ASSIGNED_BY_ID"],
}
).response
$phones = $resultContact->PHONE;
$contactPhone = reset($phones);
$result = $sb->getCRMScope()->activity()->add(
[
"SUBJECT" => "calendar title",
"DESCRIPTION" => "calendar body",
"DESCRIPTION_TYPE" => 3,
"OWNER_ID" => $contactID,
"OWNER_TYPE_ID" => 3,
"TYPE_ID" => 1,
"COMMUNICATIONS" => [
[
'VALUE' => $contactPhone->VALUE,
'ENTITY_ID' => $contactID,
'ENTITY_TYPE_ID' => 3
]
],
"START_TIME" => "2025-05-20T14:00:00",
"END_TIME" => "2025-05-20T15:00:00",
"RESPONSIBLE_ID" => $resultContact->ASSIGNED_BY_ID,
]
);
// core, ctx и contact объявлены в полном примере ниже.
// Время начала и окончания — в формате ISO 8601. Здесь встреча на час,
// завтра в это же время.
start := time.Now().Add(24 * time.Hour)
res, err = core.Call(ctx, "crm.activity.add", b24.Params{
"fields": b24.Params{
"SUBJECT": "Встреча с клиентом",
"DESCRIPTION": "Обсудить условия поставки",
// 1 — обычный текст, 2 — HTML, 3 — BB-код.
"DESCRIPTION_TYPE": 3,
"OWNER_ID": contact.ID,
"OWNER_TYPE_ID": entityTypeContact,
// 1 — встреча; полный список типов дел отдаёт crm.enum.activitytype.
"TYPE_ID": 1,
// COMMUNICATIONS связывает событие с контактными данными клиента:
// значение берётся из мультиполя PHONE, полученного на шаге 1.
"COMMUNICATIONS": []b24.Params{{
"VALUE": contact.Phone[0].Value,
"ENTITY_ID": contact.ID,
"ENTITY_TYPE_ID": entityTypeContact,
}},
"START_TIME": start.Format(time.RFC3339),
"END_TIME": start.Add(time.Hour).Format(time.RFC3339),
"RESPONSIBLE_ID": contact.AssignedByID,
},
})
if err != nil {
return fmt.Errorf("crm.activity.add: %w", err)
}
// Обёртки нет: result — сразу идентификатор созданного дела.
var activityID b24.ID
if err := json.Unmarshal(res.Result, &activityID); err != nil {
return fmt.Errorf("разбор идентификатора события: %w", err)
}
Мы создали дело и в ответ получили его идентификатор 6915. Обертки в ответе нет: result — это сразу число. Идентификатор можно использовать в методах изменения и удаления дела.
{
"result": 6915
}
Проверим результат
Откройте карточку контакта в Битрикс24. Встреча отображается в таймлайне карточки. Откройте календарь сотрудника из RESPONSIBLE_ID — событие с названием из SUBJECT стоит на дату из START_TIME.
Через REST дела контакта возвращает метод crm.activity.list с теми же значениями OWNER_TYPE_ID и OWNER_ID, что и на шаге 2. Поле COMMUNICATIONS возвращается только тогда, когда оно указано в select.
const checkResponse = await $b24.actions.v2.call.make({
method: 'crm.activity.list',
params: {
filter: {
"OWNER_TYPE_ID": 3,
"OWNER_ID": contactID
},
select: ['*', 'COMMUNICATIONS'],
order: { ID: 'DESC' }
},
requestId: 'activity-list'
});
console.dir(checkResponse.getData().result);
activities = client.crm.activity.list(
filter={
"OWNER_TYPE_ID": 3,
"OWNER_ID": contact_id,
},
select=["*", "COMMUNICATIONS"],
order={"ID": "DESC"},
).response.result
// у crm.activity.list нет обертки в SDK — вызываем метод напрямую
$activities = $sb->core->call(
'crm.activity.list',
[
'filter' => [
'OWNER_TYPE_ID' => 3,
'OWNER_ID' => $contactID,
],
'select' => ['*', 'COMMUNICATIONS'],
'order' => ['ID' => 'DESC'],
]
)->getResponseData()->getResult();
Сценарий выполнен, если в ответе есть объект с ID из шага 2, у него TYPE_ID равен 1, а в COMMUNICATIONS лежит телефон клиента.
{
"result": [
{
"ID": "6915",
"OWNER_ID": "1",
"OWNER_TYPE_ID": "3",
"TYPE_ID": "1",
"SUBJECT": "calendar title",
"START_TIME": "2025-05-20T14:00:00+03:00",
"END_TIME": "2025-05-20T15:00:00+03:00",
"COMPLETED": "N",
"RESPONSIBLE_ID": "61",
"DESCRIPTION": "calendar body",
"DESCRIPTION_TYPE": "3",
"COMMUNICATIONS": [
{
"ID": "1204",
"TYPE": "",
"VALUE": "88001001020",
"ENTITY_ID": "1",
"ENTITY_TYPE_ID": "3"
}
]
}
],
"total": 1
}
В ответе числовые поля приходят строками — "TYPE_ID": "1", хотя в запросе передавалось число, а TYPE коммуникации у встречи пустой. Это не признаки ошибки.
Ошибки и диагностика
Если метод вернул ошибку, проверьте данные запроса.
|
Код |
Причина и действие |
|
|
У пользователя нет права на изменение контакта из |
|
|
Контакта с таким |
|
|
В |
|
|
Коммуникация не передана или отброшена. Так бывает, когда у контакта нет телефона и в |
|
|
Метод не смог определить ответственного: в |
|
|
В |
|
|
В |
|
|
Во встрече передано больше одной коммуникации. Оставьте один элемент в |
|
|
Не переданы ни |
Повторяйте сценарий с того шага, который вернул ошибку. Шаг 1 ничего не создает, его можно выполнять сколько угодно раз. Если ошибку вернул шаг 2, дело не создано: исправьте fields и повторите только его.
Отдельный случай — метод вернул идентификатор, дело в карточке контакта есть, а события в календаре нет. Проверьте три условия:
-
в
TYPE_IDпередана встреча1 -
вы смотрите календарь сотрудника из
RESPONSIBLE_ID, а не свой -
если в запросе было
COMPLETEDсо значениемY, проверьте настройки CRM. По умолчанию завершенные встречи в календаре сохраняются, но при выключенной настройке событие для них не создается
Что важно учитывать
-
дело другого типа события в календаре не создаст. Звонок
2и письмо4появятся только в таймлайне контакта -
событие создается в личном календаре сотрудника из
RESPONSIBLE_ID, а не у автора запроса. Если поле пропустить, метод подставит ответственного за объект CRM изOWNER_ID -
поле
DIRECTIONдля встреч не используется. Направление имеет смысл только у звонков и писем -
повторный запуск примера создает еще одно дело и еще одно событие, дубликаты не отсеиваются
-
у метода crm.activity.add остановлено развитие, но для этого сценария замены нет: только он создает дело типа «встреча» с синхронизацией в календарь. Метод crm.activity.todo.add создает дело другого типа — запланированное дело без типа «встреча»
Пример кода
Пример объединяет оба шага: читает контакт, берет из ответа телефон и ответственного и создает дело «Встреча» в карточке контакта и событие длительностью один час в календаре сотрудника. Замените contactID на идентификатор своего контакта, а SUBJECT и DESCRIPTION — на свой текст.
import { B24Hook } from '@bitrix24/b24jssdk'
const $b24 = B24Hook.fromWebhookUrl(process.env.B24_HOOK)
// B24_HOOK = 'https://your-domain.bitrix24.ru/rest/USER_ID/TOKEN/'
async function createCalendarActivity() {
try {
var contactID = 1;
const responseContact = await $b24.actions.v2.call.make({
method: 'crm.contact.get',
params: { id: contactID },
requestId: 'contact-get'
});
var resultContact = responseContact.getData().result;
if (resultContact.ASSIGNED_BY_ID && resultContact.PHONE) {
var contactPhone = resultContact.PHONE[0];
var staffID = resultContact.ASSIGNED_BY_ID;
await $b24.actions.v2.call.make({
method: 'crm.activity.add',
params: {
fields: {
"SUBJECT": "calendar title",
"DESCRIPTION": "calendar body",
"DESCRIPTION_TYPE": 3, // тип текста (crm.enum.contenttype): обычный, HTML, BB-код
"OWNER_ID": contactID,
"OWNER_TYPE_ID": 3, // crm.enum.ownertype
"TYPE_ID": 1, // crm.enum.activitytype
"COMMUNICATIONS": [
{
'VALUE': contactPhone.VALUE,
'ENTITY_ID': contactID,
'ENTITY_TYPE_ID': 3 // crm.enum.ownertype
}
],
"START_TIME": new Date().toISOString(),
"END_TIME": new Date(new Date().getTime() + 3600 * 1000).toISOString(),
"RESPONSIBLE_ID": staffID,
}
},
requestId: 'activity-add'
});
console.log(JSON.stringify({ 'message': 'Activity add' }));
} else {
console.log(JSON.stringify({ 'message': 'Activity not added' }));
}
} catch (error) {
console.error(error);
console.log(JSON.stringify({ 'message': 'Activity not added: ' + error.message }));
}
}
createCalendarActivity();
from datetime import datetime, timedelta
from b24pysdk import BitrixWebhook, Client
from b24pysdk.errors import BitrixAPIError
client = Client(
BitrixWebhook(
domain="your-domain.bitrix24.com",
webhook_token="user_id/webhook_key",
)
)
contact_id = 1
result_activity = None
try:
contact = client.crm.contact.get(bitrix_id=contact_id).response.result
if contact.get("ASSIGNED_BY_ID") and contact.get("PHONE"):
contact_phone = contact["PHONE"][0]
staff_id = contact["ASSIGNED_BY_ID"]
now = datetime.now()
result_activity = client.crm.activity.add(
fields={
"SUBJECT": "calendar title",
"DESCRIPTION": "calendar body",
"DESCRIPTION_TYPE": 3,
"OWNER_ID": contact_id,
"OWNER_TYPE_ID": 3,
"TYPE_ID": 1,
"COMMUNICATIONS": [
{
"VALUE": contact_phone["VALUE"],
"ENTITY_ID": contact_id,
"ENTITY_TYPE_ID": 3,
}
],
"START_TIME": now.isoformat(timespec="seconds"),
"END_TIME": (now + timedelta(hours=1)).isoformat(timespec="seconds"),
"RESPONSIBLE_ID": staff_id,
}
).response
except BitrixAPIError as error:
print({"message": f"Activity not added: {error}"})
else:
if result_activity and result_activity.result:
print({"message": "Activity add"})
else:
print({"message": "Activity not added"})
<?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.ru/rest/USER_ID/TOKEN/');
$contactID = 1;
try {
$resultContact = $sb->getCRMScope()->contact()->get($contactID)->contact();
$resultActivity = null;
if (!empty($resultContact->ASSIGNED_BY_ID) && !empty($resultContact->PHONE))
{
$phones = $resultContact->PHONE;
$contactPhone = reset($phones);
$staffID = $resultContact->ASSIGNED_BY_ID;
$resultActivity = $sb->getCRMScope()->activity()->add(
[
"SUBJECT" => "calendar title",
"DESCRIPTION" => "calendar body",
"DESCRIPTION_TYPE" => 3,// тип текста (crm.enum.contenttype): обычный, HTML, BB-код
"OWNER_ID" => $contactID,
"OWNER_TYPE_ID" => 3, // crm.enum.ownertype
"TYPE_ID" => 1, // crm.enum.activitytype
"COMMUNICATIONS" => [
[
'VALUE' => $contactPhone->VALUE,
'ENTITY_ID' => $contactID,
'ENTITY_TYPE_ID' => 3// crm.enum.ownertype
]
],
"START_TIME" => date("Y-m-d H:i:s", time()),
"END_TIME" => date("Y-m-d H:i:s", time() + 3600),
"RESPONSIBLE_ID" => $staffID,
]
)->getId();
}
if (!empty($resultActivity))
{
echo json_encode(['message' => 'Activity add']);
}
else
{
echo json_encode(['message' => 'Activity not added']);
}
} catch (\Throwable $e) {
echo json_encode(['message' => 'Activity not added: ' . $e->getMessage()]);
}
// Подготовка в пустом каталоге — go get без go mod init не сработает:
//
// go mod init example && go get github.com/bitrix24/b24gosdk
//
// Запуск:
//
// export B24_WEBHOOK_URL='https://ваш-портал.bitrix24.ru/rest/1/токен/' && go run .
//
// Пример самодостаточный: он создаёт контакт с телефоном, читает его данные,
// заводит событие календаря со ссылкой на этот контакт и убирает за собой.
// Запускается на любом портале, ничего править не нужно.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"time"
b24 "github.com/bitrix24/b24gosdk"
)
// entityTypeContact — идентификатор типа объекта «контакт» из crm.enum.ownertype.
const entityTypeContact = 3
func main() {
if err := run(context.Background()); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context) error {
// Путь вебхука — это секрет, поэтому он приходит из окружения, а не из кода.
core := b24.NewClient(os.Getenv("B24_WEBHOOK_URL")).Core()
// --- подготовка: свой контакт с телефоном
contactID, err := addContact(ctx, core)
if err != nil {
return err
}
defer del(ctx, core, "crm.contact.delete", b24.Params{"id": contactID})
// --- шаг 1: данные клиента
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)
}
// Из ответа нужны телефон и ответственный. PHONE — мультиполе: список
// объектов, даже когда номер один, и приходит он только если у контакта
// вообще есть телефоны.
var contact struct {
ID b24.ID `json:"ID"`
Name string `json:"NAME"`
LastName string `json:"LAST_NAME"`
AssignedByID b24.ID `json:"ASSIGNED_BY_ID"`
Phone []struct {
ID b24.ID `json:"ID"`
Value string `json:"VALUE"`
ValueType string `json:"VALUE_TYPE"`
} `json:"PHONE"`
}
if err := json.Unmarshal(res.Result, &contact); err != nil {
return fmt.Errorf("разбор контакта: %w", err)
}
if len(contact.Phone) == 0 {
return fmt.Errorf("у контакта %d нет телефона", contactID)
}
fmt.Printf("контакт %d %s %s, телефон %s, ответственный %d\n",
contact.ID, contact.Name, contact.LastName, contact.Phone[0].Value, contact.AssignedByID)
// --- шаг 2: событие календаря
// Время начала и окончания — в формате ISO 8601. Здесь встреча на час,
// завтра в это же время.
start := time.Now().Add(24 * time.Hour)
res, err = core.Call(ctx, "crm.activity.add", b24.Params{
"fields": b24.Params{
"SUBJECT": "Встреча с клиентом",
"DESCRIPTION": "Обсудить условия поставки",
// 1 — обычный текст, 2 — HTML, 3 — BB-код.
"DESCRIPTION_TYPE": 3,
"OWNER_ID": contact.ID,
"OWNER_TYPE_ID": entityTypeContact,
// 1 — встреча; полный список отдаёт crm.enum.activitytype.
"TYPE_ID": 1,
// COMMUNICATIONS связывает событие с контактными данными клиента:
// значение берётся из мультиполя PHONE, полученного на шаге 1.
"COMMUNICATIONS": []b24.Params{{
"VALUE": contact.Phone[0].Value,
"ENTITY_ID": contact.ID,
"ENTITY_TYPE_ID": entityTypeContact,
}},
"START_TIME": start.Format(time.RFC3339),
"END_TIME": start.Add(time.Hour).Format(time.RFC3339),
"RESPONSIBLE_ID": contact.AssignedByID,
},
})
if err != nil {
return fmt.Errorf("crm.activity.add: %w", err)
}
// Обёртки нет: result — сразу идентификатор созданного события.
var activityID b24.ID
if err := json.Unmarshal(res.Result, &activityID); err != nil {
return fmt.Errorf("разбор идентификатора события: %w", err)
}
defer del(ctx, core, "crm.activity.delete", b24.Params{"id": activityID})
fmt.Printf("событие %d создано на %s\n", activityID, start.Format("02.01.2006 15:04"))
return nil
}
// --- вспомогательное: подготовка данных и уборка
// addContact создаёт контакт с телефоном: страница берёт готовый контакт с
// идентификатором 1, но на чужом портале это другой человек или никого.
func addContact(ctx context.Context, core *b24.Core) (b24.ID, error) {
res, err := core.Call(ctx, "crm.contact.add", b24.Params{
"fields": b24.Params{
"NAME": "Алексей",
"LAST_NAME": "Вронский",
// Мультиполе: строка без ID ДОБАВЛЯЕТ значение. MultifieldAdd
// собирает её за вас, чтобы не путаться в ключах.
"PHONE": []map[string]any{
b24.MultifieldAdd("+7 800 100-10-20", "MOBILE"),
},
},
})
if err != nil {
return 0, fmt.Errorf("crm.contact.add: %w", err)
}
var id b24.ID
return id, json.Unmarshal(res.Result, &id)
}
// del убирает созданное. Ошибку уборки печатаем, но не возвращаем: она не
// должна подменить собой настоящую ошибку сценария.
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, "уборка, %s: %v\n", method, err)
}
}