Aller au contenu
Préproduction : l'API répond sur https://api.visiobank.com/v1, avec une clé de test. Les exemples gardent l'adresse de production, api.vizalia.ai, qui ouvrira à la mise en production.

Vérifier la signature

N’importe qui peut envoyer un POST à votre URL. La signature prouve que l’envoi vient de Vizalia et n’a pas été modifié : vérifiez-la sur chaque envoi, avant de lire le corps.

En-tête Contenu
webhook-id

Identifiant de l'événement (evt_…), le même à chaque nouvelle tentative. Gardez-le pour ignorer un doublon.

webhook-timestamp

Horodatage Unix de l'envoi, en secondes. Il entre dans la signature ; refusez un envoi décalé de plus de 5 minutes.

webhook-signature

Signatures séparées par une espace, chacune au format v1,<base64> : HMAC-SHA256 de <webhook-id>.<webhook-timestamp>.<corps brut>, avec pour clé le secret de l'abonnement décodé (la partie base64 qui suit whsec_). L'envoi est authentique si l'une d'elles correspond ; il y en a deux pendant un renouvellement de secret. Les bibliothèques Standard Webhooks font ce calcul.

Le paquet standardwebhooks existe en Node.js, Python, Ruby, PHP, Go, Java et d’autres langages (liste). Passez-lui le secret de l’abonnement tel quel, whsec_ compris, le corps brut et les en-têtes.

recepteur.mjs
// Réception des webhooks avec Express et la bibliothèque officielle :
// npm install express standardwebhooks
import express from 'express';
import { Webhook } from 'standardwebhooks';
const webhook = new Webhook(process.env.VIZALIA_WEBHOOK_SECRET); // whsec_…, tel quel
const app = express();
// Le corps brut, octet pour octet : la signature porte sur lui, pas sur un JSON relu.
app.post('/webhooks/vizalia', express.raw({ type: 'application/json' }), (req, res) => {
let evenement;
try {
evenement = webhook.verify(req.body, req.headers);
} catch {
return res.status(400).send('Signature invalide');
}
// Répondre vite (moins de 15 secondes), traiter ensuite.
res.sendStatus(204);
traiter(evenement, req.headers['webhook-id']);
});
function traiter(evenement, idEnvoi) {
// idEnvoi (= evenement.id) ne change pas d'une tentative à l'autre : gardez-le
// pour ignorer un doublon. Les envois peuvent arriver dans le désordre.
console.log(idEnvoi, evenement.type, evenement.session_id);
}
app.listen(3000);

Sans bibliothèque, la vérification tient en trois étapes : refuser un horodatage décalé de plus de 5 minutes, calculer le HMAC-SHA256 de id.horodatage.corps avec le secret décodé, puis comparer en temps constant avec chacune des signatures reçues.

verifier.mjs
// Vérification sans bibliothèque (Node.js), pour comprendre ou pour un
// langage sans bibliothèque Standard Webhooks.
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifier(secret, corpsBrut, entetesRecus, tolerance = 300) {
// Les noms d'en-têtes ne tiennent pas compte de la casse.
const entetes = Object.fromEntries(Object.entries(entetesRecus).map(([nom, valeur]) => [nom.toLowerCase(), valeur]));
const id = entetes['webhook-id'];
const horodatage = entetes['webhook-timestamp'];
const signatures = entetes['webhook-signature'];
if (!id || !horodatage || !signatures) throw new Error('En-têtes de signature absents');
// 1. Refuser un envoi décalé de plus de 5 minutes (rejeu).
if (Math.abs(Date.now() / 1000 - Number(horodatage)) > tolerance) throw new Error('Horodatage hors délai');
// 2. HMAC-SHA256 de « id.horodatage.corps », avec le secret décodé (la partie base64 après whsec_).
const cle = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const attendue = createHmac('sha256', cle).update(`${id}.${horodatage}.${corpsBrut}`).digest();
// 3. Authentique si l'une des signatures correspond (il y en a deux pendant un renouvellement de secret).
const valide = signatures.split(' ').some((signature) => {
const [version, valeur] = signature.split(',');
const recue = Buffer.from(valeur ?? '', 'base64');
return version === 'v1' && recue.length === attendue.length && timingSafeEqual(recue, attendue);
});
if (!valide) throw new Error('Signature invalide');
return JSON.parse(corpsBrut);
}

Ces trois fonctions sont testées contre la bibliothèque officielle : même signature valide, même refus d’un corps modifié, d’un autre secret ou d’un envoi vieux de plus de 5 minutes, et deux signatures acceptées pendant un renouvellement de secret.

Les envois arrivent au moins une fois, sans ordre garanti. webhook-id (égal au champ id de l’événement) reste le même d’une tentative à l’autre : enregistrez-le en traitant l’événement, et ignorez un webhook-id déjà vu. En cas de doute sur l’état d’une session, relisez-la avec GET /sessions/{session_id}.

POST /webhook_endpoints/{endpoint_id}/test envoie tout de suite un événement fictif signé à votre URL, et renvoie la réponse de votre serveur. Un 2xx dit que la vérification passe ; un 400 de votre part, qu’elle échoue. Voir Mode test.