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 requete webhook inclut une signature cryptographique pour que vous puissiez verifier qu’elle provient bien d’Allo. Sans verification, un attaquant pourrait envoyer de faux evenements a votre endpoint.
En-tetes de signature
Chaque requete POST webhook inclut trois en-tetes :
| En-tete | 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 creez un endpoint webhook, la reponse inclut un champ signing_secret. Ce secret a le format whsec_<base64key> et est utilise pour verifier les signatures.
Stockez-le de maniere securisee — traitez-le comme un mot de passe. S’il est compromis, effectuez une rotation via l’API.
Algorithme de verification
Extraire les en-tetes
Recuperez webhook-id, webhook-timestamp et webhook-signature depuis les en-tetes de la requete.
Valider l'horodatage
Rejetez les requetes dont l’horodatage est eloigne de plus de 5 minutes de l’heure actuelle de votre serveur. Cela empeche les attaques par rejeu.
Construire le contenu signe
Concatenez l’identifiant du webhook, l’horodatage et le corps brut de la requete avec des points :{webhook-id}.{webhook-timestamp}.{raw_body}
Calculer la signature attendue
- Supprimez le prefixe
whsec_ de votre secret de signature.
- Decodez en base64 la chaine restante pour obtenir les octets de la cle.
- Calculez le HMAC-SHA256 du contenu signe avec les octets de la cle.
- Encodez le resultat en base64.
Comparer les signatures
L’en-tete webhook-signature peut contenir plusieurs signatures separees par des espaces (pour la rotation des secrets). Chacune a un prefixe v1,. Comparez votre signature calculee avec chacune d’entre elles. Si l’une correspond, la signature est valide.
Utilisez toujours le corps brut de la requete pour la verification. Si votre framework parse le JSON et le re-serialise, 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"];
// Validate timestamp (5-minute tolerance)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(webhookTimestamp)) > 300) {
throw new Error("Timestamp too old");
}
// Build signed content
const signedContent = `${webhookId}.${webhookTimestamp}.${payload}`;
// Decode secret (strip "whsec_" prefix, base64-decode)
const secretBytes = Buffer.from(secret.split("_")[1], "base64");
// Compute HMAC-SHA256
const computed = crypto
.createHmac("sha256", secretBytes)
.update(signedContent)
.digest("base64");
// Compare against each signature in the header
const signatures = webhookSignature
.split(" ")
.map((sig) => sig.split(",")[1]);
if (!signatures.some((sig) => sig === computed)) {
throw new Error("Invalid signature");
}
return JSON.parse(payload);
}
// Express.js example
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")
# Validate timestamp (5-minute tolerance)
now = int(time.time())
if abs(now - int(webhook_timestamp)) > 300:
raise ValueError("Timestamp too old")
# Build signed content
signed_content = f"{webhook_id}.{webhook_timestamp}.{payload.decode()}"
# Decode secret (strip "whsec_" prefix, base64-decode)
secret_bytes = base64.b64decode(secret.split("_")[1])
# Compute HMAC-SHA256
computed = base64.b64encode(
hmac.new(
secret_bytes,
signed_content.encode(),
hashlib.sha256,
).digest()
).decode()
# Compare against each signature in the header
signatures = [
sig.split(",")[1]
for sig in webhook_signature.split(" ")
]
if computed not in signatures:
raise ValueError("Invalid signature")
return json.loads(payload)
# Flask example
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")
// Validate timestamp (5-minute tolerance)
ts, _ := strconv.ParseInt(webhookTimestamp, 10, 64)
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
return fmt.Errorf("timestamp too old")
}
// Build signed content
signedContent := fmt.Sprintf("%s.%s.%s", webhookID, webhookTimestamp, string(payload))
// Decode secret (strip "whsec_" prefix, base64-decode)
parts := strings.SplitN(secret, "_", 2)
secretBytes, _ := base64.StdEncoding.DecodeString(parts[1])
// Compute HMAC-SHA256
mac := hmac.New(sha256.New, secretBytes)
mac.Write([]byte(signedContent))
computed := base64.StdEncoding.EncodeToString(mac.Sum(nil))
// Compare against each signature in the header
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")
}
Verifiez toujours les signatures en production. Ignorer la verification expose votre application a des evenements webhook usurpes.