Les webhooks
Un webhook prévient votre propre service quand quelque chose arrive sur un projet : un déploiement sert, échoue, un domaine est vérifié, une variable change. Bastide poste un événement signé à l'adresse que vous déclarez, réessaie en cas d'échec, et garde un journal des livraisons. Cette page explique comment déclarer un point de réception, quels événements existent, et comment vérifier leur signature.
Les concepts
- Un point de réception est une adresse
https://de votre service, déclarée sur un projet, avec la liste des types d'événements qu'elle reçoit (tous, si la liste est vide). Son identifiant commence parwhk_. - Un événement est un fait du projet. Son identifiant (
evt_…) ne change jamais d'un envoi à l'autre. - Une livraison (
lvr_…) est l'envoi d'un événement à un point. Elle est en attente (pending), livrée (delivered) ou au rebut (dead). - La livraison est faite au moins une fois : un même événement peut arriver deux fois. Dédupliquez sur son
id.
Déclarer un point de réception
Il faut le rang Administrateur sur le projet.
- Ouvrez le projet, puis Webhooks.
- Dans « Déclarer un point de réception », saisissez l'adresse et, si vous voulez, les types d'événements à recevoir.
- Déclarer le point.
- Copiez le secret affiché (
whsec_…) et rangez-le dans votre service : il ne sera plus montré.
Par l'API, l'opération est webhook.endpoint.create :
{
"url": "https://exemple.fr/bastide/evenements",
"types": ["deployment.ready", "deployment.failed"]
}
L'écran Webhooks montre ensuite chaque point, ses livraisons et leur état. Depuis la carte d'un point : Faire tourner le secret en frappe un nouveau, et Supprimer retire le point avec ses livraisons en attente.
Note. Déclarer ou supprimer un point, et faire tourner son secret, n'a pas d'outil MCP : un agent ne crée pas de flux sortant et ne reçoit pas de secret. Un agent peut lire les livraisons (
webhook_list) et en rejouer une (webhook_replay_create).
L'enveloppe
Chaque envoi est un POST JSON :
{
"id": "evt_01JQ8ZK4M7NP2RSTVWXYZ34567",
"type": "deployment.ready",
"createdAt": "2026-10-28T09:14:02.120Z",
"attempt": 1,
"tenantId": "org_01JQ8ZK4M7NP2RSTVWXYZ34567",
"projectId": "prj_01JQ8ZK4M7NP2RSTVWXYZ34567",
"payload": { }
}
attemptvaut 1 au premier envoi et continue d'un envoi à l'autre, rejeu compris.- Les en-têtes
Bastide-Event-Id,Bastide-Event-TypeetBastide-Attemptrépètent ces valeurs, à côté deBastide-Signature. payload.actordit qui a fait le geste :accountIdpour une personne,serviceTokenIdpour un compte de service,channelpour la voie (console,api,cli,mcp, ouforgeethookpour un crochet). QuandagentSessionn'est pas nul, un agent a agi, etaccountIdest la personne qui l'a mandaté.actorest nul quand aucun appel n'est derrière le fait, par exemple un déploiement qui devient sain.- La valeur d'une variable n'est jamais transmise.
Les événements
| Type | Quand |
|---|---|
deployment.created | Un déploiement est créé et attend sa construction. |
deployment.built | La construction a produit une image signée. |
deployment.ready | Le déploiement sert du trafic. |
deployment.failed | Le déploiement a échoué ; la cause se lit par deployment.read. |
deployment.canceled | Le déploiement a été annulé avant de servir. |
deployment.restored | Un déploiement antérieur est remis en service. |
project.created | Un projet est créé. |
project.renamed | Un projet change de nom. |
project.deleted | Un projet est supprimé, au terme de sa grâce. |
project.anomaly.detected | Un environnement entre en anomalie (taux d'erreur, latence, saturation, redémarrages en boucle). |
project.egress.threshold | Le trafic sortant approche le plafond soutenu du palier. |
project.images.threshold | Les transformations d'images franchissent 80 % du plafond mensuel du palier. |
domain.added | Un domaine est ajouté. |
domain.verified | La propriété d'un domaine est vérifiée. |
domain.misconfigured | Le domaine ne pointe plus vers la plateforme ; il sera retiré dans sept jours s'il n'y pointe pas de nouveau. |
domain.deleted | Un domaine cesse d'être servi. |
variable.created, variable.updated, variable.deleted | Une variable d'environnement est créée, change de valeur ou est supprimée. |
database.branch.created, database.branch.destroyed | Une branche de base est créée ou détruite. |
database.cron.failed | Une tâche planifiée en base a échoué, ou n'a pas démarré dans sa fenêtre. |
database.schema.drifted | La base réelle s'écarte du schéma que déclarent les migrations du dépôt. |
D'autres types figurent déjà au catalogue mais ne sont pas encore émis : la promotion d'un déploiement, la suppression d'un déploiement seul, les certificats, les sauvegardes, les seuils de dépense et les mouvements de membres. Vous pouvez les cocher ; ils arriveront quand la fonction sera servie. Le catalogue complet est dans la page de référence de webhook.endpoint.create.
Vérifier la signature
Chaque envoi porte :
Bastide-Signature: t=<secondes Unix>,v1=<hex de HMAC-SHA256(secret, "<t>.<corps>")>
- Recalculez
HMAC-SHA256(secret, t + "." + corps)sur le corps brut, avant tout analyseur JSON. La clé est le secret entier, préfixewhsec_compris. - Acceptez si l'une des valeurs
v1est égale, par une comparaison à temps constant. - Refusez un
tà plus de 300 secondes de votre horloge : c'est la protection contre le rejeu.
Après une rotation, l'ancien secret signe encore 24 heures : l'en-tête porte alors deux v1, et votre service peut passer au nouveau secret sans rien perdre.
Un exemple en Node, sans dépendance :
import { createServer } from "node:http";
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.BASTIDE_WEBHOOK_SECRET; // whsec_…
const FENETRE_S = 300;
function signatureValide(entete, corps) {
if (typeof entete !== "string") return false;
const champs = entete.split(",").map((p) => p.trim().split("="));
const t = champs.find(([cle]) => cle === "t")?.[1];
const signatures = champs.filter(([cle]) => cle === "v1").map(([, valeur]) => valeur);
if (!t || !/^\d+$/.test(t) || signatures.length === 0) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > FENETRE_S) return false;
const attendue = createHmac("sha256", SECRET).update(`${t}.`).update(corps).digest();
return signatures.some((s) => {
const recue = Buffer.from(s, "hex");
return recue.length === attendue.length && timingSafeEqual(recue, attendue);
});
}
const dejaVus = new Set(); // en production : une table, avec l'identifiant en clé unique
createServer((req, res) => {
const morceaux = [];
req.on("data", (m) => morceaux.push(m));
req.on("end", () => {
const corps = Buffer.concat(morceaux);
if (!signatureValide(req.headers["bastide-signature"], corps)) {
res.writeHead(401).end();
return;
}
const evenement = JSON.parse(corps.toString("utf8"));
res.writeHead(204).end(); // répondre vite, traiter ensuite
if (dejaVus.has(evenement.id)) return;
dejaVus.add(evenement.id);
console.log(evenement.type, evenement.projectId);
});
}).listen(8080);
Les nouvelles tentatives
- Une livraison réussit sur une réponse 2xx, reçue en entier en moins de 10 secondes. Une redirection n'est pas suivie : c'est un échec.
- En échec, Bastide réessaie 8 fois en 24 heures, en recul exponentiel :
| Tentative | Instant, depuis la première |
|---|---|
| 1 | à l'émission |
| 2 | +1 min |
| 3 | +5 min |
| 4 | +17 min |
| 5 | +53 min |
| 6 | +2 h 40 min |
| 7 | +8 h |
| 8 | +24 h |
- Après la 8e tentative en échec, la livraison passe au rebut. Rejouez-la depuis l'écran Webhooks (Rejouer) ou par
webhook_replay_create: une tentative immédiate, puis le rebut si elle échoue encore. Une livraison livrée se rejoue aussi. - Le journal des livraisons garde le code de réponse, le corps de réponse tronqué à 4 096 octets et la durée, pendant 30 jours, comme le rebut.
Les limites
Elles ne dépendent pas du palier.
- 25 points de réception au plus par organisation (
quota_de_points_atteint). https://seulement, sans identifiant ni mot de passe dans l'adresse.- Une adresse interne (boucle locale, réseau privé, lien local, métadonnées d'hébergeur) est refusée, et vérifiée à chaque envoi sur l'adresse vers laquelle le nom résout.
Les crochets de déploiement
Le sens inverse existe aussi : un crochet de déploiement est une adresse que votre outil appelle pour déclencher un déploiement d'une branche dans un environnement, sans compte ni jeton, chaque appel étant signé de la même façon. Il se crée sur le même écran Webhooks, au rang Mainteneur ; son secret (dhsec_…) n'est montré qu'une fois, et un projet en porte au plus 20. Voir Déployer.
Erreurs fréquentes
| Symptôme | Cause | Remède |
|---|---|---|
| Toutes les livraisons échouent, signature refusée par votre service | Le corps a été analysé puis resérialisé avant le calcul. | Calculez sur le corps brut, tel que reçu. |
| Signature refusée par intermittence | L'horloge de votre serveur dérive de plus de cinq minutes. | Synchronisez l'horloge (NTP). |
| Livraisons en échec sur une réponse 301 ou 302 | Les redirections ne sont pas suivies. | Déclarez l'adresse finale. |
| Événement traité deux fois | La livraison est au moins une fois. | Dédupliquez sur id. |
| Échecs après un délai | Votre service met plus de 10 secondes à répondre. | Répondez tout de suite, traitez ensuite. |