Webhook
Webhook — это HTTP POST, который система отправляет на ваш URL при наступлении события. Эта страница описывает актуальный формат Payload v2. Payload v1 продолжает поддерживаться и доступен через переключатель версии.
Успешной доставкой считается любой ответ со статусом 200-299.
Формат payload v2
Версия задаётся полем payloadVersion: "2" подписки. Выбранная версия фиксируется вместе с телом в
момент постановки события в очередь, поэтому изменение подписки не меняет уже созданные доставки.
Переключатель в заголовке меняет только открытую страницу документации.
Если выбор v2 в кабинете пока недоступен, окружение находится на expand-этапе: workers уже умеют доставлять v2, но создание и перевод подписок на него откроются только после activation.
| Событие | Тело | Подпись |
|---|---|---|
| Обычное | { id, type, apiVersion: "v2", occurredAt, data } | APX-подпись |
crm_change | Structured envelope с теми же верхнеуровневыми полями и CRM data | APX-подпись |
Для обычного события тело имеет вид:
{
"id": "811793c8-d361-487b-b3f7-1d59b3fe3fc6",
"type": "create_message",
"apiVersion": "v2",
"occurredAt": "2026-08-21T10:30:00.000Z",
"data": {}
}id и occurredAt одинаковы у всех v2-получателей одного обычного события и не меняются при
повторной доставке. occurredAt — время постановки события, а не время конкретной HTTP-попытки.
Событие crm_change использует тот же structured envelope, но содержит контракт изменений CRM:
{
"id": "811793c8-d361-487b-b3f7-1d59b3fe3fc6",
"type": "crm.deal.updated",
"apiVersion": "v2",
"occurredAt": "2026-08-21T10:30:00.000Z",
"data": {
"resourceType": "deal",
"resourceId": "9ba2e2f7-86b4-4dc1-b2d0-e6423c361d79",
"externalId": "order-42",
"action": "updated",
"version": 7,
"changedFields": ["amount", "stageUuid"]
}
}Для crm_change необходимо выбрать API-приложение со scope чтения соответствующего CRM-ресурса.
externalId отсутствует, если выбранное приложение не создавало reference для ресурса.
Подпись (secret)
Для любого payload v2 secret обязателен. Каждый запрос содержит:
X-APX-Event-Id: 811793c8-d361-487b-b3f7-1d59b3fe3fc6
X-APX-Event-Timestamp: 1787308200
X-APX-Signature: v1=<hex hmac-sha256>Подписывается точная строка <timestamp>.<rawBody>. Префикс v1= обозначает версию алгоритма
подписи, а не payload. Проверяйте подпись до JSON parsing, ограничивайте допустимый возраст
timestamp и дедуплицируйте события по X-APX-Event-Id. Timestamp создаётся заново на каждой
попытке, но raw body и event ID остаются прежними.
import { createHmac, timingSafeEqual } from 'crypto';
function verifyApxSignature(bodyRaw: string, timestamp: string, signature: string, secret: string) {
const expected = createHmac('sha256', secret).update(`${timestamp}.${bodyRaw}`).digest('hex');
const received = signature.startsWith('v1=') ? signature.slice(3) : '';
if (!/^[a-f0-9]{64}$/i.test(received)) return false;
return timingSafeEqual(Buffer.from(received, 'hex'), Buffer.from(expected, 'hex'));
}Базовая типизация v2
type GenericWebhookEventType =
| 'create_message'
| 'edit_message'
| 'delete_message'
| 'create_chat'
| 'close_chat'
| 'open_chat'
| 'transfer_chat'
| 'create_deal'
| 'move_deal'
| 'close_deal'
| 'delete_deal'
| 'create_pipeline'
| 'update_pipeline'
| 'delete_pipeline'
| 'create_crm_tag'
| 'update_crm_tag'
| 'delete_crm_tag'
| 'create_client'
| 'update_client';
type WebhookEnvelopeV2<T> = {
id: string;
type: GenericWebhookEventType;
apiVersion: 'v2';
occurredAt: string;
data: T;
};
type CrmChangeWebhookEnvelopeV2 = {
id: string;
type: `crm.${string}.${CrmChangeAction}`;
apiVersion: 'v2';
occurredAt: string;
data: {
resourceType: string;
resourceId: string | null;
externalId?: string;
action: CrmChangeAction;
version: number | null;
changedFields: string[];
};
};
type CrmChangeAction =
| 'created'
| 'updated'
| 'deleted'
| 'merged'
| 'anonymized'
| 'completed'
| 'failed';Повторные попытки
- Повторяются только неуспешные доставки.
- До 5 попыток всего; перед повторами используются задержки
1m,2m,4m,8m. - Обработчик должен быть идемпотентным: сетевой сбой после успешной обработки может привести к повтору.
Повторные события одного объекта
create_message и delete_message приходят один раз на сообщение. Остальные события приходят на каждое изменение, даже если объект тот же: open_chat, close_chat и transfer_chat — при каждом открытии, закрытии и передаче диалога, edit_message — при каждой правке, события сделок, воронок, тегов и клиентов — при каждом изменении. Раньше повторное событие того же диалога, сделки или клиента могло не доставляться.
Не отбрасывайте такие события как дубли по data.id: у повторного open_chat тот же id диалога. Повтор доставки одного и того же события в v2 определяйте по id события и заголовку X-APX-Event-Id.
Список событий
- Messages:
create_message,edit_message,delete_message - Deals:
create_deal,move_deal,close_deal,delete_deal - Pipelines:
create_pipeline,update_pipeline,delete_pipeline - CRM Tags:
create_crm_tag,update_crm_tag,delete_crm_tag - Clients:
create_client,update_client - Chats:
create_chat,close_chat,transfer_chat,open_chat - CRM API:
crm_change
Типы data для обычных событий
create_message
type CreateMessageData = {
id: string;
chatUuid: string;
content: string;
type: 'system' | 'default';
senderType: 'client' | 'operator';
senderUuid: string;
isUpdated: boolean;
isSpam: boolean;
isEmailSent: boolean;
answerMessageUuid: string | null;
socialMessageUuid: string | null;
aiOperatorMeta: any | null;
createdAt: string;
updatedAt: string;
};edit_message
type EditMessageData = CreateMessageData;delete_message
type DeleteMessageData = CreateMessageData & { deletedAt: string };create_chat
type CreateChatData = {
id: string;
clientUuid: string;
operatorUuid: string;
channelUuid: string;
departmentUuid: string | null;
transfersUuid: string[];
status: 'active' | 'closed';
subject?: string | null;
providerPeerId?: string | null;
avitoItem?: {
id: string;
title?: string;
price?: string;
url?: string;
image?: string;
} | null;
createdAt: string;
updatedAt: string;
};providerPeerId — идентификатор собеседника в мессенджере, на который APX отправляет ответы в этом
диалоге: например, chat_id в Telegram, peer_id во VK, JID в WhatsApp, chat:<id> или
user:<id> у бота MAX, chat_id чата Avito. Формат зависит от канала, поэтому используйте значение
как непрозрачную строку. Адресат закрепляется за диалогом при его создании и сохраняется при
объединении клиентов. null — если канал не адресует собеседника таким идентификатором (сайт,
почта) или диалог создан до появления поля. Поле добавлено обратно совместимо: оно приходит в
create_chat, close_chat, open_chat и transfer_chat в payload v1 и v2 без смены версии.
avitoItem — объявление Avito, по которому идёт диалог. В Avito чат — это пара «покупатель +
объявление», поэтому у покупателя, написавшего по двум объявлениям, будет два диалога, каждый со
своим providerPeerId и avitoItem. price — строка цены от Avito вместе с валютой. null или
поле отсутствует — канал не Avito, объявление не удалось получить или диалог создан до появления
поля (до октября 2026 года у диалогов Avito providerPeerId — числовой id покупателя). Поле
добавлено обратно совместимо, без смены версии payload.
close_chat
type CloseChatData = CreateChatData & { closedAt: string };open_chat
type OpenChatData = CreateChatData & {
openedAt: string;
/** Номер обращения в диалоге: 1, 2, 3…; после «Вернуться в группу» — номер продолжающегося обращения */
episode: number | null;
/** Почему диалог снова открыт */
reason: 'reopened_inbound' | 'reopened_manual' | 'reopened_api' | null;
};open_chat приходит, когда закрытый диалог снова становится активным. reason показывает причину:
reopened_inbound— клиент написал в закрытый диалог;reopened_manual— диалог возобновил сотрудник: в APX или приложением Messaging API «Рабочее место сотрудника» перед отправкой в закрытые почту, Avito или сайт, — или сотрудник вернулся в группу («Вернуться в группу»): обращение группы продолжается,episodeпрежний;reopened_api— приложение-автоматизация Messaging API отправило сообщение в закрытый диалог, и он вернулся в работу по правилам канала.
Если номер или причину определить не удалось, поле равно null.
Сообщение сотрудника в закрытый диалог 1:1 мессенджера — из «Закрытых» в APX или приложением
«Рабочее место сотрудника» — диалог не открывает: он остаётся закрытым, и open_chat не приходит.
Когда клиент ответит, придёт open_chat с reason: "reopened_inbound", а диалог вернётся к
ответственному, если тот доступен, иначе — по правилам канала. Подробнее — в разделе
Messaging API.
В мессенджерах вернувшийся клиент продолжает тот же диалог: вместо create_chat с новым id придёт
open_chat с прежним id, следующим номером episode и reason: "reopened_inbound". Если
интеграция создавала запись на каждый create_chat, обрабатывайте и такой open_chat: это новое
обращение в том же диалоге. Почта, Avito и групповые чаты по-прежнему создают новый диалог и
присылают create_chat. Сводка изменений — в разделе
Изменения 29.09.2026 (v1).
transfer_chat
type TransferChatData = CreateChatData & {
previousOperatorUuid: string;
newOperatorUuid: string;
transferredAt: string;
};create_deal
type CreateDealData = {
id: string;
title: string;
description: string | null;
amount: number;
currency: string;
status: 'active' | 'won' | 'lost' | 'paused';
priority: 'low' | 'medium' | 'high' | 'urgent';
source: string;
expectedCloseDate: string | null;
actualCloseDate: string | null;
lossReason: string | null;
customFields: object | null;
clientUuid: string | null;
ownerUuid: string;
pipelineUuid: string;
stageUuid: string;
lostReasonCategoryUuid: string | null;
chatUuid: string | null;
rootChanelUuid: string;
createdAt: string;
updatedAt: string;
};delete_deal
type DeleteDealData = CreateDealData & { deletedAt: string };move_deal
type MoveDealData = CreateDealData & {
previousStageUuid: string;
newStageUuid: string;
movedAt: string;
};close_deal
type CloseDealData = CreateDealData & { closedAt: string };create_pipeline
type CreatePipelineData = {
id: string;
name: string;
description: string | null;
color: string;
isActive: boolean;
isDefault: boolean;
sortOrder: number;
rootChanelUuid: string;
createdAt: string;
updatedAt: string;
};update_pipeline
type UpdatePipelineData = CreatePipelineData;delete_pipeline
type DeletePipelineData = CreatePipelineData & { deletedAt: string };create_crm_tag
type CreateCrmTagData = {
id: string;
name: string;
color: string;
description: string | null;
type: 'client' | 'deal' | 'universal';
isActive: boolean;
usageCount: number;
rootChanelUuid: string;
createdAt: string;
updatedAt: string;
};update_crm_tag
type UpdateCrmTagData = CreateCrmTagData;delete_crm_tag
type DeleteCrmTagData = CreateCrmTagData & { deletedAt: string };create_client
type CreateClientData = {
id: string;
type: 'site' | 'telegram' | 'mail' | 'whatsapp' | 'vk' | 'max';
rootChanelUuid: string;
socialId: number | null;
nameSocial: string | null;
lastName: string | null;
name: string | null;
email: string[];
avatarUrl: string | null;
phone: string[];
ip: string[];
city: string[];
isOnline: boolean;
isContactRequestPending: boolean;
otherFields: object | null;
lastVisit: string;
createdAt: string;
updatedAt: string;
};update_client
type UpdateClientData = CreateClientData;Поля в data могут расширяться обратно-совместимо. Не завязывайтесь на строго фиксированный набор полей без необходимости.