Webhooks
Одна подписанная конечная точка на аккаунт. Проверка подписи, каталог событий, политика повторных попыток и ротация секрета.
Укажите WASync адрес своей конечной точки, и в ответ вы получите секрет для подписи. Используется
whatsapp.events. Регистрировать приложение и давать отдельное согласие не нужно.
Один слот webhook на аккаунт
У аккаунта ровно один URL webhook. PUT /webhook задаёт его: повторный вызов
с другим URL переносит конечную точку, а не добавляет вторую, и удалить её
нельзя. Это важно, как только события понадобились нескольким получателям:
триггер n8n и ваш собственный бэкенд не могут одновременно
занимать этот слот. Раздавайте события дальше на своей стороне.
Задайте конечную точку
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"}'{
"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}.
Полезная нагрузка
{
"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 обновляет одно из ваших исходящих сообщений:
{
"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 минут: так блокируются
повторные отправки.
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));
}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 — обновления интерфейса в реальном времени. Опрос — корректность данных. Используйте оба.
Жизненный цикл подключения
{
"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_apiwebhook не отправляют. - Событие
message.receivedвызывают только входящие сообщения: ваши собственные отправки — никогда.