Aller au contenu principal

Livraison des webhooks

Comment Audact signe, relance et place en dead-letter les livraisons de webhooks sortants. Couvre le schéma de signature HMAC-SHA256, la période de grâce lors de la rotation, le calendrier des relances et l’API de renvoi.

Ce document décrit le mécanisme de livraison des webhooks : signature, relances et comportement de dead-letter. Chaque livraison de webhook sortant comporte les en-têtes suivants et suit le calendrier de relance et de dead-letter ci-dessous.

Schéma de signature

Chaque livraison est authentifiée avec HMAC-SHA256 et comporte quatre en-têtes :

En-têteSignification
X-Audact-Signaturet=<unix_seconds>,v1=<hex_hmac> (voir plus bas pour la forme à double v1= lors d’une rotation)
X-Audact-TimestampLa même valeur <unix_seconds>, dans un en-tête distinct
X-Audact-EventLe type d’événement (par ex. call_started, call_summary)
X-Audact-Delivery-IdUn UUID propre à chaque tentative ; unique à chaque nouvelle tentative du même événement

Ne le confondez pas avec l’en-tête de tool-dispatch. Les appels d’outils sortants (agent → point de terminaison d’outil du tenant) sont authentifiés avec un en-tête différent et sans rapport, X-Signature — voir tool-dispatch. Une recherche de X-Signature dans cette spécification ne trouvera pas le schéma des webhooks.

Ce que couvre la signature. La chaîne exacte "<unix_timestamp>.<raw_request_body>" — l’horodatage, un . littéral, puis les octets bruts du corps JSON tels qu’ils ont été envoyés (et non une re-sérialisation). Le MAC est HMAC-SHA256, encodé en hexadécimal minuscule.

Valeur d’en-tête à clé unique :

t=1750000000,v1=5f4dcc3b5aa765d61d8327deb882cf99...

Période de grâce à double signature lors de la rotation (72 heures). POST /v1/webhooks/secrets/rotate conserve la clé de signature précédente comme valide pendant 72 heures après la rotation. Pendant cette fenêtre, chaque livraison comporte deux valeurs v1= dans le même en-tête — un HMAC avec l’ancienne clé, un avec la nouvelle clé :

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

Un vérificateur doit accepter la signature si l’une quelconque des valeurs v1= correspond (c’est exactement ce que fait le vérificateur côté serveur : il parcourt toutes les valeurs v1= et compare chacune en temps constant). Un récepteur n’a donc qu’à conserver le secret qu’il connaît actuellement et à l’essayer sur chaque valeur v1= ; il n’a pas besoin de savoir laquelle est « actuelle » et laquelle est « ancienne ». GET /v1/webhooks/secrets/status indique si une rotation est en cours, ainsi que l’horodatage grace_period_ends.

Protection contre le rejeu. L’en-tête de signature n’expire pas de lui-même — c’est au vérificateur de rejeter les horodatages périmés. Utilisez un contrôle de tolérance avec un âge maximal recommandé de 300 secondes (5 minutes) entre la valeur t= et « maintenant ». La comparaison du condensat HMAC doit se faire en temps constant ; faites de même côté réception pour éviter les canaux auxiliaires temporels.

Obtention du secret de signature (révélé une seule fois)

Le secret de signature propre à chaque tenant est généré côté serveur et stocké uniquement dans un gestionnaire de secrets — il n’est jamais récupérable après coup, selon le même modèle que Stripe/Twilio. Il est renvoyé dans le corps de la réponse JSON, en clair, exactement une fois, par deux appels :

  • POST /v1/webhooks — lorsque cet appel provisionne pour la première fois le secret de signature du tenant (c’est-à-dire que le tenant n’avait aucun point de terminaison de webhook, et donc aucun secret, avant cet appel), la réponse inclut signing_secret avec le point de terminaison créé. Tout appel ultérieur à POST /v1/webhooks pour le même tenant réutilise le secret déjà provisionné et omet signing_secret dans sa réponse.
  • POST /v1/webhooks/secrets/rotate — chaque rotation réussie renvoie la clé nouvellement générée sous le nom signing_secret dans le corps de sa réponse. C’est la seule façon de revoir un secret de signature après la première révélation — effectuez une rotation et notez immédiatement la nouvelle valeur.

Aucun autre point de terminaison ne renvoie jamais le secret. En particulier, GET /v1/webhooks/secrets/status ne renvoie que des métadonnées de rotation (in_rotation, grace_period_ends, created_at) — jamais le matériel de clé lui-même. Conservez la valeur révélée en lieu sûr (par ex. dans votre propre gestionnaire de secrets) ; si elle est perdue, la seule voie de récupération est POST /v1/webhooks/secrets/rotate, qui invalide l’ancienne clé après la période de grâce à double signature de 72 heures décrite ci-dessus.

Vérificateur à copier-coller (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
}

Pour réimplémenter sans l’assistant du SDK Python : analysez t= et chaque valeur v1= de l’en-tête, recalculez hex(hmac_sha256(secret, fmt.Sprintf("%d.%s", ts, rawBody))), et acceptez si le résultat correspond en temps constant à l’une quelconque des valeurs v1= et que abs(now - ts) <= 300s.

Règles de destination (protection SSRF)

Les récepteurs de webhooks doivent être des points de terminaison HTTPS publics.

  • Rejetés à la création ou à la mise à jour (400, details.reason = "blocked_destination") : les adresses IP littérales dans les plages de bouclage (loopback), privées (RFC 1918), de lien local (y compris l’adresse de métadonnées cloud 169.254.169.254), CGNAT, de documentation, de benchmarking, réservées, multicast et NAT64, ainsi que leurs équivalents IPv6 ; le nom d’hôte de bouclage ; les noms d’hôte à un seul label (noms de services de conteneurs tels que redis) ; les noms se terminant par .internal, .local, .localdomain, .home.arpa, .lan ou .corp ; et les noms qui se résolvent vers une adresse non publique.
  • Appliqué à chaque livraison, renvoi et ping de test : l’adresse est revérifiée au moment de la connexion, après la résolution du nom d’hôte. Cela couvre aussi les webhooks créés avant l’existence de cette protection. Ces livraisons échouent avec blocked_destination.
  • Les redirections ne sont pas suivies. Une réponse 3xx compte comme un échec non 2xx. Faites pointer le webhook vers l’URL finale.
  • Signalement des erreurs : un échec au niveau réseau est signalé (dans test_webhook et dans le last_error du journal de livraison) sous l’une des formes blocked_destination, timeout, tls_error, dns_error ou connection_failed. L’erreur réseau sous-jacente n’est jamais renvoyée.

Calendrier des relances

  • Maximum de 4 tentatives au total (1 livraison initiale + 3 relances). Ce plafond est actuellement fixe et n’est pas configurable par tenant.
  • Délais de base entre les tentatives : 1s, 10s, 60s — appliqués avant les tentatives 2, 3 et 4 respectivement.
  • Gigue (jitter) : chaque délai de base est multiplié par un facteur aléatoire dans [0.7, 1.3] (±30 %), de sorte que les délais réels sont d’environ 0.7–1.3s, 7–13s, 42–78s.
  • Délai d’expiration HTTP par tentative : 10 secondes.
  • Toute réponse non 2xx ou toute erreur réseau/d’expiration déclenche une relance (dans la limite du plafond ci-dessus).

Dead-letter et renvoi

  • Une fois les 4 tentatives épuisées, la livraison est écrite dans un stockage dead-letter, avec la charge utile d’origine conservée telle quelle.
  • Conservation : 90 jours à compter du moment où l’entrée est placée en dead-letter.
  • Récupérez une livraison placée en dead-letter avec POST /v1/webhooks/deliveries/{id}/resend — cela remet l’événement d’origine en file d’attente sur le chemin de distribution normal (nouvelles tentatives de livraison, nouvelles valeurs X-Audact-Delivery-Id).
  • Listez les livraisons placées en dead-letter pour un tenant via le point de terminaison de liste des dead-letters afin de mettre en place une supervision et des alertes.

Implications pratiques

  • Considérez la livraison comme au moins une fois (at-least-once) : un récepteur peut voir le même X-Audact-Delivery-Id plus d’une fois uniquement s’il relance son propre traitement après qu’un 2xx a déjà été renvoyé à Audact et que la logique de relance d’Audact se redéclenche (par ex. une micro-coupure réseau après l’envoi de votre 2xx mais avant qu’Audact ne l’ait reçu) — concevez vos gestionnaires pour qu’ils soient idempotents sur X-Audact-Delivery-Id, quoi qu’il arrive.
  • Si votre point de terminaison est indisponible plus longtemps que la fenêtre complète de relances (~90 secondes) plus le temps qu’il vous faut pour vous en apercevoir, la livraison arrive dans le stockage dead-letter ; surveillez-le et appelez le point de terminaison de renvoi une fois votre point de terminaison rétabli, plutôt que de compter sur Audact pour relancer indéfiniment.

Source mise à jour: 19 octobre 2026