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
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…"
}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
| Event | Fires when | Direction |
|---|---|---|
message.received | Eine WhatsApp-Nachricht (Text oder Medien) trifft auf einer freigegebenen Verbindung ein. | Nur eingehend – von Ihnen gesendete Nachrichten lösen dieses Ereignis nicht aus. |
message.status | Der Zustellstatus einer Ihrer ausgehenden Nachrichten ändert sich. | Nur ausgehend. |
connection.disconnected | Eine Verbindung funktioniert nicht mehr, und ihr Inhaber muss sie erneut koppeln. | Einmal pro Ausfall, nicht wiederholt, solange sie gestört bleibt. |
connection.connected | Eine 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
{
"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, denGET /messagesalsidzurü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+.textist bei reinen Medien-Nachrichtennull;media_url/media_typesind bei Textnachrichten oder bei nicht öffentlich abrufbarer Dateinull.timestamp– Epoch in Millisekunden als Zahl, kein ISO-String.
Ein message.status-Ereignis aktualisiert eine Ihrer ausgehenden Nachrichten:
{
"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.
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)
});Zustell- und Wiederholungsrichtlinie
| Property | Value |
|---|---|
| Success criterion | Jeder 2xx-Status. Alles andere, einschließlich Weiterleitungen, gilt als Fehler. |
| Attempts | 3 pro Ereignis – sofort, dann nach 500 ms, dann nach 1 s. |
| Per-attempt timeout | 10 Sekunden. Bestätigen Sie schnell und verarbeiten Sie asynchron. |
| After the last failure | Das Ereignis wird verworfen. Es gibt keine lange Wiederzustellungswarteschlange. |
| Duplicates | Möglich. Entdoppeln Sie über message.id. |
| Ordering | Nicht garantiert. Sortieren Sie nach timestamp, wenn es darauf ankommt. |
| Signature on retries | Derselbe 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
{
"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 mitGET /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
402beim 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.receivedaus – Ihre eigenen Sendungen nie.
MCP
Verbinden Sie Claude, Cursor oder einen beliebigen MCP-Client mit dem Remote-Server von WASync. Sechs Tools, kein Code auf Ihrer Seite.
Kopplungscode & QR
Der QR-Code wechselt alle 20 Sekunden. Bieten Sie beide Wege an, denn ein Onboarding auf nur einem Gerät kann den eigenen Bildschirm nicht fotografieren.