> ## Documentation Index
> Fetch the complete documentation index at: https://developers.0flaw.fr/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Recevez les événements de sécurité dans votre SIEM, en quelques secondes, avec des livraisons signées.

0flaw pousse les événements vers votre endpoint HTTPS. Chaque livraison est signée : vous pouvez
prouver qu'elle vient bien de nous.

## Configurer un endpoint

Dans le dashboard : **Réglages → Webhooks**. Renseignez votre URL, cochez les événements
souhaités, puis copiez le **secret de signature** — il n'est affiché qu'une seule fois. Le bouton
**Tester** envoie immédiatement un `webhook.ping` pour valider votre réception.

<Warning>
  **Contraintes sur l'URL.** Elle doit être en **HTTPS**, et les adresses réseau internes
  (`127.0.0.1`, `10.0.0.0/8`, `192.168.0.0/16`, `169.254.169.254`…) sont refusées. C'est une
  protection contre les attaques SSRF : notre serveur ne doit pas pouvoir servir de relais vers un
  réseau privé. Les redirections ne sont pas suivies.
</Warning>

## Événements disponibles

| Type                             | Déclenché quand                                        |
| -------------------------------- | ------------------------------------------------------ |
| `phishing.link.clicked`          | Un employé clique sur le lien d'un email de phishing   |
| `phishing.credentials.submitted` | Un employé saisit ses identifiants sur une fausse page |
| `phishing.email.reported`        | Un employé signale un email suspect                    |
| `campaign.finished`              | Une campagne se termine                                |
| `training.completed`             | Un employé termine une formation assignée              |
| `quiz.completed`                 | Un employé termine un quiz                             |

Les événements sont détectés en continu et livrés en général en **moins de trente secondes**.

<Note>
  Seuls les événements survenus **après** la création de l'abonnement sont envoyés : nous ne
  rejouons pas l'historique.
</Note>

## Format des livraisons

Toutes partagent la même enveloppe ; seul `data` varie selon le `type`.

```json theme={null}
{
  "id": "pet:aa4a6435a97fb152ee899964e390e42b:clicked",
  "type": "phishing.link.clicked",
  "created_at": "2026-07-22T11:12:04.000Z",
  "entreprise_id": "00000000-0000-0000-0000-000000000000",
  "data": {
    "employe": { "id": "2ddee9a7-…", "email": "jane.doe@acme.com" },
    "campagne": { "id": 1290, "nom": "Campagne trimestrielle" },
    "clicked_at": "2026-07-22T11:12:04.000Z"
  }
}
```

| En-tête             | Contenu                              |
| ------------------- | ------------------------------------ |
| `X-0flaw-Event`     | Le type d'événement                  |
| `X-0flaw-Delivery`  | Identifiant de la livraison          |
| `X-0flaw-Signature` | Signature HMAC, au format `t=…,v1=…` |

<Info>
  **`id` est votre clé d'idempotence.** Un même événement métier porte toujours le même `id` et ne
  vous sera jamais livré deux fois. Si un employé clique dix fois, vous recevez une seule
  livraison.
</Info>

## Vérifier la signature

Vérifiez **systématiquement** la signature avant de traiter une livraison. Sans cette étape,
quiconque connaît votre URL pourrait vous injecter de faux incidents.

L'en-tête contient un horodatage et une signature : `X-0flaw-Signature: t=1784718035,v1=8f794a…`.
La signature est un **HMAC-SHA256**, calculé avec votre secret sur la chaîne `` `${t}.${corps_brut}` ``.

<Warning>
  **Signez le corps brut, pas le JSON reparsé.** Utilisez les octets exacts reçus. Si vous parsez
  puis re-sérialisez le JSON avant de calculer le HMAC, l'ordre des clés ou l'espacement changent
  et la signature ne correspondra jamais. C'est de loin l'erreur la plus fréquente.
</Warning>

<CodeGroup>
  ```javascript Node.js (Express) theme={null}
  const crypto = require('crypto');

  // Important : récupérer le corps BRUT, pas l'objet parsé
  app.post('/hooks/0flaw', express.raw({ type: 'application/json' }), (req, res) => {
    const header = req.get('X-0flaw-Signature') || '';
    const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));

    // 1. Rejeter les livraisons trop anciennes (protection anti-rejeu)
    if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);

    // 2. Recalculer la signature
    const expected = crypto
      .createHmac('sha256', process.env.OFLAW_WEBHOOK_SECRET)
      .update(`${t}.${req.body}`)
      .digest('hex');

    // 3. Comparer en temps constant
    const ok =
      v1.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(v1, 'hex'), Buffer.from(expected, 'hex'));
    if (!ok) return res.sendStatus(401);

    const event = JSON.parse(req.body.toString('utf8'));
    // … traitez l'événement, puis répondez rapidement
    res.sendStatus(200);
  });
  ```

  ```python Python (Flask) theme={null}
  import hmac, hashlib, time
  from flask import request, abort

  @app.post("/hooks/0flaw")
  def oflaw_webhook():
      header = request.headers.get("X-0flaw-Signature", "")
      parts = dict(p.split("=", 1) for p in header.split(","))
      t, v1 = parts.get("t", ""), parts.get("v1", "")

      # 1. Anti-rejeu : tolérance de 5 minutes
      if abs(time.time() - int(t)) > 300:
          abort(400)

      # 2. Recalcul sur le corps BRUT
      raw = request.get_data()
      expected = hmac.new(
          OFLAW_WEBHOOK_SECRET.encode(),
          f"{t}.".encode() + raw,
          hashlib.sha256,
      ).hexdigest()

      # 3. Comparaison en temps constant
      if not hmac.compare_digest(v1, expected):
          abort(401)

      event = request.get_json()
      return "", 200
  ```
</CodeGroup>

## Ce que nous attendons de votre endpoint

<Steps>
  <Step title="Répondez 2xx">
    N'importe quel code 2xx accuse réception.
  </Step>

  <Step title="Répondez vite">
    Moins de 5 secondes — au-delà nous considérons la livraison en échec. Mettez l'événement dans
    votre propre file et traitez-le ensuite.
  </Step>

  <Step title="Ne dépendez pas de l'ordre d'arrivée">
    Deux événements proches peuvent arriver dans le désordre.
  </Step>
</Steps>

## Réessais et mise en pause

Si votre endpoint ne répond pas, nous réessayons avec un espacement croissant :

| Tentative | Délai après l'échec précédent |
| --------- | ----------------------------- |
| 2ᵉ        | 1 minute                      |
| 3ᵉ        | 5 minutes                     |
| 4ᵉ        | 30 minutes                    |
| 5ᵉ        | 2 heures                      |
| dernière  | 6 heures                      |

* Nous réessayons sur les **erreurs réseau**, les **5xx** et les **429**.
* Nous **abandonnons immédiatement sur une 4xx** (hors 429) : elle signale une configuration
  incorrecte, insister n'aiderait pas.
* Après **dix livraisons abandonnées consécutives**, l'endpoint est **mis en pause
  automatiquement** et signalé dans le dashboard. Corrigez, puis réactivez-le d'un clic.

<Tip>
  Vous pouvez régénérer le secret à tout moment depuis le dashboard. L'ancien cesse immédiatement
  d'être valide : prévoyez de mettre à jour votre configuration dans la foulée.
</Tip>
