Webhooks
Recevoir les événements de FORGE sur votre serveur : création d'un endpoint, liste des événements, vérification de signature, idempotence et tentatives.
Les webhooks notifient votre système dès qu'un événement survient : run terminé, revue requise, batch en pause, changement détecté par un monitor. Chaque livraison est signée en HMAC-SHA256. Les endpoints se gèrent dans Développeurs ou par API, avec le rôle admin.
Créer un endpoint
curl -X POST https://api.forge.example/v1/webhooks \
-H "Authorization: Bearer $FORGE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.fr/hooks/forge",
"events": ["run.completed", "run.needs_review", "batch.completed"]
}'whsec_…. Il n'est affiché qu'une fois : enregistrez-le aussitôt dans votre gestionnaire de secrets. Sans liste events, l'endpoint reçoit tous les événements. L'URL doit être publique et en HTTPS.Événements
| Événement | Envoyé quand |
|---|---|
run.completed | Un run se termine avec succès, ou une revue est approuvée. |
run.failed | Un run échoue, dépasse sa durée ou est rejeté en revue. |
run.needs_review | Un run attend une revue humaine. |
batch.completed | Toutes les lignes d'un batch sont traitées. |
batch.paused | Un batch se met en pause, par exemple plafond max_credits atteint. |
schedule.finding | Un loop ou un monitor détecte un changement. |
campaign.needs_attention | Une étape de campagne dépasse le seuil d'échecs. |
campaign.completed | Une campagne se termine. |
optimization.ready | Une version optimisée d'un agent est prête à être activée. |
wallet.low_balance | Le solde de crédits passe sous le seuil d'alerte. |
Format de livraison
Chaque livraison est un POST JSON accompagné de trois en-têtes : X-Forge-Event (type d'événement), X-Forge-Delivery (identifiant de livraison, égal au champ id) et X-Forge-Signature. Le corps est une enveloppe { id, type, created_at, data }.
{
"id": "0f6c2d1e-…",
"type": "run.completed",
"created_at": "2026-10-12T09:40:00.000Z",
"data": {
"run_id": "5a7e…",
"agent_id": "8f1c…",
"cost_eur": 0.18,
"structured_output": { "siren": "123456789", "risque": "faible" }
}
}Vérifier la signature
L'en-tête X-Forge-Signature a la forme t=<horodatage>,v1=<signature>. La signature est un HMAC-SHA256 en hexadécimal, calculé avec votre secret sur la chaîne <t>.<corps brut>. Calculez-la sur le corps exact reçu, avant tout parsing JSON, comparez à temps constant et rejetez toute requête dont l'horodatage s'écarte de plus de 300 secondes de votre horloge.
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const SECRET = process.env.FORGE_WEBHOOK_SECRET!; // whsec_…
const TOLERANCE_S = 300;
export function verifyForgeSignature(rawBody: string, header: string | undefined): boolean {
if (!header) return false;
let t: number | undefined;
const signatures: string[] = [];
for (const part of header.split(',')) {
const [key, value] = part.split('=', 2);
if (key === 't') t = Number(value);
else if (key === 'v1' && value) signatures.push(value);
}
if (!t || signatures.length === 0) return false;
// Anti-rejeu : l'horodatage doit être dans la fenêtre de tolérance
if (Math.abs(Math.floor(Date.now() / 1000) - t) > TOLERANCE_S) return false;
// HMAC-SHA256 sur "<t>.<corps brut>"
const expected = createHmac('sha256', SECRET).update(t + '.' + rawBody).digest();
return signatures.some((sig) => {
const given = Buffer.from(sig, 'hex');
// Comparaison à temps constant
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
// En production : table ou Redis avec expiration
const processed = new Set<string>();
const app = express();
// Corps brut : la signature porte sur les octets reçus, pas sur un JSON re-sérialisé
app.post('/hooks/forge', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verifyForgeSignature(raw, req.header('X-Forge-Signature'))) {
return res.status(400).send('signature invalide');
}
// Idempotence : même identifiant de livraison à chaque tentative
const deliveryId = req.header('X-Forge-Delivery')!;
if (processed.has(deliveryId)) return res.sendStatus(200);
processed.add(deliveryId);
const event = JSON.parse(raw);
if (event.type === 'run.needs_review') {
// Créer une tâche pour vos analystes, en asynchrone
}
res.sendStatus(200);
});
app.listen(3000);Tentatives et idempotence
- Répondez par un code 2xx en moins de 10 secondes et traitez le travail lourd en asynchrone.
- Sur une réponse 408, 429 ou 5xx, un délai dépassé ou une erreur réseau, FORGE retente : 5 tentatives au total, avec un backoff exponentiel (1 s, 4 s, 16 s, puis 64 s).
- Les autres codes 4xx et les redirections 3xx sont des échecs définitifs : aucune nouvelle tentative.
X-Forge-Deliveryne change pas d'une tentative à l'autre : utilisez-le comme clé d'idempotence pour ne jamais traiter deux fois le même événement.