Comment vérifier qu'un webhook vient vraiment de Safina

Safina peut signer chaque webhook qu'elle envoie. Découvre comment activer la signature, vérifier l'en-tête X-Safina-Signature en Node.js ou Python, et contrôler les appels dans n8n, Zapier et Make.

Comment vérifier qu'un webhook vient vraiment de Safina Guides
Karsten Kreh Karsten Kreh
Mis à jour

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érequisDétails
Offre SafinaPro ou supérieure
Un webhook dans l’app SafinaAvec un secret de signature (voir ci-dessous)
Un récepteurTon 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": true avec POST /webhooks. La réponse contient signing_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
  • t est l’heure de la tentative de livraison, en secondes Unix.
  • v1 est 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 :

  1. Heure : refuse la requête si t est plus de 300 secondes (5 minutes) avant ou après ton horloge.
  2. 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.
  3. Comparaison : compare ta valeur à chaque valeur v1 en 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).

  1. 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).
  2. 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.

  1. Ajoute une étape Code by Zapier (Run JavaScript). Sous Input Data, associe rawBody au body brut du trigger et signature à l’en-tête X-Safina-Signature.
  2. 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 };
  1. Ajoute une étape Filter : continuer uniquement si valid vaut 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 :

  • t dans l’en-tête est à 300 secondes au plus de formatDate(now; "X") (secondes Unix).
  • L’en-tête X-Safina-Signature contient v1= suivi de sha256(<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_id que 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.

9:41

Safina a traité 51 appels cette semaine

46

Fiable

4

Suspect

1

Dangereux

7 derniers jours
Filter
EB
Emma Martin 67s 15:30

Souhaite discuter de l'offre pour la nouvelle campagne et a des questions sur le calendrier.

LS
Laura Dupont 54s 14:45

Demande le statut de la commande et la date de livraison.

TH
Thomas Petit 34s 13:10

Planifier une réunion pour la discussion projet la semaine prochaine.

Inconnu 44s 11:30

Promesse de gain – probablement du spam.

SM
Sophie Moreau 10s 09:15

Réclamation sur la dernière commande, demande un rappel.

MM
Michel Mercier 95s 13 déc.

Souhaite discuter d'une collaboration potentielle.

AR
Amélie Robert 85s 13 déc.

C'est votre collègue et elle souhaite discuter du projet.

JK
Julien Keller 42s 12 déc.

Se renseigne sur les créneaux disponibles la semaine prochaine.

LB
Louise Bernard 68s 12 déc.

A des questions sur la facture et demande des éclaircissements.

Appels
Safina
Contacts
Profil
9:41
Appel d'Emma Martin
12 déc.
11:30
67s
+33612345678

Souhaite discuter de l'offre pour la nouvelle campagne et a des questions sur le calendrier.

Points clés

  • Rappeler Emma Martin
  • Clarifier les questions de calendrier et de tarifs
Rappeler
Modifier le contact

Aperçus IA

Humeur de l'appelant Très bon

L'appelant était coopératif et a fourni les informations nécessaires.

Urgence Faible

L'appelant peut attendre une réponse.

Audio et transcription

0:16

Bonjour, ici Safina AI, l'assistante digitale de Peter. Comment puis-je vous aider ?

Bonjour Safina, ici Emma Martin. Je voulais discuter de l'offre et du calendrier.

Merci Emma. Hésitez-vous surtout entre le forfait Standard et le forfait Pro pour le lancement ?

Exactement. Nous avons besoin du forfait Pro et nous aimerions démarrer le mois prochain si l'onboarding est possible la première semaine.

Dites adieu à votre messagerie vocale dépassée.

Essayez Safina gratuitement et commencez à gérer vos appels intelligemment.

Safina Support Bot
Safina Support Bot

Bonjour ! 👋 Je suis l'assistant support de Safina. Pose-moi tes questions sur la configuration, le renvoi d'appel, les tarifs ou ton compte. Remarque : tes messages sont traités conformément à notre politique de confidentialité.

Tes messages sont traités conformément à notre politique de confidentialité