Webhooks
Amobit Chats отправляет события в ваш HTTPS-эндпоинт асинхронно, с повторами и подписью HMAC-SHA256. Событие содержит диалог, контакт, сообщение и профиль канала — этого достаточно, чтобы создать обращение в CRM без дополнительных запросов.
Событие message.created
Событие создаётся для входящего сообщения от клиента и для исходящего из кабинета, API или коннектора. Событие для исходящего сообщения формируется после того, как отправка подтверждена локально, поэтому ваше собственное сообщение тоже может прийти в webhook. Внутренние заметки операторов во внешнюю систему не отправляются.
Фильтруйте по message.direction. incoming — от клиента, outgoing — ответ из Amobit. Служебные сообщения с direction: internal в вебхуки не попадают.
| Поле верхнего уровня | Смысл |
|---|---|
event | Всегда message.created. |
event_version | Версия схемы события. Текущая — 2026-07-03. Сравнивайте с ожидаемой и не падайте на неизвестной версии. |
workspace | Компания: id, slug, name. |
thread | Диалог: id, канал, external_chat_id, имя/телефон, группа, статус, непрочитанные, updated_at. |
contact | Контакт Amobit: id, отображаемое имя, заметки. |
message | Сообщение: направление, тип, текст, автор, статус доставки, идентификаторы провайдера, время. |
channel_profile | Подключённый канал: id, имя, тип, статус. |
attachments | Массив вложений: тип, имя, MIME, размер, ссылки, размеры, длительность. |
Доставка и повторы
- Доставка асинхронная через очередь: webhook не задерживает обработку сообщения в Amobit.
- Семантика at-least-once в пределах 8 попыток: событие может прийти дважды, поэтому отбрасывайте повторы по
X-Amobit-Chats-Idempotency-Keyилиmessage.id. Если все попытки неудачны, доставка прекращается — успешной доставки может не быть вовсе. - Успех — любой ответ
2xx. Таймаут ответа — 10 секунд. - При ошибке, таймауте или не-
2xxвыполняется до 8 попыток с нарастающей паузой: 1, 2, 4, 8, 16, 32 и 60 минут (около 2 часов суммарно). - После исчерпания попыток доставка помечается
dead. Автоматического или ручного повтора послеdeadнет — событие остаётся только в истории доставок, которую можно получить через APIGET /api/v1/external-integrations/workspaces/{workspace_slug}/deliveries/(сессия, последние 100 записей, без payload). В карточке интеграции этот журнал пока не отображается. - Редиректы не выполняются:
3xxсчитается ошибкой доставки. - Порядок событий не гарантируется. Сортируйте по
message.sent_atи используйтеmessage.idкак ключ. - Отключение интеграции переводит ожидающие доставки в
deadи останавливает отправку.
Заголовки запроса
| Заголовок | Пример | Назначение |
|---|---|---|
X-Amobit-Chats-Event | message.created | Тип события. |
X-Amobit-Chats-Event-Id | f09de45d-… | ID сообщения (стабильный идентификатор события). |
X-Amobit-Chats-Delivery-Id | 6a1c… | ID доставки события. Не меняется между попытками; счётчика попыток в заголовках нет. |
X-Amobit-Chats-Workspace | client-workspace | Slug компании. |
X-Amobit-Chats-Timestamp | 1786010487 | Unix-время подписи. Меняется при каждой попытке. |
X-Amobit-Chats-Signature-Version | hmac-sha256-v1 | Алгоритм подписи. |
X-Amobit-Chats-Signature | sha256=… | HMAC-SHA256 от {timestamp}.{raw_body}. |
X-Amobit-Chats-Idempotency-Key | external:webhook:… | Ключ дедупликации на стороне Amobit. |
Проверка подписи
Строка подписи — {timestamp}.{raw_body}: значение заголовка X-Amobit-Chats-Timestamp, точка, затем сырое тело запроса в байтах. Ключ — секрет, заданный в настройках интеграции. Сравнивайте хеши за постоянное время.
Не пересобирайте JSON. Подпись считается по исходным байтам. Если распарсить тело и сериализовать заново, порядок ключей и пробелы изменятся, и проверка ложно провалится. Храните raw body до проверки подписи.
Node.js
import crypto from "node:crypto";
export function verifyAmobitSignature({ rawBody, timestamp, signature, secret }) {
const expected =
"sha256=" +
crypto.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express: нужен raw body, не JSON-парсер
// app.post("/amobit/webhook", express.raw({ type: "application/json" }), handler)
PHP
$raw = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_AMOBIT_CHATS_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_AMOBIT_CHATS_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, getenv('AMOBIT_WEBHOOK_SECRET'));
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit;
}
Python (Django / FastAPI)
import hmac, hashlib
def verify(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
signed = timestamp.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
# Django: request.body — сырые байты; не вызывайте request.json() до проверки.
# FastAPI: await request.body() до разбора.
Дополнительно рекомендуется:
- Отклонять запросы со
X-Amobit-Chats-Timestampстарше 5 минут (защита от повторного воспроизведения). - Дедуплицировать по
X-Amobit-Chats-Idempotency-Keyв пределах 24 часов. - Не логировать полное тело с персональными данными; хранить
event_id,message.idи статус обработки.
Payload
{
"event": "message.created",
"event_version": "2026-07-03",
"workspace": {
"id": "0d2b5e7c-0000-4000-9000-000000000001",
"slug": "client-workspace",
"name": "Client Workspace"
},
"thread": {
"id": "9fd48a99-f5f3-4c3d-8ca1-1f0e047d5b25",
"channel": "whatsapp",
"external_chat_id": "79990001122@c.us",
"display_name": "Client",
"phone": "79990001122",
"contact_name": "Client",
"is_group": false,
"status": "open",
"unread_count": 1,
"updated_at": "2026-10-20T12:00:00+00:00"
},
"contact": {
"id": "3c16c8d6-0000-4000-9000-000000000002",
"display_name": "Client",
"notes": ""
},
"message": {
"id": "f09de45d-0000-4000-9000-000000000003",
"direction": "incoming",
"message_type": "text",
"text": "Здравствуйте",
"preview_text": "Здравствуйте",
"sender_name": "Client",
"delivery_status": "delivered",
"provider_message_id": "provider-message-1",
"provider_transport_id": "",
"source": "",
"sent_at": "2026-10-20T12:00:00+00:00",
"created_at": "2026-10-20T12:00:01+00:00",
"updated_at": "2026-10-20T12:00:01+00:00"
},
"channel_profile": {
"id": "6c7b41f5-0000-4000-9000-000000000004",
"name": "WhatsApp Sales",
"channel": "whatsapp",
"status": "connected",
"support_code": ""
},
"attachments": []
}
| Поле | Тип | Примечание |
|---|---|---|
message.direction | enum | incoming — от клиента, outgoing — из Amobit, internal — служебное. |
message.message_type | enum | text, image, video, audio, ptt, document, file, location, reaction, internal_note. |
message.delivery_status | enum | pending, sent, delivered, read, failed. |
message.source | string | Источник, если известен. Для сообщений, отправленных через этот API, — external_api: удобно, чтобы не зациклить обработку собственных ответов. |
message.text | string | Текст или подпись к медиа; может быть пустым для служебных типов. |
attachments[] | array | id, kind, file_name, mime_type, size_bytes, share_url, source_url, storage_status, duration_ms, width, height. Ссылки могут быть пустыми, если файл ещё не сохранён. |
Как отвечать
- Верните
2xxв течение 10 секунд. Тяжёлую обработку вынесите в очередь. - Не-
2xxзапускает повтор всей доставки — операция должна быть идемпотентной. - Если ваш сервис недоступен, ничего не делайте: повтор произойдёт автоматически.
- После восстановления проверьте, какие события ушли в
dead: черезGET …/deliveries/с сессией владельца/администратора или через поддержку. В карточке интеграции журнал пока не отображается.
Ограничения
- Поддерживается одно событие —
message.created. Отдельных событий статуса доставки, редактирования или удаления сообщения нет. - Webhook-адрес — только публичный HTTPS. Localhost, приватные и непубличные адреса отклоняются при сохранении и перепроверяются при каждой попытке.
- Редиректы запрещены; цепочка
3xxне поддерживается. - Секрет подписи — не короче 16 символов. Если секрет не задан, заголовок подписи не отправляется; включать интеграцию без секрета нельзя.