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"]
  }'
La réponse contient le secret de signature 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énementEnvoyé quand
run.completedUn run se termine avec succès, ou une revue est approuvée.
run.failedUn run échoue, dépasse sa durée ou est rejeté en revue.
run.needs_reviewUn run attend une revue humaine.
batch.completedToutes les lignes d'un batch sont traitées.
batch.pausedUn batch se met en pause, par exemple plafond max_credits atteint.
schedule.findingUn loop ou un monitor détecte un changement.
campaign.needs_attentionUne étape de campagne dépasse le seuil d'échecs.
campaign.completedUne campagne se termine.
optimization.readyUne version optimisée d'un agent est prête à être activée.
wallet.low_balanceLe 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-Delivery ne change pas d'une tentative à l'autre : utilisez-le comme clé d'idempotence pour ne jamais traiter deux fois le même événement.