> ## Documentation Index
> Fetch the complete documentation index at: https://verifner.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Recevez chaque verdict sur votre serveur, signé et renvoyé jusqu'à réception.

À chaque verdict, VerifNer envoie un `POST` à vos endpoints avec l'événement `verification.completed`. Quand un humain tranche une vérification en revue, un second `verification.completed` part avec `final_verdict` renseigné.

## Enregistrer un endpoint

Depuis la console, page **Webhooks**, ou par l'API :

```bash theme={"dark"}
curl https://api.verifner.com/v1/webhooks \
  -H "Authorization: Bearer $VERIFNER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://votre-organisation.ne/hooks/verifner", "description": "Production"}'
```

```json theme={"dark"}
{
  "id": "whe_4k2m9x7q1c5v8b3n6z0a",
  "url": "https://votre-organisation.ne/hooks/verifner",
  "description": "Production",
  "created_at": "2026-09-26T10:00:00.000Z",
  "secret": "whsec_Hq3...Zk"
}
```

* L'URL doit être en **HTTPS**. `http://localhost` est accepté pour vos tests.
* Le `secret` ne s'affiche qu'une fois : il sert à vérifier la signature.
* Chaque endpoint reçoit les événements de son [application](/console/applications) (celle de la clé qui l'a créé, ou celle choisie dans la console), en test comme en live. Les champs `data.app_id` et `data.mode` vous disent lesquels.

## L'événement

```http theme={"dark"}
POST /hooks/verifner HTTP/1.1
Content-Type: application/json
VerifNer-Event: verification.completed
VerifNer-Delivery: whd_8f1c...
VerifNer-Signature: t=1790416800,v1=5d41402abc4b2a76b9719d911017c592...
```

```json theme={"dark"}
{
  "id": "evt_q8w2e4r6t8y0u2i4o6p8",
  "type": "verification.completed",
  "created_at": "2026-09-26T10:00:06.000Z",
  "data": {
    "id": "vrf_w6e35w8a9iehsf6shzuw",
    "mode": "live",
    "app_id": "app_4k2m9x7q1c5v8b3n6z0a",
    "verdict": "review",
    "final_verdict": null,
    "score": 0.88,
    "document_type": "cni",
    "document": { "...": "..." },
    "checks": [ "..." ],
    "latency_ms": 6120,
    "created_at": "2026-09-26T10:00:00.000Z"
  }
}
```

`data` est la vérification complète, comme dans la réponse de `POST /v1/verify`, avec `mode` et `final_verdict` en plus. Pour retrouver votre dossier, conservez l'`id` de la vérification, ou la `reference` passée à la session et lisible avec `GET /v1/sessions/{id}`.

## Vérifier la signature

L'en-tête `VerifNer-Signature` contient un horodatage `t` (secondes Unix) et `v1`, le HMAC-SHA256 en hexadécimal de la chaîne `"<t>.<corps brut>"`, calculé avec le secret de l'endpoint.

<Warning>
  Calculez le HMAC sur le **corps brut** reçu, avant tout parsing JSON. Re-sérialiser le JSON change les octets et la signature ne correspond plus.
</Warning>

<CodeGroup>
  ```javascript Node.js (Express) theme={"dark"}
  import crypto from 'node:crypto';
  import express from 'express';

  const app = express();

  app.post('/hooks/verifner', express.raw({ type: 'application/json' }), (req, res) => {
    const header = req.get('VerifNer-Signature') ?? '';
    const parts = Object.fromEntries(header.split(',').map((kv) => kv.trim().split('=')));
    const t = Number(parts.t);
    const body = req.body.toString('utf8');

    const expected = crypto
      .createHmac('sha256', process.env.VERIFNER_WEBHOOK_SECRET)
      .update(`${t}.${body}`)
      .digest('hex');
    const fresh = Math.abs(Date.now() / 1000 - t) <= 300;
    const valid =
      fresh &&
      typeof parts.v1 === 'string' &&
      parts.v1.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
    if (!valid) return res.sendStatus(400);

    const event = JSON.parse(body);
    // Traitez l'événement de façon idempotente : event.id peut arriver plusieurs fois.
    handle(event).catch(console.error);
    res.sendStatus(200);
  });
  ```

  ```python Python (Flask) theme={"dark"}
  import hashlib, hmac, json, os, time
  from flask import Flask, request, abort

  app = Flask(__name__)
  SECRET = os.environ["VERIFNER_WEBHOOK_SECRET"].encode()

  @app.post("/hooks/verifner")
  def verifner_hook():
      header = request.headers.get("VerifNer-Signature", "")
      parts = dict(kv.strip().split("=", 1) for kv in header.split(",") if "=" in kv)
      body = request.get_data(as_text=True)
      try:
          t = int(parts.get("t", ""))
      except ValueError:
          abort(400)
      expected = hmac.new(SECRET, f"{t}.{body}".encode(), hashlib.sha256).hexdigest()
      if abs(time.time() - t) > 300 or not hmac.compare_digest(parts.get("v1", ""), expected):
          abort(400)

      event = json.loads(body)
      # Traitez l'événement de façon idempotente : event["id"] peut arriver plusieurs fois.
      handle(event)
      return "", 200
  ```

  ```php PHP theme={"dark"}
  <?php
  $body = file_get_contents('php://input');
  $header = $_SERVER['HTTP_VERIFNER_SIGNATURE'] ?? '';
  $parts = [];
  foreach (explode(',', $header) as $kv) {
      [$k, $v] = array_pad(explode('=', trim($kv), 2), 2, '');
      $parts[$k] = $v;
  }
  $t = (int) ($parts['t'] ?? 0);
  $expected = hash_hmac('sha256', $t . '.' . $body, getenv('VERIFNER_WEBHOOK_SECRET'));
  if (abs(time() - $t) > 300 || !hash_equals($expected, $parts['v1'] ?? '')) {
      http_response_code(400);
      exit;
  }
  $event = json_decode($body, true);
  // Traitez l'événement de façon idempotente : $event['id'] peut arriver plusieurs fois.
  http_response_code(200);
  ```
</CodeGroup>

Refusez un horodatage de plus de 5 minutes : cela empêche de rejouer un ancien événement intercepté.

## Répondre et réessayer

* Répondez avec un statut **2xx** en moins de **10 secondes**. Faites le travail lourd en arrière-plan.
* Sans 2xx (erreur, délai dépassé, serveur injoignable), VerifNer réessaie avec un délai qui double à chaque fois, à partir d'une minute : **8 tentatives** au total, sur environ quatre heures.
* Un même événement garde le même `id` à chaque tentative. Traitez-le de façon **idempotente** : ignorez un `id` déjà traité.
* Les événements peuvent arriver dans le désordre. Pour une vérification donnée, un événement avec `final_verdict` renseigné est toujours le plus récent.

## Suivre les livraisons

`GET /v1/webhooks/{id}/deliveries` renvoie les 50 dernières livraisons d'un endpoint, avec leur état, le nombre de tentatives et la dernière erreur. La console les affiche aussi, page **Webhooks**.

Pour supprimer un endpoint : `DELETE /v1/webhooks/{id}`.
