recalled.dev
Référence

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

  1. Tu configures une URL cible dans les paramètres notifications du projet.
  2. À 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.
  3. Chaque fois qu'un event matchant ton filtre est ingéré, Recalled envoie un POST vers ton URL avec un body JSON.
  4. La requête porte des headers X-Recalled-Timestamp et X-Recalled-Signature que tu vérifies de ton côté avec le secret.
  5. 2xx = acquittement. 5xx / timeout = transient, retenté avec backoff. 4xx = fatal, droppé.
  6. 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 :

json
{
  "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

HeaderExempleRôle
Content-Typeapplication/jsonToujours JSON
User-AgentRecalled-Webhooks/1.0UA stable pour ton allowlist
X-Recalled-Timestamp1713183780Unix seconds de la signature
X-Recalled-Signaturev1=9f86d081...HMAC-SHA256 de ${timestamp}.${rawBody}
X-Recalled-Event-Idevt_abcClé de dédup pour l'idempotence
X-Recalled-Delivery-Idwhd_abc123xyzUnique 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)

ts
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)

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

TentativeDélai depuis la précédenteCumulé
1immédiat0
21 min1 min
35 min6 min
430 min36 min
52 h2 h 36
66 h8 h 36
712 h20 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.