Webhooks sortants
Recalled peut POSTer chaque event ingéré vers une URL que tu contrôles, signée en HMAC-SHA256. Utilise-le pour router les events vers ton back-end, un data warehouse, Zapier, n8n, un SIEM, ou n'importe quel outil qui parle HTTPS.
> Prérequis plan + rôle. Les webhooks génériques sont disponibles sur Pro (1 webhook) et Scale (jusqu'à 10). Seul le owner du projet peut les créer, les modifier ou les supprimer. Les admins et viewers invités ne peuvent ni les voir ni les gérer, même s'ils peuvent éditer le reste des paramètres.
Comment ça marche
- Tu configures une URL cible dans les paramètres notifications du projet.
- À la sauvegarde, Recalled génère un secret HMAC et l'affiche une seule fois. Stocke-le dans ton env à côté de tes clés API.
- Chaque fois qu'un event matchant ton filtre est ingéré, Recalled envoie un
POSTvers ton URL avec un body JSON. - La requête porte des headers
X-Recalled-TimestampetX-Recalled-Signatureque tu vérifies de ton côté avec le secret. - 2xx = acquittement. 5xx / timeout = transient, retenté avec backoff. 4xx = fatal, droppé.
- Après 7 échecs consécutifs, le webhook est automatiquement désactivé pour ne pas marteler un endpoint cassé. Corrige ton endpoint puis réactive-le depuis le dashboard.
Format du payload
Chaque livraison est enveloppée pour qu'on puisse ajouter de nouveaux types de webhook events plus tard sans casser les consommateurs :
{
"id": "whd_abc123xyz",
"type": "event.created",
"createdAt": "2026-04-15T14:23:00.123Z",
"projectId": "proj_xyz",
"event": {
"id": "evt_abc",
"action": "invoice.refunded",
"actor": { "id": "user_1", "email": "alice@acme.co", "name": "Alice" },
"organization": "org_acme",
"targets": [{ "type": "invoice", "id": "inv_42" }],
"metadata": { "amount": 4200, "currency": "eur" },
"occurredAt": "2026-04-15T14:22:58.000Z",
"hash": "sha256:..."
}
}Headers de requête
| Header | Exemple | Rôle |
|---|---|---|
Content-Type | application/json | Toujours JSON |
User-Agent | Recalled-Webhooks/1.0 | UA stable pour ton allowlist |
X-Recalled-Timestamp | 1713183780 | Unix seconds de la signature |
X-Recalled-Signature | v1=9f86d081... | HMAC-SHA256 de ${timestamp}.${rawBody} |
X-Recalled-Event-Id | evt_abc | Clé de dédup pour l'idempotence |
X-Recalled-Delivery-Id | whd_abc123xyz | Unique par tentative |
Les headers custom que tu configures sur le channel (ex : Authorization: Bearer ... pour un endpoint privé) sont mergés avec les autres. Les headers réservés Recalled gagnent toujours.
Vérifier la signature (Node.js)
import crypto from "node:crypto";
const SECRET = process.env.RECALLED_WEBHOOK_SECRET!;
const MAX_SKEW_SECONDS = 5 * 60;
export function verifyRecalledSignature(req: {
rawBody: string;
headers: Record<string, string | undefined>;
}): boolean {
const timestamp = req.headers["x-recalled-timestamp"];
const signature = req.headers["x-recalled-signature"];
if (!timestamp || !signature) return false;
// Refuse les livraisons expirées pour bloquer le rejeu.
const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
if (!Number.isFinite(skew) || skew > MAX_SKEW_SECONDS) return false;
const expected =
"v1=" +
crypto
.createHmac("sha256", SECRET)
.update(`${timestamp}.${req.rawBody}`, "utf8")
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Vérifier la signature (Python)
import hmac, hashlib, os, time
SECRET = os.environ["RECALLED_WEBHOOK_SECRET"]
MAX_SKEW = 5 * 60
def verify(raw_body: bytes, headers: dict) -> bool:
ts = headers.get("x-recalled-timestamp")
sig = headers.get("x-recalled-signature")
if not ts or not sig:
return False
if abs(int(time.time()) - int(ts)) > MAX_SKEW:
return False
msg = f"{ts}.{raw_body.decode('utf-8')}".encode("utf-8")
expected = "v1=" + hmac.new(SECRET.encode("utf-8"), msg, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)Politique de retry
| Tentative | Délai depuis la précédente | Cumulé |
|---|---|---|
| 1 | immédiat | 0 |
| 2 | 1 min | 1 min |
| 3 | 5 min | 6 min |
| 4 | 30 min | 36 min |
| 5 | 2 h | 2 h 36 |
| 6 | 6 h | 8 h 36 |
| 7 | 12 h | 20 h 36 |
Les erreurs transientes (408, 429, 5xx, network, timeout) sont retentées. Les erreurs fatales (4xx hors 408/429, 401/403/404) sont droppées immédiatement.
Après 7 échecs consécutifs sur des events distincts, le webhook passe en enabled = false et ne reçoit plus de livraisons jusqu'à réactivation manuelle.
Idempotence
Les livraisons sont at-least-once : le même event peut arriver deux fois si une première tentative a timeout chez nous alors qu'elle a réussi chez toi. Déduplique sur X-Recalled-Event-Id ou le champ event.id du body. Les deux sont stables par event.
Protection SSRF
À la création et avant chaque livraison, Recalled refuse les URLs qui résolvent vers des plages IP privées (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, loopback, carrier-grade NAT, link-local, IPv6 ULA), les ports bloqués (SSH, SQL, Redis, endpoints metadata), et les redirections ne sont pas suivies. Ta cible doit être joignable depuis internet en HTTPS.
Test
Clique Envoyer une notification de test dans le dashboard. Recalled envoie un event test.ping avec des champs synthétiques pour que tu puisses tester ton code de vérification de signature sans attendre une vraie action utilisateur.