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

Ein signierter Endpunkt pro Konto. Signaturprüfung, Ereigniskatalog, Wiederholungsrichtlinie und Rotation des Secrets.

Richten Sie WASync auf Ihren Endpunkt aus, und Sie erhalten ein Signing Secret zurück. Verwendet whatsapp.events. Es ist keine App-Registrierung und kein Zustimmungsschritt erforderlich.

Ein Webhook-Slot pro Konto

Ein Konto hat genau eine Webhook-URL. PUT /webhook legt sie fest – ein erneuter Aufruf mit einer anderen URL verschiebt den Endpunkt, statt einen zweiten hinzuzufügen, und es gibt keine Löschfunktion. Das ist wichtig, sobald mehr als eine Stelle Ereignisse erhalten möchte: Ein n8n-Trigger und Ihr eigenes Backend können den Slot nicht gleichzeitig belegen. Verteilen Sie die Ereignisse auf Ihrer Seite weiter.

Endpunkt festlegen

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

Speichern Sie das Secret. Es wird hier ausgegeben, weil Sie es genau jetzt benötigen, um Ihren Prüfcode zu schreiben, und GET /webhook liefert es nicht erneut zurück – dieser Endpunkt meldet { url, events, secretSet }, bewusst ohne das Secret, weil Lesezugriffe protokolliert und in Tickets eingefügt werden. Verloren? Rufen Sie POST /webhook/rotate auf, um ein neues zu erhalten. Die URL muss HTTPS auf einem öffentlich erreichbaren Host sein; http, localhost und private Adressbereiche werden mit 400 invalid_webhook abgelehnt.

Die Rotation hat kein Überlappungsfenster: Das alte Secret ist ungültig, sobald das neue ausgegeben wird. Rollen Sie das neue Secret zügig aus – die unten beschriebene Wiederholungsrichtlinie deckt einen Rollout von wenigen Sekunden ab, nicht von Stunden.

Ereigniskatalog

EventFires whenDirection
message.receivedEine WhatsApp-Nachricht (Text oder Medien) trifft auf einer freigegebenen Verbindung ein.Nur eingehend – von Ihnen gesendete Nachrichten lösen dieses Ereignis nicht aus.
message.statusDer Zustellstatus einer Ihrer ausgehenden Nachrichten ändert sich.Nur ausgehend.
connection.disconnectedEine Verbindung funktioniert nicht mehr, und ihr Inhaber muss sie erneut koppeln.Einmal pro Ausfall, nicht wiederholt, solange sie gestört bleibt.
connection.connectedEine zuvor unterbrochene Verbindung funktioniert wieder.Einmal, und nur für eine Verbindung, bei der Ihnen eine Unterbrechung gemeldet wurde.

Ignorieren Sie unbekannte event-Werte, statt sie als Fehler zu behandeln – neue Typen werden im Changelog angekündigt, bevor sie erscheinen.

Ereignisse werden nur für qr-Verbindungen ausgelöst. cloud_api-Verbindungen senden derzeit keine Webhooks; fragen Sie für diese GET /messages und GET /connections/{id} per Polling ab.

Payload

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 – die stabile ID von WASync (derselbe Wert, den GET /messages als id zurückgibt). Entdoppeln und verknüpfen Sie darüber – die Zustellung erfolgt mindestens einmal (at-least-once).
  • message.wa_id – die eigene ID von WhatsApp, null, wenn unbekannt. Nur für Support und Debugging, niemals als Schlüssel zum Entdoppeln oder Verknüpfen.
  • from – nur internationale Ziffern, ohne +.
  • text ist bei reinen Medien-Nachrichten null; media_url / media_type sind bei Textnachrichten oder bei nicht öffentlich abrufbarer Datei null.
  • timestamp – Epoch in Millisekunden als Zahl, kein ISO-String.

Ein message.status-Ereignis aktualisiert eine Ihrer ausgehenden Nachrichten:

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

Signatur prüfen

Jede Zustellung enthält X-WASync-Timestamp (Epoch in ms) und X-WASync-Signature: sha256=<hex>, also HMAC-SHA256(timestamp + "." + rawBody, secret). Berechnen Sie sie über den rohen Body – vor jedem JSON-Parsing – und vergleichen Sie in konstanter Zeit. Weisen Sie Zustellungen ab, deren Zeitstempel mehr als etwa 5 Minuten alt ist; das verhindert Replays.

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

Zustell- und Wiederholungsrichtlinie

PropertyValue
Success criterionJeder 2xx-Status. Alles andere, einschließlich Weiterleitungen, gilt als Fehler.
Attempts3 pro Ereignis – sofort, dann nach 500 ms, dann nach 1 s.
Per-attempt timeout10 Sekunden. Bestätigen Sie schnell und verarbeiten Sie asynchron.
After the last failureDas Ereignis wird verworfen. Es gibt keine lange Wiederzustellungswarteschlange.
DuplicatesMöglich. Entdoppeln Sie über message.id.
OrderingNicht garantiert. Sortieren Sie nach timestamp, wenn es darauf ankommt.
Signature on retriesDerselbe Body und dieselbe Signatur werden erneut gesendet – prüfen Sie jeden Versuch auf die gleiche Weise.

Statusleiter

Die Werte von message.status bilden eine Einbahn-Leiter: sent (1) → delivered (2) → read (3), wobei failed ein Endzustand ist, der nicht auf read folgt.

Wenden Sie Updates monoton an. Übernehmen Sie einen neuen Status nur, wenn sein Rang höher ist als der gespeicherte. Eine Nachricht kann mit read vor delivered eintreffen (verspätete Zustellung), und doppelte Ereignisse sind möglich. Setzen Sie eine Nachricht in Ihrer Oberfläche nie zurück.

Abgleich – Polling als Sicherheitsnetz

Zustellungen werden dreimal versucht und dann verworfen. Ihr Webhook-Handler ist daher der schnelle Weg, nicht die Garantie. Betreiben Sie eine Hintergrundschleife, die alle 30–60 Sekunden GET /messages?connectionId=… abfragt und alles per Upsert übernimmt, was der Handler nicht gesehen hat; nutzen Sie dafür den Cursor für inkrementelle Abrufe. Dieselbe Schleife holt auch Statusupdates nach, die eintrafen, während Ihr Endpunkt nicht erreichbar war.

Webhooks = Aktualisierungen der Oberfläche in Echtzeit. Polling = Korrektheit. Setzen Sie beides ein.

Verbindungslebenszyklus

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
}

Eine qr-Verbindung kann aus Gründen ausfallen, die niemand beeinflussen kann – das Telefon ist einen Tag lang ausgeschaltet, WhatsApp wird auf einem anderen Gerät geöffnet, die Sitzung läuft ab. Die Nummer verstummt dann in beide Richtungen, und ohne ein Ereignis bemerkt es als Erstes Ihr Kunde.

Der QR-Code ist bewusst nicht im Payload enthalten. Ein WhatsApp-QR-Code ist eine Zugangsberechtigung per Scan: Wer ihn lesen kann, kann die Sitzung übernehmen. Das Ereignis teilt Ihnen mit, dass eine erneute Kopplung nötig ist – rufen Sie den QR-Code selbst über Ihren authentifizierten Aufruf GET /connections/{id} ab und zeigen Sie ihn dem Inhaber der Nummer, genau wie bei der Ersteinrichtung. Steuern Sie Ihre Oberfläche über needs_reconnect; betrachten Sie reason nur als Hinweis für das Log, da neue Werte hinzukommen können.

Erkennungsdauer: Eine Trennung wird durch eine Statusprüfung im 5-Minuten-Takt erkannt (~5 Min.). Wenn die Sitzung wiederherstellbar aussieht, wird zuerst eine automatische Reparatur versucht; dieser Weg dauert ca. 15–20 Minuten.

Sie erhalten keine Ereignisse?

  • Ist die Zustell-URL HTTPS auf einem öffentlichen Host? Localhost, private IPs und http:// werden abgelehnt. Nutzen Sie während der Entwicklung einen Tunnel und prüfen Sie mit GET /webhook, was tatsächlich gespeichert ist.
  • Enthält die Zugangsberechtigung whatsapp.events, und deckt sie die Verbindung ab, auf der die Nachricht eingetroffen ist?
  • Ist die Lizenz der Verbindung noch aktiv? Eine abgelaufene Lizenz bedeutet keine Ereignisse und 402 beim Senden.
  • Antwortet Ihr Endpunkt innerhalb von 10 Sekunden mit 2xx? Langsame Handler wirken wie Fehlschläge und verbrauchen alle drei Versuche.
  • Handelt es sich um eine qr-Verbindung? cloud_api-Verbindungen senden keine Webhooks.
  • Nur eingehende Nachrichten lösen message.received aus – Ihre eigenen Sendungen nie.
War diese Seite hilfreich?

On this page