Aller au contenu
Bastide Connexion Créer un compte
Appuyez sur / pour chercher

Les webhooks

EnglishMarkdownv0.3.0

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 par whk_.
  • 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.

  1. Ouvrez le projet, puis Webhooks.
  2. Dans « Déclarer un point de réception », saisissez l'adresse et, si vous voulez, les types d'événements à recevoir.
  3. Déclarer le point.
  4. 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": { }
}
  • attempt vaut 1 au premier envoi et continue d'un envoi à l'autre, rejeu compris.
  • Les en-têtes Bastide-Event-Id, Bastide-Event-Type et Bastide-Attempt répètent ces valeurs, à côté de Bastide-Signature.
  • payload.actor dit qui a fait le geste : accountId pour une personne, serviceTokenId pour un compte de service, channel pour la voie (console, api, cli, mcp, ou forge et hook pour un crochet). Quand agentSession n'est pas nul, un agent a agi, et accountId est la personne qui l'a mandaté. actor est 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

TypeQuand
deployment.createdUn déploiement est créé et attend sa construction.
deployment.builtLa construction a produit une image signée.
deployment.readyLe déploiement sert du trafic.
deployment.failedLe déploiement a échoué ; la cause se lit par deployment.read.
deployment.canceledLe déploiement a été annulé avant de servir.
deployment.restoredUn déploiement antérieur est remis en service.
project.createdUn projet est créé.
project.renamedUn projet change de nom.
project.deletedUn projet est supprimé, au terme de sa grâce.
project.anomaly.detectedUn environnement entre en anomalie (taux d'erreur, latence, saturation, redémarrages en boucle).
project.egress.thresholdLe trafic sortant approche le plafond soutenu du palier.
project.images.thresholdLes transformations d'images franchissent 80 % du plafond mensuel du palier.
domain.addedUn domaine est ajouté.
domain.verifiedLa propriété d'un domaine est vérifiée.
domain.misconfiguredLe domaine ne pointe plus vers la plateforme ; il sera retiré dans sept jours s'il n'y pointe pas de nouveau.
domain.deletedUn domaine cesse d'être servi.
variable.created, variable.updated, variable.deletedUne variable d'environnement est créée, change de valeur ou est supprimée.
database.branch.created, database.branch.destroyedUne branche de base est créée ou détruite.
database.cron.failedUne tâche planifiée en base a échoué, ou n'a pas démarré dans sa fenêtre.
database.schema.driftedLa 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>")>
  1. Recalculez HMAC-SHA256(secret, t + "." + corps) sur le corps brut, avant tout analyseur JSON. La clé est le secret entier, préfixe whsec_ compris.
  2. Acceptez si l'une des valeurs v1 est égale, par une comparaison à temps constant.
  3. 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 :
TentativeInstant, 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ômeCauseRemède
Toutes les livraisons échouent, signature refusée par votre serviceLe corps a été analysé puis resérialisé avant le calcul.Calculez sur le corps brut, tel que reçu.
Signature refusée par intermittenceL'horloge de votre serveur dérive de plus de cinq minutes.Synchronisez l'horloge (NTP).
Livraisons en échec sur une réponse 301 ou 302Les redirections ne sont pas suivies.Déclarez l'adresse finale.
Événement traité deux foisLa livraison est au moins une fois.Dédupliquez sur id.
Échecs après un délaiVotre service met plus de 10 secondes à répondre.Répondez tout de suite, traitez ensuite.

Voir aussi

Signaler une erreur sur cette page