Zum Hauptinhalt springen
Audact
Angebot ansehen

Webhook-Zustellung

Wie Audact ausgehende Webhook-Zustellungen signiert, wiederholt und in den Dead-Letter-Speicher schreibt. Behandelt das HMAC-SHA256-Signaturschema, den Rotations-Übergangszeitraum, den Wiederholungsplan und die Resend-API.

Dieses Dokument beschreibt den Webhook-Zustellungsmechanismus: Signierung, Wiederholung und Dead-Letter-Verhalten. Jede ausgehende Webhook-Zustellung trägt die folgenden Header und folgt dem unten beschriebenen Wiederholungs- und Dead-Letter-Plan.

Signaturschema

Jede Zustellung wird mit HMAC-SHA256 signiert und trägt vier Header:

HeaderBedeutung
X-Audact-Signaturet=<unix_seconds>,v1=<hex_hmac> (siehe unten für die Doppel-v1=-Rotationsform)
X-Audact-TimestampDerselbe <unix_seconds>-Wert als eigenständiger Header
X-Audact-EventDer Ereignistyp (z. B. call_started, call_summary)
X-Audact-Delivery-IdEine zustellungsversuchsspezifische UUID; bei jedem Wiederholungsversuch desselben Ereignisses eindeutig

Verwechseln Sie diesen Header nicht mit dem Tool-Dispatch-Header. Ausgehende Tool-Aufrufe (Agent → Mandanten-Tool-Endpunkt) werden mit einem anderen, unabhängigen Header signiert, X-Signature — siehe tool-dispatch. Die Suche nach X-Signature in dieser Spezifikation findet das Webhook-Schema nicht.

Was signiert wird. Die genaue Zeichenkette "<unix_timestamp>.<raw_request_body>" — der Zeitstempel, ein wörtlicher ., dann die rohen JSON-Body-Bytes genau wie gesendet (keine Re-Serialisierung). Der MAC ist HMAC-SHA256, kodiert als Kleinbuchstaben-Hex.

Einzelschlüssel-Headerwert:

t=1750000000,v1=5f4dcc3b5aa765d61d8327deb882cf99...

Doppelsignatur-Rotationsübergangszeitraum (72 Stunden). POST /v1/webhooks/secrets/rotate behält den vorherigen Signing-Key für 72 Stunden nach der Rotation als gültig. In diesem Zeitfenster trägt jede Zustellung zwei v1=-Werte im selben Header — einen HMAC unter dem alten Schlüssel, einen unter dem neuen:

t=1750000000,v1=<hmac_under_old_key>,v1=<hmac_under_new_key>

Ein Prüfer sollte die Signatur akzeptieren, wenn irgendein v1=-Wert übereinstimmt (genau das macht der serverseitige Prüfer — er iteriert alle v1=-Werte und vergleicht jeden mit konstantem Zeitaufwand). Das bedeutet, ein Empfänger muss nur sein aktuell bekanntes Secret speichern und es gegen jeden v1=-Wert prüfen; er muss nicht wissen, welcher „aktuell" und welcher „alt" ist. GET /v1/webhooks/secrets/status meldet, ob eine Rotation läuft, und den grace_period_ends-Zeitstempel.

Replay-Schutz. Der Signatur-Header läuft nicht selbst ab — der Prüfer ist dafür verantwortlich, veraltete Zeitstempel abzulehnen. Verwenden Sie eine Toleranzprüfung mit einem empfohlenen Höchstalter von 300 Sekunden (5 Minuten) zwischen dem t=-Wert und „jetzt". Der Vergleich des HMAC-Digests muss mit konstantem Zeitaufwand erfolgen; wenden Sie dasselbe auf der Empfängerseite an, um Timing-Seitenkanalangriffe zu vermeiden.

Abrufen des Signing-Secrets (Einmaloffenlegung)

Das mandantenspezifische Signing-Secret wird serverseitig generiert und nur in einem Secrets-Manager gespeichert — es ist im Nachhinein nicht abrufbar, entsprechend dem Stripe/Twilio-Muster. Es wird im JSON-Antwort-Body, im Klartext, genau einmal von zwei Aufrufen zurückgegeben:

  • POST /v1/webhooks — wenn dieser Aufruf das Signing-Secret des Mandanten zum ersten Mal bereitstellt (d. h. der Mandant hatte zuvor keinen Webhook-Endpunkt und daher kein Secret), enthält die Antwort signing_secret neben dem erstellten Endpunkt. Jeder nachfolgende POST /v1/webhooks-Aufruf für denselben Mandanten verwendet das bereits bereitgestellte Secret wieder und lässt signing_secret in seiner Antwort weg.
  • POST /v1/webhooks/secrets/rotate — jede erfolgreiche Rotation gibt den neu generierten Schlüssel als signing_secret in ihrem Antwort-Body zurück. Dies ist der einzige Weg, ein Signing-Secret nach der ersten Offenlegung erneut einzusehen — rotieren und den neuen Wert sofort notieren.

Kein anderer Endpunkt gibt jemals das Secret zurück. Insbesondere meldet GET /v1/webhooks/secrets/status nur Rotations-Metadaten (in_rotation, grace_period_ends, created_at) — niemals das Schlüsselmaterial selbst. Speichern Sie den offenbarten Wert sicher (z. B. in Ihrem eigenen Secrets-Manager); falls er verloren geht, ist der einzige Wiederherstellungspfad POST /v1/webhooks/secrets/rotate, das den alten Schlüssel nach dem oben beschriebenen 72-stündigen Doppelsignatur-Übergangszeitraum ungültig macht.

Copy-paste-Prüffunktion (Go)

go

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strconv"
	"strings"
	"time"
)

func verifyAudactWebhook(secret []byte, sigHeader string, rawBody []byte) bool {
	var ts int64
	var sigs []string
	for _, part := range strings.Split(sigHeader, ",") {
		if strings.HasPrefix(part, "t=") {
			ts, _ = strconv.ParseInt(strings.TrimPrefix(part, "t="), 10, 64)
		} else if strings.HasPrefix(part, "v1=") {
			sigs = append(sigs, strings.TrimPrefix(part, "v1="))
		}
	}
	age := time.Now().Unix() - ts
	if age < 0 {
		age = -age
	}
	if age > 300 {
		return false
	}
	mac := hmac.New(sha256.New, secret)
	mac.Write([]byte(fmt.Sprintf("%d.%s", ts, rawBody)))
	expected := hex.EncodeToString(mac.Sum(nil))
	for _, sig := range sigs {
		if hmac.Equal([]byte(sig), []byte(expected)) {
			return true
		}
	}
	return false
}

Neuimplementierung ohne den Python-SDK-Helfer: t= und jeden v1=-Wert aus dem Header parsen, hex(hmac_sha256(secret, fmt.Sprintf("%d.%s", ts, rawBody))) neu berechnen und akzeptieren, wenn es mit irgendeinem v1=-Wert mit konstantem Zeitaufwand übereinstimmt und abs(now - ts) <= 300s.

Zielregeln (SSRF-Schutz)

Webhook-Empfänger müssen öffentliche HTTPS-Endpunkte sein.

  • Abgelehnt beim Erstellen/Aktualisieren (400, details.reason = "blocked_destination"): IP-Literale in Loopback-, privaten (RFC 1918)-, Link-Local- (einschließlich der Cloud-Metadatenadresse 169.254.169.254), CGNAT-, Dokumentations-, Benchmarking-, reservierten, Multicast- und NAT64-Bereichen sowie deren IPv6-Entsprechungen; der Loopback-Hostname; einlabelige Hostnamen (Container-Service-Namen wie redis); Namen, die auf .internal, .local, .localdomain, .home.arpa, .lan oder .corp enden; und Namen, die zu einer nicht-öffentlichen Adresse auflösen.
  • Bei jeder Zustellung, jedem Resend und jedem Test-Ping erzwungen: Die Adresse wird zur Verbindungszeit nach der Hostnamen-Auflösung erneut geprüft. Dies gilt auch für Webhooks, die vor dieser Schutzmaßnahme erstellt wurden. Solche Zustellungen schlagen mit blocked_destination fehl.
  • Weiterleitungen werden nicht gefolgt. Eine 3xx-Antwort zählt als Nicht-2xx-Fehler. Richten Sie den Webhook auf die endgültige URL.
  • Fehlermeldung: Ein Fehler auf Netzwerkebene wird (in test_webhook und im last_error des Zustellungsprotokolls) als eines von blocked_destination, timeout, tls_error, dns_error oder connection_failed gemeldet. Der zugrunde liegende Netzwerkfehler wird niemals zurückgegeben.

Wiederholungsplan

  • Maximal 4 Gesamtversuche (1 Erstzustellung + 3 Wiederholungen). Diese Obergrenze ist derzeit fest, nicht pro Mandant konfigurierbar.
  • Basisverzögerungen zwischen den Versuchen: 1s, 10s, 60s — angewandt vor den Versuchen 2, 3 und 4.
  • Jitter: Jede Basisverzögerung wird mit einem Zufallsfaktor in [0.7, 1.3] (±30 %) multipliziert, sodass die tatsächlichen Verzögerungen ungefähr 0,7–1,3 s, 7–13 s, 42–78 s betragen.
  • HTTP-Timeout pro Versuch: 10 Sekunden.
  • Jede Nicht-2xx-Antwort oder jeder Netzwerk-/Timeout-Fehler löst eine Wiederholung aus (vorbehaltlich der obigen Obergrenze).

Dead-Letter und Resend

  • Nachdem alle 4 Versuche erschöpft sind, wird die Zustellung mit dem original erhaltenen Payload in einem Dead-Letter-Speicher gespeichert.
  • Aufbewahrung: 90 Tage ab dem Zeitpunkt, zu dem der Eintrag in den Dead-Letter-Speicher geschrieben wurde.
  • Stellen Sie eine Dead-Letter-Zustellung mit POST /v1/webhooks/deliveries/{id}/resend wieder her — dies reiht das ursprüngliche Ereignis erneut in den normalen Dispatch-Pfad ein (neue Zustellungsversuche, neue X-Audact-Delivery-Id-Werte).
  • Listen Sie Dead-Letter-Zustellungen für einen Mandanten über den Dead-Letter-Listing-Endpunkt auf, um Monitoring/Alerting aufzubauen.

Praktische Hinweise

  • Behandeln Sie die Zustellung als mindestens einmal: Ein Empfänger kann dieselbe X-Audact-Delivery-Id mehr als einmal sehen, wenn er seine eigene Verarbeitung nach einem bereits an Audact zurückgegebenen 2xx wiederholt und die eigene Wiederholungslogik von Audact erneut auslöst (z. B. ein Netzwerkausfall nachdem Ihr 2xx gesendet, aber bevor Audact ihn empfangen wurde) — gestalten Sie Handler unabhängig von X-Audact-Delivery-Id idempotent.
  • Wenn Ihr Endpunkt länger als das gesamte Wiederholungsfenster (~90 Sekunden) plus die Zeit, die Sie benötigen, um es zu bemerken, ausgefallen ist, landet die Zustellung im Dead-Letter-Speicher; überwachen Sie ihn und rufen Sie den Resend-Endpunkt auf, sobald Ihr Endpunkt wieder verfügbar ist, anstatt darauf zu vertrauen, dass Audact unbegrenzt wiederholt.

Quelle aktualisiert: 19. Oktober 2026