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

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

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…"
}

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ń

ZdarzenieWystępuje, gdyKierunek
message.receivedWiadomość 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.statusZmienia się stan dostarczenia jednej z Państwa wiadomości wychodzących.Tylko wychodzące.
connection.disconnectedPołą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.connectedPołą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

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 — stały identyfikator WASync (ta sama wartość, którą GET /messages zwraca jako id). 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 +.
  • text ma wartość null dla wiadomości zawierających tylko multimedia; media_url / media_type mają wartość null dla 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:

POST your webhook URL
{
  "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.

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)
});

Dostarczanie i zasady ponawiania

WłaściwośćWartość
Kryterium sukcesuDowolny kod 2xx. Wszystko inne, w tym przekierowania, jest błędem.
Próby3 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óbieZdarzenie jest odrzucane. Nie ma długiej kolejki ponownego dostarczania.
DuplikatyMożliwe. Deduplikujcie po message.id.
KolejnośćNiegwarantowana. Jeśli to istotne, porządkujcie według timestamp.
Podpis przy ponowieniachPonownie 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

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
}

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.events i 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 402 przy wysyłaniu.
  • Czy Państwa endpoint odpowiada kodem 2xx w ciągu 10 sekund? Wolne handlery wyglądają jak błędy i zużywają wszystkie trzy próby.
  • Czy to połączenie qr? Połączenia cloud_api nie wysyłają webhooków.
  • Zdarzenie message.received wywołują tylko wiadomości przychodzące — Państwa własne wysyłki nigdy go nie wywołują.
Czy ta strona była pomocna?

On this page