Après chaque appel, Safina envoie le résumé à ton URL de webhook. Ton CRM, ton workflow n8n ou ton Zap en fait un lead ou une tâche. Mais une URL de webhook n’est qu’une adresse. Quiconque la connaît peut y envoyer un faux événement « appel terminé ».
Une signature règle ce problème. Safina signe chaque requête avec un secret que seuls toi et Safina connaissez. Ton récepteur vérifie la signature et sait deux choses : Safina a envoyé la requête, et personne n’a modifié le contenu en route. Un horodatage dans la signature empêche aussi quelqu’un de renvoyer une ancienne requête.
Tu découvres les webhooks ? Lis d’abord Qu’est-ce qu’un webhook ?. La configuration dans l’app se trouve dans le guide Connecter Safina AI à ton CRM via Webhook.
Ce qu’il te faut
| Prérequis | Détails |
|---|---|
| Offre Safina | Pro ou supérieure |
| Un webhook dans l’app Safina | Avec un secret de signature (voir ci-dessous) |
| Un récepteur | Ton propre serveur, n8n, Zapier ou Make |
Tu crées et signes les webhooks dans l’app Safina. Tu peux aussi les créer avec l’API REST de Safina (POST /webhooks). L’API REST et les clés API sont en bêta : l’accès anticipé est disponible sur demande à info@safina.ai, et les clés API nécessitent l’offre Pro ou supérieure.
Étape 1 : active la signature
La signature est facultative. Safina signe les requêtes d’un webhook uniquement s’il a un secret de signature. Tu l’obtiens de trois façons :
- Nouveau webhook dans l’app : dans l’app Safina, ouvre l’onglet Safina AI, puis « Synchronisation CRM », et ajoute un webhook. Active Signer les envois avant d’enregistrer.
- Webhook existant dans l’app : appuie sur Créer un secret sur le webhook. Les webhooks antérieurs à cette fonction restent non signés tant que tu ne le fais pas.
- API REST (bêta) : envoie
"create_signing_secret": trueavecPOST /webhooks. La réponse contientsigning_secret.
Le secret commence par whsec_. Safina ne l’affiche qu’une seule fois. Copie-le et enregistre-le dans ton récepteur, comme un mot de passe.
Un webhook sans secret continue de fonctionner et envoie des requêtes non signées, comme avant. Tu peux aussi utiliser l’authentification du webhook (auth_type) : Safina envoie alors un jeton bearer ou une clé API dans un en-tête avec chaque requête. Cela prouve que l’expéditeur connaît le jeton. Une signature prouve en plus que le contenu n’a pas changé et que la requête est récente.
Étape 2 : comprends l’en-tête
Chaque requête signée porte cet en-tête :
X-Safina-Signature: t=1794569912,v1=5f2c…e91a
test l’heure de la tentative de livraison, en secondes Unix.v1est le HMAC-SHA256 du texte<t>.<body brut>, avec ton secret de signature comme clé, en hexadécimal.
Ton récepteur vérifie trois choses :
- Heure : refuse la requête si
test plus de 300 secondes (5 minutes) avant ou après ton horloge. - Signature : calcule le HMAC sur le body brut de la requête, exactement tel qu’il est arrivé, avant d’analyser le JSON. Si tu analyses le JSON puis le réécris, les espaces ou l’ordre des clés peuvent changer, et la signature ne correspond plus.
- Comparaison : compare ta valeur à chaque valeur
v1en temps constant. Une correspondance suffit.
Pourquoi peut-il y avoir plusieurs v1 ? Quand tu appuies sur Créer un nouveau secret, l’ancien secret reste valable 24 heures. Pendant ce temps, l’en-tête contient une valeur v1 pour chaque secret valide. Tu peux ainsi mettre à jour ton récepteur sans perdre d’événements.
Chaque tentative de livraison a un nouvel horodatage et une nouvelle signature. Une nouvelle tentative envoie le même event_id : ignore donc un event_id que tu as déjà traité.
Étape 3 : vérifie la signature dans ton code
Ces exemples sont les mêmes que dans la référence de l’API (en anglais). Nous les avons testés tous les deux avec de vraies signatures produites par le code de signature de Safina.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received (a Buffer or a string)
const verifySafinaSignature = (header, rawBody, secret) => {
const parts = header.split(",").map((part) => part.trim().split("="));
const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
if (!Number.isInteger(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = Buffer.from(
createHmac("sha256", secret)
.update(`${timestamp}.`)
.update(rawBody)
.digest("hex")
);
return parts.some(([key, value]) => {
const candidate = Buffer.from(value ?? "");
return (
key === "v1" &&
candidate.length === expected.length &&
timingSafeEqual(candidate, expected)
);
});
};
Avec Express, lis le body de cette route en octets bruts, par exemple avec express.raw({ type: "application/json" }). req.body est alors un Buffer. N’analyse le JSON qu’après la vérification :
app.post("/safina-webhook", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Safina-Signature") ?? "";
if (!verifySafinaSignature(header, req.body, process.env.SAFINA_SIGNING_SECRET)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString("utf8"));
// … process the event
res.status(200).end();
});
Python
import hashlib
import hmac
import time
# raw_body: the request body exactly as received, as bytes
def verify_safina_signature(header: str, raw_body: bytes, secret: str) -> bool:
parts = [part.strip().split("=", 1) for part in header.split(",")]
values = [(part[0], part[1]) for part in parts if len(part) == 2]
timestamp = next((value for key, value in values if key == "t"), "")
if not (timestamp.isascii() and timestamp.isdigit()):
return False
if abs(time.time() - int(timestamp)) > 300:
return False
expected = hmac.new(
secret.encode(), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return any(
key == "v1" and hmac.compare_digest(value.encode(), expected.encode())
for key, value in values
)
Dans Flask, request.get_data() te donne les octets bruts. Dans FastAPI, utilise await request.body().
Vérifie la signature dans les outils d’automatisation
n8n
n8n vérifie la signature avec un nœud Code. Son JavaScript dispose du module crypto de Node.js : sur n8n Cloud, il est disponible ; sur un n8n auto-hébergé, tu l’autorises avec la variable d’environnement NODE_FUNCTION_ALLOW_BUILTIN=crypto (documentation n8n).
- Dans le nœud Webhook, choisis POST et active l’option Raw Body. n8n garde alors le body sous forme de données binaires. Repère le nom du champ binaire dans la sortie du nœud (en général
data). - Ajoute ensuite un nœud Code en mode « Run Once for All Items » avec ce code :
const crypto = require("crypto");
const secret = "whsec_…"; // your signing secret
const header = $input.first().json.headers["x-safina-signature"] ?? "";
const rawBody = await this.helpers.getBinaryDataBuffer(0, "data");
const parts = header.split(",").map((part) => part.trim().split("="));
const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
const expected = Buffer.from(
crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex")
);
const valid =
Number.isInteger(timestamp) &&
Math.abs(Date.now() / 1000 - timestamp) <= 300 &&
parts.some(
([key, value]) =>
key === "v1" &&
value?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(value), expected)
);
if (!valid) {
throw new Error("The Safina signature is not valid");
}
return [{ json: JSON.parse(rawBody.toString("utf8")) }];
Si la signature est fausse, le workflow s’arrête. Si elle est correcte, les nœuds suivants reçoivent l’événement analysé. Le nœud Crypto de n8n peut aussi calculer un HMAC (action Hmac, type SHA256, encodage HEX), mais tu dois d’abord construire le texte <t>.<body brut>, puis comparer les valeurs dans un nœud IF. Le nœud Code fait tout en une seule étape.
Zapier
Dans Zapier, utilise le trigger Webhooks by Zapier → Catch Raw Hook. Contrairement à Catch Hook, il te donne le body non analysé et les en-têtes de la requête (aide Zapier). Webhooks by Zapier nécessite l’offre Professional ou supérieure.
- Ajoute une étape Code by Zapier (Run JavaScript). Sous Input Data, associe
rawBodyau body brut du trigger etsignatureà l’en-têteX-Safina-Signature. - Colle ce code. La bibliothèque standard de Node.js, et donc
crypto, est disponible dans Code by Zapier (aide Zapier) :
const crypto = require("crypto");
const secret = "whsec_…"; // your signing secret
const parts = inputData.signature.split(",").map((part) => part.trim().split("="));
const timestamp = Number(parts.find(([key]) => key === "t")?.[1]);
const expected = Buffer.from(
crypto.createHmac("sha256", secret).update(`${timestamp}.`).update(inputData.rawBody).digest("hex")
);
const valid =
Number.isInteger(timestamp) &&
Math.abs(Date.now() / 1000 - timestamp) <= 300 &&
parts.some(
([key, value]) =>
key === "v1" &&
value?.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(value), expected)
);
output = { valid };
- Ajoute une étape Filter : continuer uniquement si
validvaut true.
Le secret se trouve dans le code de l’étape : toute personne qui peut modifier le Zap le voit. Catch Raw Hook ne découpe pas le JSON en champs. Pour associer des champs isolés dans les étapes suivantes, analyse le body dans la même étape de code et renvoie les champs dont tu as besoin.
Make
Make peut faire la vérification avec ses fonctions intégrées. La fonction sha256(text; [encoding]; [key]) renvoie un HMAC quand tu lui donnes une clé (aide Make). Dans le module Custom webhook, active JSON pass-through (tu reçois le JSON d’origine sous forme de texte) et, dans les paramètres avancés, Get request headers (aide Make). Ajoute ensuite un filtre après le webhook avec deux conditions :
tdans l’en-tête est à 300 secondes au plus deformatDate(now; "X")(secondes Unix).- L’en-tête
X-Safina-Signaturecontientv1=suivi desha256(<t>.<body brut>; hex; <ton secret>).
Cela fonctionne, mais le filtre ne compare pas en temps constant, et tu dois extraire t de l’en-tête avec split et get. Pour la plupart des scénarios Make, l’authentification du webhook est l’option la plus simple : Make peut protéger un custom webhook avec une clé API, qu’il attend dans l’en-tête x-make-apikey (aide Make). Dans Safina, choisis l’authentification « Clé API », l’emplacement « En-tête » et le nom d’en-tête x-make-apikey, puis saisis la même clé.
Erreurs fréquentes
- La signature ne correspond jamais : ton framework a analysé le JSON avant ta vérification. Utilise le body brut.
- La signature correspond, mais le contrôle de l’heure échoue : l’horloge de ton serveur est fausse. Synchronise-la avec NTP.
- Plus rien ne fonctionne après un nouveau secret : l’ancien secret n’est valable que 24 heures. Enregistre le nouveau secret dans ton récepteur.
- Le même événement arrive deux fois : c’est une nouvelle tentative après un timeout ou une erreur. Ignore un
event_idque tu as déjà traité.
En résumé
Active Signer les envois pour chaque webhook, enregistre le secret une fois et vérifie X-Safina-Signature avant de traiter un événement : heure, body brut, comparaison en temps constant. Ainsi, aucune fausse requête n’atteint ton CRM. Des questions ? Écris-nous à info@safina.ai.