Webhooks
Ereignisse empfangen, Signaturen prüfen, Wiederholungen verstehen.
Format
Wir leiten jedes Ereignis im Originalformat von Meta weiter, je Ereignis eine Anfrage:
{
"object": "whatsapp_business_account",
"entry": [{
"id": "WABA_ID",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": { "display_phone_number": "4989123456", "phone_number_id": "1234567890" },
"contacts": [{ "profile": { "name": "Erika" }, "wa_id": "491701234567" }],
"messages": [{ "from": "491701234567", "id": "wamid.…", "timestamp": "1767000000", "type": "text", "text": { "body": "Hallo!" } }]
}
}]
}]
}
Bestehender Code für Meta-Webhooks funktioniert daher unverändert – nur das Geheimnis zur Signaturprüfung ist Ihr whsec_….
Ereignisse (field)
| Feld | Inhalt |
|---|---|
messages |
Eingehende Nachrichten und Zustellstatus (statuses) |
smb_message_echoes |
Nachrichten, die Ihr Team in der WhatsApp Business App gesendet hat |
smb_app_state_sync |
Kontakte aus der App (einmalige Synchronisation und spätere Änderungen) |
history |
Chatverlauf der letzten bis zu 6 Monate (einmalig, in Abschnitten) |
account_update |
Kontoereignisse, z. B. Trennung (PARTNER_REMOVED) |
message_template_status_update |
Prüfergebnis von Vorlagen |
phone_number_quality_update |
Qualitätsbewertung und Versandlimit |
connection_status |
Von uns erzeugt: Statuswechsel einer Nummer (connected, degraded, disconnected) |
test |
Testereignis über POST /webhooks/{id}/test |
Über events beim Anlegen filtern Sie, welche Felder ein Endpunkt erhält (leer = alle). Mit phone_number_id erhält ein Endpunkt nur Ereignisse dieser Nummer (plus Kontoereignisse ihres WhatsApp Business Kontos).
Signatur prüfen
Jede Anfrage trägt diese Header:
| Header | Bedeutung |
|---|---|
X-Webhook-Id |
eindeutige ID der Zustellung (für Deduplizierung) |
X-Webhook-Timestamp |
Unix-Zeit der Signatur |
X-Webhook-Signature |
t=<timestamp>,v1=<hex HMAC-SHA256(secret, timestamp + "." + body)> |
X-Hub-Signature-256 |
sha256=<hex HMAC-SHA256(secret, body)> – kompatibel zu Meta |
X-Webhook-Event |
das field des Ereignisses |
X-Webhook-Attempt |
Zustellversuch (1, 2, …) |
Prüfen Sie immer den rohen Request-Body (nicht neu serialisiertes JSON) und verwerfen Sie Zeitstempel, die älter als 5 Minuten sind.
Node.js:
import crypto from 'node:crypto';
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const ts = Number(parts.t);
if (!ts || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1 ?? '', 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python:
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts = int(parts.get("t", "0"))
if abs(time.time() - ts) > 300:
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
PHP:
function verify(string $rawBody, string $header, string $secret): bool {
parse_str(str_replace(',', '&', $header), $p);
$ts = (int)($p['t'] ?? 0);
if (abs(time() - $ts) > 300) return false;
$expected = hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
return hash_equals($expected, $p['v1'] ?? '');
}
Antwort und Wiederholungen
Antworten Sie innerhalb von 10 Sekunden mit einem 2xx-Status – am besten sofort und verarbeiten Sie asynchron. Alles andere (Fehlercode, Zeitüberschreitung, Weiterleitung) gilt als Fehlschlag. Wir wiederholen mit wachsendem Abstand (10 s, 30 s, 2 min, 10 min, 30 min, 1 h, 2 h, 4 h, 8 h, 12 h). Nach dem letzten Versuch wird die Zustellung als fehlgeschlagen markiert und der verschlüsselte Inhalt nach Ablauf der Aufbewahrungsfrist (Standard 72 Stunden) gelöscht.
Die Reihenfolge der Zustellung ist nicht garantiert und ein Ereignis kann in seltenen Fällen doppelt ankommen. Deduplizieren Sie anhand von X-Webhook-Id bzw. der wamid.
Ziel-URLs müssen öffentlich per https erreichbar sein; private und interne Adressen sind aus Sicherheitsgründen gesperrt. Endpunkte, die über 72 Stunden ununterbrochen fehlschlagen, werden automatisch deaktiviert (PATCH /webhooks/{id} mit {"active": true} aktiviert wieder).