xaxumessageAPI
События

Вебхуки: приём событий

Всё, что происходит с номерами и сообщениями, шлюз отправляет на ваш адрес POST-запросом, подписанным секретом. Те же события можно забирать списком.

Как приходит событие

POST /api/xaxumessage/events HTTP/1.1
Content-Type: application/json
User-Agent: xaxumessage/0.1
X-Xaxu-Event-Id: 1042
X-Xaxu-Event-Type: message.created
X-Xaxu-Timestamp: 1791625691
X-Xaxu-Signature: sha256=5d0c3e…

{"id":1042,"type":"message.created","createdAt":"2026-10-10T09:28:11.200Z","tenantId":"c3d4e5f6-1a2b-4c3d-8e9f-0a1b2c3d4e5f","accountId":"5b1e9c3a-8f2d-4c71-9a0e-2d6f1b7c4e10","data":{…}}
ПолеЧто это
idномер события, растёт монотонно — по нему убирайте дубли
typeтип события (все типы)
createdAtкогда событие произошло
tenantId, accountIdваш id клиента и номер, к которому относится событие
dataсодержимое — своё у каждого типа

Что должен делать ваш приёмник

  • Проверить подпись (ниже) и отклонить запрос с неверной подписью или старше 5 минут.
  • Ответить 2xx быстро — за 15 секунд. Долгую обработку делайте после ответа (очередь, фоновая задача).
  • **Убирать дубли по id**: доставка «хотя бы один раз», одно событие может прийти повторно.
  • Не полагаться на порядок: события могут прийти не по порядку. Статус сообщения не откатывается назад — сравнивайте по порядку статусов.

Подпись

X-Xaxu-Signature = sha256= + hex(HMAC-SHA256(secret, <X-Xaxu-Timestamp>.<сырое тело запроса>)). secret — секрет, выданный методом PUT /v1/webhook. Считайте подпись по точным байтам тела, а не по пересобранному JSON.

Node.js / Next.js
import { createHmac, timingSafeEqual } from 'node:crypto';

// rawBody — ровно те байты, что пришли (не JSON.stringify от разобранного объекта!)
export function verify(rawBody: string, headers: Headers, secret: string): boolean {
  const ts = headers.get('x-xaxu-timestamp');
  const sig = headers.get('x-xaxu-signature');
  if (!ts || !sig) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // старше 5 минут — отклонить

  const expected = 'sha256=' + createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(sig);
  return a.length === b.length && timingSafeEqual(a, b);
}

// Next.js (App Router): app/api/xaxumessage/events/route.ts
export async function POST(req: Request) {
  const raw = await req.text();
  if (!verify(raw, req.headers, process.env.XM_WEBHOOK_SECRET!)) return new Response('bad signature', { status: 401 });
  const event = JSON.parse(raw);
  // дедупликация по event.id, затем обработка; долгую работу — в очередь
  return new Response('ok');
}
PHP
<?php
$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_XAXU_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_XAXU_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, getenv('XM_WEBHOOK_SECRET'));

if (abs(time() - (int)$ts) > 300 || !hash_equals($expected, $sig)) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true);
// дедупликация по $event['id'], затем обработка
http_response_code(200);
Python
import hmac, hashlib, time

def verify(raw_body: bytes, headers, secret: str) -> bool:
    ts = headers.get('X-Xaxu-Timestamp', '')
    sig = headers.get('X-Xaxu-Signature', '')
    if not ts or abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b'.' + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest('sha256=' + mac, sig)

В TypeScript-клиенте это уже готово: verifyWebhookSignature() и readVerifiedEvent(request, secret).

Повторы

Если приёмник ответил не 2xx или не ответил за 15 секунд, шлюз повторяет доставку с растущей паузой (от 5 секунд до 1 часа), всего до 12 попыток — около 2,5 часов. Потом событие откладывается, оператор получает уведомление и может отправить отложенные заново. Ничего не теряется: всё доступно через GET /v1/events.

Надёжная схема: вебхук для мгновенной реакции + периодически GET /v1/events?after=<последний обработанный id> — чтобы догнать то, что пропустили, пока приёмник лежал.

Указать адрес для событий

PUT/v1/webhook
API-ключ клиентаУспех: 200

Куда шлюз будет отправлять события (POST). Если секрета подписи ещё нет и вы не передали свой, шлюз создаст его и вернёт один раз в поле secret — сохраните. При смене адреса секрет сохраняется.

url: null выключает отправку событий и удаляет секрет (при повторном включении будет выдан новый). События по-прежнему можно забирать методом GET /v1/events.

Тело запроса

urlurl | nullобязательно
адрес вашего приёмника (https)
secretstring ≥ 8
свой секрет подписи вместо сгенерированного
Запрос
curl -X PUT "https://msg.xaxu.am/v1/webhook" \
  -H "Authorization: Bearer $XM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.ru/api/xaxumessage/events"}'
Ответ 200
{
  "url": "https://example.ru/api/xaxumessage/events",
  "secret": "whsec_3q2Lw0…"
}

Забрать события списком

GET/v1/events
API-ключ клиентаУспех: 200

Все события клиента по порядку id. Подходит, чтобы догнать пропущенное после простоя приёмника или вовсе обходиться без вебхука: запоминайте next и передавайте его в after.

Параметры запроса

afterint
вернуть события с id больше этого (по умолчанию 0)
limitint 1…500
сколько (по умолчанию 100)
Запрос
curl "https://msg.xaxu.am/v1/events?after=1041&limit=100" \
  -H "Authorization: Bearer $XM_API_KEY"
Ответ 200
{
  "events": [
    {
      "id": 1042,
      "type": "message.created",
      "accountId": "5b1e9c3a-8f2d-4c71-9a0e-2d6f1b7c4e10",
      "createdAt": "2026-10-10T09:28:11.200Z",
      "data": {
        "channel": "whatsapp",
        "chatExternalId": "79181234567@c.us",
        "message": {
          "id": "0b8e4d2a-6c1f-4a3e-9b7d-5e2f1c0a9d8b",
          "accountId": "5b1e9c3a-8f2d-4c71-9a0e-2d6f1b7c4e10",
          "chatId": "9e2f7a1c-3b4d-4e5f-8a6b-7c8d9e0f1a2b",
          "externalId": "BAE5F4C1D2A3B4C5",
          "clientId": null,
          "direction": "in",
          "origin": "contact",
          "kind": "text",
          "text": "Здравствуйте! Есть свободные номера на выходные?",
          "data": {},
          "quotedExternalId": null,
          "sender": {
            "id": "79181234567@c.us",
            "name": "Анна",
            "phone": "79181234567"
          },
          "status": "received",
          "statusDetail": null,
          "sentAt": "2026-10-10T09:28:11.000Z",
          "editedAt": null,
          "deletedAt": null,
          "attachments": [],
          "reactions": []
        }
      }
    }
  ],
  "next": 1042
}