# Webhooks

> Événements webhook nouko, contenu envoyé, 6 essais automatiques, rejeu 30 jours et récepteurs prêts à l’emploi en Node.js, Python, PHP et Go.

Source : https://nouko.openclouds.fr/docs/webhooks

nouko envoie un `POST` JSON signé à votre serveur à chaque changement d’état. Abonnez chaque adresse à tous les événements ou à une sélection, et choisissez l’environnement (sandbox, production ou les deux).

## Événements

| Type | Quand |
| --- | --- |
| `session.created` | La session vient d’être créée |
| `session.started` | Le client a ouvert le parcours |
| `session.submitted` | Le client a soumis ses pièces |
| `session.in_review` | Une personne examine le dossier |
| `session.approved` | Identité vérifiée |
| `session.declined` | Vérification refusée |
| `session.resubmission_requested` | Pièces à refaire |
| `session.expired` | Lien expiré |
| `session.cancelled` | Session annulée |
| `webhook.test` | Envoi de test depuis la console |

## Contenu envoyé

```
POST /webhooks/nouko HTTP/1.1
Content-Type: application/json
Nouko-Signature: t=1760000000,v1=5f2b9c…
Nouko-Event: session.approved
Nouko-Event-Id: 0c9d4e…
Nouko-Delivery-Id: 81aa…

{
  "id": "0c9d4e…",
  "type": "session.approved",
  "created_at": "2026-10-17T18:12:00.000Z",
  "environment": "production",
  "data": {
    "id": "6f1c2a9e-…",
    "reference": "client-42",
    "status": "approved",
    "country": "BJ",
    "tier": 2,
    "achieved_tier": 2,
    "metadata": { "user_id": "42" }
  }
}
```

## Récepteur prêt à l’emploi

Chaque exemple lit le corps brut, vérifie la signature, répond tout de suite puis traite l’événement en arrière-plan. La fonction de vérification est sur la page [Signature](https://nouko.openclouds.fr/docs/signature).

**Node.js**

```javascript
// Express — npm i express
import express from 'express';
import { verifyNouko } from './nouko.js';

const app = express();
// Corps brut obligatoire pour vérifier la signature
app.post('/webhooks/nouko', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyNouko(req.body, req.get('Nouko-Signature') ?? '', process.env.NOUKO_WEBHOOK_SECRET);
  if (!ok) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(200);           // répondre vite…
  queueMicrotask(() => handle(event)); // …traiter ensuite
});

async function handle(event) {
  if (await db.processedEvents.has(event.id)) return; // dédoublonnage
  if (event.type === 'session.approved') await db.users.markVerified(event.data.metadata.user_id);
  if (event.type === 'session.declined') await db.users.markDeclined(event.data.metadata.user_id);
  await db.processedEvents.add(event.id);
}

app.listen(3000);
```

**Python**

```python
# FastAPI — pip install fastapi uvicorn
import json, os
from fastapi import BackgroundTasks, FastAPI, Request, Response

app = FastAPI()

@app.post("/webhooks/nouko")
async def nouko_webhook(request: Request, tasks: BackgroundTasks):
    raw = await request.body()  # corps brut
    if not verify_nouko(raw, request.headers.get("Nouko-Signature", ""),
                        os.environ["NOUKO_WEBHOOK_SECRET"]):
        return Response(status_code=400)
    tasks.add_task(handle, json.loads(raw))  # traité après la réponse
    return Response(status_code=200)

def handle(event: dict):
    if already_processed(event["id"]):
        return
    if event["type"] == "session.approved":
        mark_verified(event["data"]["metadata"]["user_id"])
    remember(event["id"])
```

**PHP**

```php
<?php
// Laravel — routes/api.php (excluez la route du contrôle CSRF)
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post('/webhooks/nouko', function (Request $request) {
    $raw = $request->getContent(); // corps brut
    if (!verify_nouko($raw, $request->header('Nouko-Signature', ''), config('services.nouko.webhook_secret'))) {
        abort(400);
    }
    $event = json_decode($raw, true);
    ProcessNoukoEvent::dispatch($event); // job en file d'attente, dédoublonné sur $event['id']
    return response('', 200);
});
```

**Go**

```go
// net/http
package main

import (
	"encoding/json"
	"io"
	"net/http"
	"os"
)

func noukoWebhook(w http.ResponseWriter, r *http.Request) {
	raw, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))
	if err != nil || !VerifyNouko(raw, r.Header.Get("Nouko-Signature"), os.Getenv("NOUKO_WEBHOOK_SECRET")) {
		http.Error(w, "invalid signature", http.StatusBadRequest)
		return
	}
	var event struct {
		ID   string          `json:"id"`
		Type string          `json:"type"`
		Data json.RawMessage `json:"data"`
	}
	json.Unmarshal(raw, &event)
	w.WriteHeader(http.StatusOK)
	go handle(event.ID, event.Type, event.Data) // dédoublonnez sur event.ID
}

func main() {
	http.HandleFunc("/webhooks/nouko", noukoWebhook)
	http.ListenAndServe(":3000", nil)
}
```

## Livraison et réessais

- Répondez avec un code `2xx` en moins de 8 secondes. Tout autre code, ou un dépassement de délai, est un échec.
- En cas d’échec, nouko réessaie après 1 min, 5 min, 30 min, 2 h, 12 h (6 essais au total), puis marque la livraison en échec.
- Après 20 livraisons en échec consécutives, l’adresse est mise en pause.
- Rejeu manuel possible depuis la console pendant 30 jours.
- Les événements peuvent arriver **en double** ou **dans le désordre** : dédoublonnez sur `id`, et relisez la session via l’API si l’ordre compte.

> **Note :** Pour votre sécurité, chaque adresse est validée par l’équipe nouko avant le premier envoi. Les adresses privées, locales et les redirections sont refusées.
