# Webhooks

Plutôt que d'interroger l'API en boucle, donnez-nous une adresse : nous l'appelons dès qu'un envoi est terminé.

## Configurer

Sur la page **API** de votre espace ([france-verification.com/mon-api](https://france-verification.com/mon-api)) :

1. saisissez l'adresse de votre serveur (obligatoirement en `https://`, et accessible depuis Internet) ;
2. copiez le **secret de signature** (`whsec_...`) dans votre configuration ;
3. cliquez sur « Envoyer un webhook de test » : vous recevez un évènement `ping`.

Une seule adresse par compte. Les envois de la clé test déclenchent aussi le webhook, ce qui permet de tout tester sans crédit.

## Ce que nous envoyons

Une requête `POST`, corps JSON :

```json
{
  "id": "evt_9120",
  "type": "submission.completed",
  "created_at": "2026-09-28T14:49:17Z",
  "data": {
    "id": "sub_1842",
    "mode": "live",
    "status": "completed",
    "...": "l'objet Submission complet, au format minimal"
  }
}
```

| Évènement | Quand |
|---|---|
| `submission.completed` | Toutes les vérifications de l'envoi sont terminées (et le dossier éventuel aussi). `data` = l'objet [Submission](/developpeurs/objets). |
| `ping` | Webhook de test, déclenché depuis la page API. |

En-têtes :

| En-tête | Contenu |
|---|---|
| `X-FV-Event` | Le type de l'évènement. |
| `X-FV-Delivery` | L'identifiant de l'évènement (`evt_...`), identique à `id` dans le corps. |
| `X-FV-Signature` | `t=<horodatage unix>,v1=<signature>` |
| `User-Agent` | `FranceVerification-Webhooks/1.0` |

## Répondre

Répondez un code `2xx` en moins de 10 secondes. Faites le traitement lourd après avoir répondu (file d'attente de votre côté).

Sans réponse `2xx`, nous réessayons 5 fois : après 1 minute, 5 minutes, 30 minutes, 2 heures, puis 6 heures. Ensuite l'évènement est abandonné, mais le résultat reste lisible par `GET /submissions/{id}`. Le même évènement peut donc arriver plus d'une fois : dédoublonnez sur `id`.

## Vérifier la signature

La signature prouve que l'appel vient de nous. Calculez un HMAC SHA-256 de `<t>.<corps brut>` avec votre secret, et comparez-le à `v1`. Refusez aussi un horodatage vieux de plus de 5 minutes.

```php
<?php
$secret = getenv('FV_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_FV_SIGNATURE'] ?? ''), $sig);

$expected = hash_hmac('sha256', $sig['t'] . '.' . $body, $secret);
if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) $sig['t']) > 300) {
    http_response_code(400);
    exit;
}

$event = json_decode($body, true);
http_response_code(200);
```

```python
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(item.split("=", 1) for item in header.split(","))
    expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", "")) and abs(time.time() - int(parts["t"])) <= 300
```

```javascript
import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= 300;
  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}
```

Important : signez le corps **brut**, tel que reçu, avant tout décodage JSON.

## Changer de secret

« Régénérer le secret » sur la page API crée un nouveau secret : l'ancien ne signe plus rien dès cet instant. Mettez votre configuration à jour aussitôt.
