Webhooks
Jeden podpisany endpoint na konto. Weryfikacja podpisu, katalog zdarzeń, zasady ponawiania i rotacja sekretu.
Wskażą Państwo WASync swój endpoint, a w odpowiedzi otrzymają klucz podpisu. Wymaga zakresu
whatsapp.events. Nie trzeba rejestrować aplikacji ani przechodzić kroku zgody.
Jedno miejsce na webhook na konto
Konto ma dokładnie jeden adres URL webhooka. PUT /webhook ustawia go — ponowne
wywołanie z innym adresem przenosi endpoint, zamiast dodawać drugi,
a usunięcie nie jest możliwe. Ma to znaczenie, gdy zdarzeń potrzebuje więcej niż jeden odbiorca:
wyzwalacz n8n i Państwa własny backend nie mogą jednocześnie zajmować
tego miejsca. Rozdzielanie zdarzeń trzeba zrealizować po Państwa stronie.
Ustawianie endpointu
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…"
}Zapiszcie Państwo sekret. Jest wydawany właśnie tutaj, ponieważ to moment, w którym jest potrzebny
do napisania kodu weryfikacji, a GET /webhook nie zwróci go ponownie —
ten endpoint zwraca { url, events, secretSet }, celowo bez sekretu,
ponieważ odczyty trafiają do logów i są wklejane do zgłoszeń. Zgubili Państwo sekret? Wywołajcie
POST /webhook/rotate, aby otrzymać nowy. Adres URL musi używać HTTPS i wskazywać publicznie dostępny host;
http, localhost i zakresy prywatne są odrzucane z kodem
400 invalid_webhook.
Rotacja nie ma okresu przejściowego: stary sekret przestaje działać w chwili wydania nowego. Należy szybko wdrożyć nowy sekret — opisane niżej zasady ponawiania obejmują wdrożenie liczone w sekundach, nie w godzinach.
Katalog zdarzeń
| Zdarzenie | Występuje, gdy | Kierunek |
|---|---|---|
message.received | Wiadomość WhatsApp dociera na udostępnione połączenie (tekst lub multimedia). | Tylko przychodzące — wiadomości wysyłane przez Państwa nie wywołują tego zdarzenia. |
message.status | Zmienia się stan dostarczenia jednej z Państwa wiadomości wychodzących. | Tylko wychodzące. |
connection.disconnected | Połączenie przestało działać i jego właściciel musi sparować je ponownie. | Raz na awarię, a nie wielokrotnie, dopóki problem trwa. |
connection.connected | Połączenie, które przestało działać, znów działa. | Raz, i tylko dla połączenia, o którego awarii Państwa poinformowano. |
Nieznane wartości event należy ignorować, a nie traktować jako błędy — nowe typy
są zapowiadane w dzienniku zmian przed wdrożeniem.
Zdarzenia są generowane wyłącznie dla połączeń qr. Połączenia cloud_api nie wysyłają dziś
żadnych webhooków; w ich przypadku należy odpytywać GET /messages i GET /connections/{id}.
Ładunek
{
"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— stały identyfikator WASync (ta sama wartość, którąGET /messageszwraca jakoid). Deduplikujcie i łączcie dane po nim — dostarczanie działa w trybie co najmniej raz.message.wa_id— własny identyfikator WhatsApp,null, gdy nieznany. Wyłącznie do wsparcia i debugowania, nigdy jako klucz deduplikacji ani łączenia.from— wyłącznie cyfry w formacie międzynarodowym, bez+.textma wartośćnulldla wiadomości zawierających tylko multimedia;media_url/media_typemają wartośćnulldla wiadomości tekstowych lub gdy plik nie jest publicznie dostępny.timestamp— czas epoki w milisekundach jako liczba, a nie ciąg ISO.
Zdarzenie message.status aktualizuje jedną z Państwa wiadomości wychodzących:
{
"event": "message.status",
"connection_id": "conn_8f3a21",
"message": {
"id": "cmqj3k2ab0001xyz",
"wa_id": "[email protected]_3EB0A1B2C3",
"status": "read",
"timestamp": 1766138641000
}
}Weryfikacja podpisu
Każde dostarczenie zawiera nagłówki X-WASync-Timestamp (epoka w ms) oraz
X-WASync-Signature: sha256=<hex>, czyli
HMAC-SHA256(timestamp + "." + rawBody, secret). Należy go obliczyć dla surowej
treści — przed jakimkolwiek parsowaniem JSON — i porównać w stałym czasie. Dostarczenia,
których znacznik czasu jest starszy niż ok. 5 minut, należy odrzucać, co blokuje ataki typu replay.
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)
});Dostarczanie i zasady ponawiania
| Właściwość | Wartość |
|---|---|
| Kryterium sukcesu | Dowolny kod 2xx. Wszystko inne, w tym przekierowania, jest błędem. |
| Próby | 3 na zdarzenie — natychmiast, następnie po 500 ms, a potem po 1 s. |
| Limit czasu na próbę | 10 sekund. Potwierdzajcie szybko i przetwarzajcie asynchronicznie. |
| Po ostatniej nieudanej próbie | Zdarzenie jest odrzucane. Nie ma długiej kolejki ponownego dostarczania. |
| Duplikaty | Możliwe. Deduplikujcie po message.id. |
| Kolejność | Niegwarantowana. Jeśli to istotne, porządkujcie według timestamp. |
| Podpis przy ponowieniach | Ponownie wysyłana jest identyczna treść i identyczny podpis — każdą próbę weryfikujcie w ten sam sposób. |
Drabina statusów
Wartości message.status tworzą jednokierunkową drabinę: sent (1) → delivered (2) →
read (3), przy czym failed jest wynikiem końcowym, który nie następuje po read.
Stosujcie aktualizacje monotonicznie. Nowy status należy zastosować tylko wtedy, gdy jego ranga jest wyższa
niż ranga zapisanego. Wiadomość może otrzymać read przed
delivered (opóźnione dostarczenie), możliwe są też zduplikowane zdarzenia. Nigdy nie cofajcie
wiadomości do wcześniejszego statusu w interfejsie.
Uzgadnianie — odpytywanie jako siatka bezpieczeństwa
Dostarczenie jest próbowane trzykrotnie, a następnie odrzucane, dlatego Państwa handler webhooka to
szybka ścieżka, a nie gwarancja. Uruchomcie pętlę w tle, która co 30–60 sekund odpytuje
GET /messages?connectionId=… i zapisuje (upsert) wszystko, czego handler
nie widział, używając kursora do pobierania przyrostowego. Ta sama pętla uzupełnia
aktualizacje statusów, które nadeszły, gdy Państwa endpoint był niedostępny.
Webhooki = aktualizacje interfejsu w czasie rzeczywistym. Odpytywanie = poprawność danych. Uruchomcie oba mechanizmy.
Cykl życia połączenia
{
"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
}Połączenie qr może przestać działać z powodów niezależnych od kogokolwiek — telefon
jest wyłączony przez dobę, WhatsApp zostaje otwarty na innym urządzeniu, sesja
wygasa. Numer przestaje wtedy działać w obu kierunkach, a bez zdarzenia
pierwszą osobą, która to zauważy, będzie Państwa klient.
Kodu QR celowo nie ma w ładunku. Kod QR WhatsApp to dane logowania
po zeskanowaniu: każdy, kto go odczyta, może przejąć sesję. Zdarzenie informuje,
że potrzebne jest ponowne sparowanie — pobierzcie sam kod QR przez uwierzytelnione
GET /connections/{id} i pokażcie go właścicielowi numeru, dokładnie tak jak
podczas pierwszej konfiguracji. Interfejs opierajcie na needs_reconnect; reason traktujcie
jako doradczą wskazówkę do logów, ponieważ mogą pojawiać się nowe wartości.
Opóźnienie wykrycia: rozłączenie jest wykrywane przez sondę statusu co 5 minut (~5 min). Gdy sesja wygląda na możliwą do odzyskania, najpierw podejmowana jest automatyczna próba naprawy, więc ta ścieżka trwa ok. 15–20 minut.
Nie otrzymują Państwo zdarzeń?
- Czy adres URL dostarczania używa HTTPS i wskazuje publiczny host? Localhost, prywatne adresy IP i
http://są odrzucane. W trakcie developmentu użyjcie tunelu i sprawdźcie, co jest faktycznie zapisane, za pomocąGET /webhook. - Czy dane uwierzytelniające mają zakres
whatsapp.eventsi czy obejmują połączenie, na które dotarła wiadomość? - Czy licencja połączenia jest nadal aktywna? Wygasła licencja oznacza brak zdarzeń
i kod
402przy wysyłaniu. - Czy Państwa endpoint odpowiada kodem
2xxw ciągu 10 sekund? Wolne handlery wyglądają jak błędy i zużywają wszystkie trzy próby. - Czy to połączenie
qr? Połączeniacloud_apinie wysyłają webhooków. - Zdarzenie
message.receivedwywołują tylko wiadomości przychodzące — Państwa własne wysyłki nigdy go nie wywołują.