Documentation Index
Fetch the complete documentation index at: https://help.withallo.com/llms.txt
Use this file to discover all available pages before exploring further.
Chaque requête webhook inclut une signature cryptographique vous permettant de vérifier qu’elle provient bien d’Allo. Sans vérification, un attaquant pourrait envoyer de faux événements à votre endpoint.
En-têtes de signature
Chaque POST webhook inclut trois en-têtes :
| En-tête | Description | Exemple |
|---|
webhook-id | Identifiant unique du message | msg_2NfDKEm9sF8xK3pQr1Zt |
webhook-timestamp | Horodatage Unix (secondes) de l’envoi du message | 1710510600 |
webhook-signature | Une ou plusieurs signatures HMAC-SHA256 | v1,K5oZfzN95Z3M+nrYOgGHfLGC7t8VjLtV... |
Secret de signature
Lorsque vous créez un endpoint webhook, la réponse inclut un champ signing_secret. Ce secret a le format whsec_<clé_base64> et est utilisé pour vérifier les signatures.
Conservez-le en sécurité — traitez-le comme un mot de passe. En cas de compromission, effectuez une rotation via l’API.
Algorithme de vérification
Extraire les en-têtes
Récupérez webhook-id, webhook-timestamp et webhook-signature des en-têtes de la requête.
Valider l'horodatage
Rejetez les requêtes dont l’horodatage s’écarte de plus de 5 minutes de l’heure actuelle de votre serveur. Cela prévient les attaques par rejeu.
Construire le contenu signé
Concaténez l’identifiant du webhook, l’horodatage et le corps brut de la requête avec des points :{webhook-id}.{webhook-timestamp}.{raw_body}
Calculer la signature attendue
- Retirez le préfixe
whsec_ de votre secret de signature.
- Décodez la chaîne restante en base64 pour obtenir les octets de la clé.
- Calculez le HMAC-SHA256 du contenu signé avec les octets de la clé.
- Encodez le résultat en base64.
Comparer les signatures
L’en-tête webhook-signature peut contenir plusieurs signatures séparées par des espaces (pour la rotation des secrets). Chacune a un préfixe v1,. Comparez votre signature calculée avec chacune d’elles. Si l’une correspond, la signature est valide.
Utilisez toujours le corps brut de la requête pour la vérification. Si votre framework parse le JSON puis le re-sérialise, la signature ne correspondra pas.
Exemples de code
const crypto = require("crypto");
function verifyWebhook(payload, headers, secret) {
const webhookId = headers["webhook-id"];
const webhookTimestamp = headers["webhook-timestamp"];
const webhookSignature = headers["webhook-signature"];
// Valider l'horodatage (tolérance de 5 minutes)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(webhookTimestamp)) > 300) {
throw new Error("Timestamp too old");
}
// Construire le contenu signé
const signedContent = `${webhookId}.${webhookTimestamp}.${payload}`;
// Décoder le secret (retirer le préfixe "whsec_", décoder en base64)
const secretBytes = Buffer.from(secret.split("_")[1], "base64");
// Calculer le HMAC-SHA256
const computed = crypto
.createHmac("sha256", secretBytes)
.update(signedContent)
.digest("base64");
// Comparer avec chaque signature de l'en-tête
const signatures = webhookSignature
.split(" ")
.map((sig) => sig.split(",")[1]);
if (!signatures.some((sig) => sig === computed)) {
throw new Error("Invalid signature");
}
return JSON.parse(payload);
}
// Exemple avec Express.js
const express = require("express");
const app = express();
app.post(
"/webhooks/allo",
express.raw({ type: "application/json" }),
(req, res) => {
try {
const event = verifyWebhook(
req.body.toString(),
req.headers,
process.env.WEBHOOK_SECRET
);
console.log("Verified event:", event.topic);
res.sendStatus(200);
} catch (err) {
console.error("Verification failed:", err.message);
res.sendStatus(400);
}
}
);
import hmac
import hashlib
import base64
import time
import json
def verify_webhook(payload: bytes, headers: dict, secret: str) -> dict:
webhook_id = headers.get("webhook-id")
webhook_timestamp = headers.get("webhook-timestamp")
webhook_signature = headers.get("webhook-signature")
# Valider l'horodatage (tolérance de 5 minutes)
now = int(time.time())
if abs(now - int(webhook_timestamp)) > 300:
raise ValueError("Timestamp too old")
# Construire le contenu signé
signed_content = f"{webhook_id}.{webhook_timestamp}.{payload.decode()}"
# Décoder le secret (retirer le préfixe "whsec_", décoder en base64)
secret_bytes = base64.b64decode(secret.split("_")[1])
# Calculer le HMAC-SHA256
computed = base64.b64encode(
hmac.new(
secret_bytes,
signed_content.encode(),
hashlib.sha256,
).digest()
).decode()
# Comparer avec chaque signature de l'en-tête
signatures = [
sig.split(",")[1]
for sig in webhook_signature.split(" ")
]
if computed not in signatures:
raise ValueError("Invalid signature")
return json.loads(payload)
# Exemple avec Flask
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhooks/allo", methods=["POST"])
def handle_webhook():
try:
event = verify_webhook(
request.get_data(),
request.headers,
os.environ["WEBHOOK_SECRET"],
)
print(f"Verified event: {event['topic']}")
return "", 200
except ValueError as e:
print(f"Verification failed: {e}")
return "", 400
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"fmt"
"math"
"net/http"
"strconv"
"strings"
"time"
)
func verifyWebhook(payload []byte, headers http.Header, secret string) error {
webhookID := headers.Get("webhook-id")
webhookTimestamp := headers.Get("webhook-timestamp")
webhookSignature := headers.Get("webhook-signature")
// Valider l'horodatage (tolérance de 5 minutes)
ts, _ := strconv.ParseInt(webhookTimestamp, 10, 64)
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return fmt.Errorf("timestamp too old")
}
// Construire le contenu signé
signedContent := fmt.Sprintf("%s.%s.%s", webhookID, webhookTimestamp, string(payload))
// Décoder le secret (retirer le préfixe "whsec_", décoder en base64)
parts := strings.SplitN(secret, "_", 2)
secretBytes, _ := base64.StdEncoding.DecodeString(parts[1])
// Calculer le HMAC-SHA256
mac := hmac.New(sha256.New, secretBytes)
mac.Write([]byte(signedContent))
computed := base64.StdEncoding.EncodeToString(mac.Sum(nil))
// Comparer avec chaque signature de l'en-tête
for _, sig := range strings.Split(webhookSignature, " ") {
parts := strings.SplitN(sig, ",", 2)
if len(parts) == 2 && parts[1] == computed {
return nil
}
}
return fmt.Errorf("invalid signature")
}
Vérifiez toujours les signatures en production. Ignorer la vérification expose votre application à des événements webhook falsifiés.