What's new, 21 October 2026: A customer who sends several messages before the bot's first reply no longer gets the opening sequence of a Chat Flow twice
WASync · Developers

Webhooks

Одна подписанная конечная точка на аккаунт. Проверка подписи, каталог событий, политика повторных попыток и ротация секрета.

Укажите WASync адрес своей конечной точки, и в ответ вы получите секрет для подписи. Используется whatsapp.events. Регистрировать приложение и давать отдельное согласие не нужно.

Один слот webhook на аккаунт

У аккаунта ровно один URL webhook. PUT /webhook задаёт его: повторный вызов с другим URL переносит конечную точку, а не добавляет вторую, и удалить её нельзя. Это важно, как только события понадобились нескольким получателям: триггер n8n и ваш собственный бэкенд не могут одновременно занимать этот слот. Раздавайте события дальше на своей стороне.

Задайте конечную точку

PUT /webhook
curl -X PUT https://developers.wasync.app/api/v1/webhook \
  -H "Authorization: Bearer $WASYNC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/wasync/webhook"}'
200 response
{
  "url": "https://example.com/wasync/webhook",
  "events": [
    "message.received",
    "message.status",
    "connection.disconnected",
    "connection.connected"
  ],
  "secret": "whsec_3f9a…"
}

Сохраните секрет. Он выдаётся здесь, потому что именно сейчас он нужен вам для кода проверки, а GET /webhook повторно его не вернёт: этот запрос сообщает { url, events, secretSet } — намеренно без секрета, ведь ответы на чтение попадают в логи и вставляются в тикеты. Потеряли секрет? Вызовите POST /webhook/rotate, чтобы получить новый. URL должен быть HTTPS на публично доступном хосте; http, localhost и приватные диапазоны отклоняются с ошибкой 400 invalid_webhook.

У ротации нет периода перекрытия: старый секрет перестаёт действовать в тот же момент, когда выдан новый. Разверните новый секрет быстро: политика повторных попыток ниже рассчитана на выкатку за секунды, а не за часы.

Каталог событий

СобытиеКогда срабатываетНаправление
message.receivedСообщение WhatsApp (текст или медиа) поступает на подключение, к которому выдан доступ.Только входящие: отправленные вами сообщения это событие не вызывают.
message.statusМеняется статус доставки одного из ваших исходящих сообщений.Только исходящие.
connection.disconnectedПодключение перестало работать, и его владельцу нужно выполнить сопряжение заново.Один раз за сбой, а не повторно, пока подключение остаётся нерабочим.
connection.connectedПодключение, которое перестало работать, снова работает.Один раз и только для подключения, о поломке которого вам сообщали.

Неизвестные значения event игнорируйте, а не считайте ошибкой: новые типы объявляются в журнале изменений до их выхода.

События срабатывают только для подключений qr. Подключения cloud_api сегодня webhook не отправляют; для них опрашивайте GET /messages и GET /connections/{id}.

Полезная нагрузка

POST your webhook URL
{
  "event": "message.received",
  "connection_id": "conn_8f3a21",
  "message": {
    "id": "cmqj3k2ab0001xyz",
    "wa_id": "[email protected]_3EB0A1B2C3",
    "from": "40700000000",
    "text": "Hello!",
    "media_url": null,
    "media_type": null,
    "timestamp": 1766138640000
  }
}
  • message.id — стабильный идентификатор WASync (то же значение, которое GET /messages возвращает как id). Дедуплицируйте и связывайте записи по нему: доставка выполняется как минимум один раз.
  • message.wa_id — собственный идентификатор WhatsApp, null, если он неизвестен. Только для поддержки и отладки, но не как ключ дедупликации или связывания.
  • from — только цифры в международном формате, без +.
  • text равен null у сообщений только с медиа; media_url / media_type равны null у текстовых сообщений или когда файл недоступен для публичной загрузки.
  • timestamp — время в эпохе в миллисекундах, числом, а не строкой ISO.

Доставка message.status обновляет одно из ваших исходящих сообщений:

POST your webhook URL
{
  "event": "message.status",
  "connection_id": "conn_8f3a21",
  "message": {
    "id": "cmqj3k2ab0001xyz",
    "wa_id": "[email protected]_3EB0A1B2C3",
    "status": "read",
    "timestamp": 1766138641000
  }
}

Проверьте подпись

Каждая доставка содержит X-WASync-Timestamp (эпоха в мс) и X-WASync-Signature: sha256=<hex>, то есть HMAC-SHA256(timestamp + "." + rawBody, secret). Вычисляйте её по сырому телу, до любого разбора JSON, и сравнивайте за постоянное время. Отклоняйте доставки, у которых метка времени старше примерно 5 минут: так блокируются повторные отправки.

verify.js
import crypto from "node:crypto";

// rawBody MUST be the exact bytes you received (verify BEFORE JSON.parse).
export function verifyWASync(headers, rawBody, secret) {
  const ts  = headers["x-wasync-timestamp"];
  const sig = headers["x-wasync-signature"];          // "sha256=<hex>"
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", secret)
          .update(`${ts}.${rawBody}`)
          .digest("hex");
  return Boolean(sig) &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
receiver (express)
import express from "express";
import { verifyWASync } from "./verify.js";

const app = express();

// Capture the RAW body — signature verification needs the exact bytes.
app.post("/webhooks/wasync", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyWASync(req.headers, req.body.toString("utf8"), process.env.WASYNC_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  // Reject stale deliveries (replay window) — 5 minutes is a sane tolerance.
  if (Math.abs(Date.now() - Number(req.headers["x-wasync-timestamp"])) > 5 * 60_000) {
    return res.status(401).end();
  }

  res.status(200).end(); // ACK fast — under 10s — then process async
  const { message } = JSON.parse(req.body.toString("utf8"));
  // ... your logic (dedupe on message.id — retries can deliver duplicates)
});

Доставка и политика повторных попыток

ПараметрЗначение
Критерий успехаЛюбой код 2xx. Всё остальное, включая перенаправления, считается сбоем.
Попытки3 на событие: сразу, затем через 500 мс, затем через 1 с.
Тайм-аут одной попытки10 секунд. Подтверждайте быстро, а обрабатывайте асинхронно.
После последнего сбояСобытие отбрасывается. Длинной очереди повторной доставки нет.
ДубликатыВозможны. Дедуплицируйте по message.id.
ПорядокНе гарантирован. Если он важен, упорядочивайте по timestamp.
Подпись при повторахПовторно отправляются то же тело и та же подпись: проверяйте каждую попытку одинаково.

Лестница статусов

Значения message.status образуют одностороннюю лестницу: sent (1) → delivered (2) → read (3); failed — конечный исход, который не наступает после read.

Применяйте обновления монотонно. Новый статус применяйте, только если его ранг выше сохранённого. Сообщение может прийти со статусом read раньше delivered (запоздавшая доставка), возможны и дубликаты событий. Никогда не возвращайте сообщение назад в своём интерфейсе.

Сверка: опрос как страховка

Доставка пробуется три раза и затем отбрасывается, поэтому ваш обработчик webhook — это быстрый путь, а не гарантия. Запустите фоновый цикл, который каждые 30–60 секунд опрашивает GET /messages?connectionId=… и добавляет или обновляет всё, чего обработчик не видел, используя курсор для инкрементальной выборки. Тот же цикл восполняет обновления статусов, пришедшие, пока ваша конечная точка была недоступна.

Webhooks — обновления интерфейса в реальном времени. Опрос — корректность данных. Используйте оба.

Жизненный цикл подключения

POST your webhook URL
{
  "event": "connection.disconnected",
  "connection_id": "conn_8f2a…",
  "connection": {
    "id": "conn_8f2a…",
    "phone_number": "393331234567",
    "label": "Studio Rossi",
    "status": "disconnected",
    "needs_reconnect": true,
    "license_status": "active",
    "license_expires": "2027-08-08T09:00:00.000Z",
    "reason": "AUTH_LOST"
  },
  "timestamp": 1786000000000
}

Подключение qr может перестать работать по причинам, от которых никто не зависит: телефон выключен на сутки, WhatsApp открыли на другом устройстве, сессия устарела. Тогда номер замолкает в обе стороны, и без события первым заметит это ваш клиент.

QR-кода в нагрузке намеренно нет. QR-код WhatsApp — это учётные данные для входа по сканированию: любой, кто его прочитает, может перехватить сессию. Событие сообщает, что нужно выполнить сопряжение заново: получите сам QR-код через аутентифицированный GET /connections/{id} и покажите его владельцу номера, как при первой настройке. Ориентируйтесь в интерфейсе на needs_reconnect; reason считайте необязательной подсказкой для журнала, так как могут появляться новые значения.

Задержка обнаружения: отключение выявляется 5-минутной проверкой статуса (~5 минут). Если сессию, судя по всему, можно восстановить, сначала предпринимается автоматическое восстановление, поэтому в таком случае проходит ~15–20 минут.

События не приходят?

  • URL доставки — это HTTPS на публичном хосте? Localhost, приватные IP и http:// отклоняются. На время разработки используйте туннель и проверьте, что именно сохранено, через GET /webhook.
  • Есть ли у учётных данных whatsapp.events и охватывают ли они подключение, на которое пришло сообщение?
  • Действует ли ещё лицензия подключения? Истёкшая лицензия означает отсутствие событий и ответ 402 при отправке.
  • Отвечает ли ваша конечная точка кодом 2xx в течение 10 секунд? Медленные обработчики выглядят как сбои и расходуют все три попытки.
  • Это подключение qr? Подключения cloud_api webhook не отправляют.
  • Событие message.received вызывают только входящие сообщения: ваши собственные отправки — никогда.
Была ли эта страница полезной?

On this page