# Documentation complète nouko

> Toutes les pages de https://nouko.openclouds.fr/docs en Markdown.

---

# Documentation API nouko

> Intégrez la vérification d’identité (KYC) avec revue humaine de nouko : sessions, webhooks signés, sandbox et exemples de code prêts à l’emploi.

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

L’API nouko vous permet de vérifier l’identité de vos clients au Bénin et en Côte d’Ivoire sans gérer vous-même les pièces d’identité. Vous créez une **session** depuis votre serveur, votre client suit un parcours sécurisé sur son téléphone, et vous recevez la décision par **webhook signé**.

> **Note :** Une personne examine chaque cas ambigu. Un fournisseur indisponible n’entraîne jamais de refus automatique : le dossier passe en revue manuelle.

- [Démarrage rapide](https://nouko.openclouds.fr/docs/demarrage) : Votre première session de test en 5 minutes.
- [Référence des sessions](https://nouko.openclouds.fr/docs/sessions) : Créer, lire, lister, annuler, obtenir la décision.
- [Webhooks](https://nouko.openclouds.fr/docs/webhooks) : Événements, réessais, signature HMAC.

## Fonctionnement

**Flux complet**

```
Votre serveur ──POST /v1/sessions──────────▶ nouko   → id + verification_url
Votre client  ──ouvre verification_url────▶ nouko   → consentement, pièces, selfie
nouko         ──contrôles + revue humaine
nouko         ──webhook signé session.*───▶ votre serveur
Votre serveur ──GET /v1/sessions/{id}/decision──▶ décision motivée
```

## L’essentiel

| Élément | Valeur |
| --- | --- |
| URL de base | `https://nouko.openclouds.fr/api/public` |
| Format | JSON (UTF-8), dates ISO 8601 en UTC |
| Authentification | `Authorization: Bearer nk_test_…` ou `nk_live_…` |
| Limite | 120 requêtes par minute et par clé |
| Pays | `BJ` (Bénin), `CI` (Côte d’Ivoire) |
| Contrat machine | [OpenAPI 3.1](https://nouko.openclouds.fr/nouko-openapi.json) |
| Pour les assistants IA | [llms.txt](https://nouko.openclouds.fr/llms.txt) · [llms-full.txt](https://nouko.openclouds.fr/llms-full.txt) · chaque page en `.md` |


---

# Démarrage rapide

> Créez votre première session de vérification nouko en sandbox, ouvrez le parcours client et recevez le webhook en 5 minutes.

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

## 1. Obtenir une clé de test

Créez un compte, puis ouvrez **Console → Développeurs → Clés API** et créez une clé **sandbox**. Elle commence par `nk_test_` et n’est affichée qu’une fois : enregistrez-la dans une variable d’environnement.

```
export NOUKO_API_KEY="nk_test_…"
```

## 2. Créer une session

**curl**

```bash
curl -X POST https://nouko.openclouds.fr/api/public/v1/sessions \
  -H "Authorization: Bearer $NOUKO_API_KEY" \
  -H "Idempotency-Key: commande-001" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "client-42",
    "country": "BJ",
    "tier": 2,
    "redirect_url": "https://votre-site.com/kyc/retour",
    "metadata": { "user_id": "42" }
  }'
```

**Node.js**

```javascript
// Node.js 18+ (fetch intégré)
const res = await fetch('https://nouko.openclouds.fr/api/public/v1/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOUKO_API_KEY}`,
    'Idempotency-Key': 'commande-001',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    reference: 'client-42',
    country: 'BJ',
    tier: 2,
    redirect_url: 'https://votre-site.com/kyc/retour',
    metadata: { user_id: '42' },
  }),
});
const session = await res.json();
if (!res.ok) throw new Error(`${session.error.code}: ${session.error.message}`);
// Redirigez votre client vers session.verification_url
console.log(session.id, session.verification_url);
```

**Python**

```python
# pip install requests
import os, requests

res = requests.post(
    "https://nouko.openclouds.fr/api/public/v1/sessions",
    headers={
        "Authorization": f"Bearer {os.environ['NOUKO_API_KEY']}",
        "Idempotency-Key": "commande-001",
    },
    json={
        "reference": "client-42",
        "country": "BJ",
        "tier": 2,
        "redirect_url": "https://votre-site.com/kyc/retour",
        "metadata": {"user_id": "42"},
    },
    timeout=15,
)
session = res.json()
if not res.ok:
    raise RuntimeError(f"{session['error']['code']}: {session['error']['message']}")
print(session["id"], session["verification_url"])
```

**PHP**

```php
<?php
$ch = curl_init('https://nouko.openclouds.fr/api/public/v1/sessions');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('NOUKO_API_KEY'),
        'Idempotency-Key: commande-001',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'reference' => 'client-42',
        'country' => 'BJ',
        'tier' => 2,
        'redirect_url' => 'https://votre-site.com/kyc/retour',
        'metadata' => ['user_id' => '42'],
    ]),
]);
$session = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) throw new RuntimeException($session['error']['code']);
echo $session['id'], ' ', $session['verification_url'];
```

**Go**

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	body, _ := json.Marshal(map[string]any{
		"reference":    "client-42",
		"country":      "BJ",
		"tier":         2,
		"redirect_url": "https://votre-site.com/kyc/retour",
		"metadata":     map[string]string{"user_id": "42"},
	})
	req, _ := http.NewRequest("POST", "https://nouko.openclouds.fr/api/public/v1/sessions", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("NOUKO_API_KEY"))
	req.Header.Set("Idempotency-Key", "commande-001")
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	var s struct {
		ID              string `json:"id"`
		VerificationURL string `json:"verification_url"`
	}
	json.NewDecoder(res.Body).Decode(&s)
	fmt.Println(res.StatusCode, s.ID, s.VerificationURL)
}
```

**Java**

```java
// Java 15+
import java.net.URI;
import java.net.http.*;

public class CreateSession {
  public static void main(String[] args) throws Exception {
    String body = """
      {"reference":"client-42","country":"BJ","tier":2,
       "redirect_url":"https://votre-site.com/kyc/retour",
       "metadata":{"user_id":"42"}}""";
    HttpRequest req = HttpRequest.newBuilder(URI.create("https://nouko.openclouds.fr/api/public/v1/sessions"))
        .header("Authorization", "Bearer " + System.getenv("NOUKO_API_KEY"))
        .header("Idempotency-Key", "commande-001")
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();
    HttpResponse<String> res = HttpClient.newHttpClient()
        .send(req, HttpResponse.BodyHandlers.ofString());
    System.out.println(res.statusCode() + " " + res.body());
  }
}
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://nouko.openclouds.fr/api/public/v1/sessions")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("NOUKO_API_KEY")}"
req["Idempotency-Key"] = "commande-001"
req["Content-Type"] = "application/json"
req.body = {
  reference: "client-42", country: "BJ", tier: 2,
  redirect_url: "https://votre-site.com/kyc/retour",
  metadata: { user_id: "42" }
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
session = JSON.parse(res.body)
puts session["id"], session["verification_url"]
```

Réponse :

```
HTTP/1.1 201 Created
X-Request-Id: 3b1e…

{
  "id": "6f1c2a9e-…",
  "reference": "client-42",
  "status": "draft",
  "country": "BJ",
  "tier": 2,
  "environment": "sandbox",
  "redirect_url": "https://votre-site.com/kyc/retour",
  "metadata": { "user_id": "42" },
  "expires_at": "2026-10-24T18:00:00.000Z",
  "verification_url": "https://nouko.openclouds.fr/verify/…",
  "replayed": false,
  "sandbox_scenario": null
}
```

## 3. Ouvrir le parcours client

Redirigez votre client vers `verification_url`. En sandbox, le parcours affiche « Parcours de test » : vous pouvez remplir avec des données fictives et choisir le résultat. À la fin, le client revient sur votre `redirect_url` avec `?nouko_session=…&status=…`.

> **Attention :** Le retour sur `redirect_url` sert uniquement à l’affichage. N’accordez jamais d’accès sur cette base : la décision fait foi seulement via le webhook signé ou `GET /v1/sessions/{id}/decision`.

## 4. Recevoir la décision

Dans **Console → Développeurs → Webhooks**, ajoutez l’adresse de votre serveur et conservez le secret affiché. Puis implémentez le récepteur : voir [Vérifier la signature](https://nouko.openclouds.fr/docs/signature).

## Raccourci : tester sans interface

Utilisez la référence `test_approve` (ou `test_decline`, `test_review`, `test_expire`) : la session avance immédiatement et les vrais webhooks partent. Voir [Sandbox](https://nouko.openclouds.fr/docs/sandbox).


---

# Authentification

> Clés API nouko nk_test_ et nk_live_, en-tête Bearer, séparation sandbox / production et bonnes pratiques de sécurité.

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

Chaque requête porte une clé API dans l’en-tête `Authorization`.

```
Authorization: Bearer nk_test_0123…(64 caractères hexadécimaux)
```

| Préfixe | Environnement | Usage |
| --- | --- | --- |
| `nk_test_` | Sandbox | Développement, données fictives, scénarios de test |
| `nk_live_` | Production | Vrais clients, après activation de la production par nouko |

Les données des deux environnements sont strictement séparées : une clé de test ne voit jamais une session de production, et inversement.

## Bonnes pratiques

- Gardez la clé **côté serveur** uniquement (jamais dans une application mobile ou du JavaScript navigateur).
- Stockez-la dans une variable d’environnement ou un coffre de secrets, jamais dans le code.
- Créez une clé par service ; révoquez-la depuis la console en cas de fuite.
- Chaque réponse porte un en-tête `X-Request-Id` : communiquez-le au support en cas de problème.

## En cas d’échec

```
HTTP/1.1 401 Unauthorized

{ "error": { "code": "unauthorized", "message": "Clé API manquante ou invalide" } }
```


---

# Sessions

> Référence de l’API sessions nouko : créer une session, lire son statut, lister avec pagination, annuler et obtenir la décision motivée.

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

Une **session** représente une vérification d’identité pour un client. Elle possède un lien de collecte à usage unique et un cycle de vie.

## Créer une session

```
POST /v1/sessions
```

En-tête `Idempotency-Key` **obligatoire** (120 caractères max.). Renvoyer la même clé renvoie la même session (`200`, `replayed: true`) avec un nouveau lien si l’ancien n’a pas été utilisé.

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `reference` | string | Oui | Votre identifiant, 1 à 120 caractères |
| `country` | string | Oui | `BJ` ou `CI` |
| `tier` | integer | Non | Palier visé, 1 à 3 (défaut 2) |
| `redirect_url` | string | Non | URL HTTPS de retour, 500 caractères max. |
| `metadata` | object | Non | 20 clés max., renvoyées dans chaque événement. Aucune donnée personnelle. |
| `expires_in` | integer | Non | Durée de validité en secondes, 900 à 2 592 000 (défaut 604 800, soit 7 jours) |

**curl**

```bash
curl -X POST https://nouko.openclouds.fr/api/public/v1/sessions \
  -H "Authorization: Bearer $NOUKO_API_KEY" \
  -H "Idempotency-Key: commande-001" \
  -H "Content-Type: application/json" \
  -d '{
    "reference": "client-42",
    "country": "BJ",
    "tier": 2,
    "redirect_url": "https://votre-site.com/kyc/retour",
    "metadata": { "user_id": "42" }
  }'
```

**Node.js**

```javascript
// Node.js 18+ (fetch intégré)
const res = await fetch('https://nouko.openclouds.fr/api/public/v1/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOUKO_API_KEY}`,
    'Idempotency-Key': 'commande-001',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    reference: 'client-42',
    country: 'BJ',
    tier: 2,
    redirect_url: 'https://votre-site.com/kyc/retour',
    metadata: { user_id: '42' },
  }),
});
const session = await res.json();
if (!res.ok) throw new Error(`${session.error.code}: ${session.error.message}`);
// Redirigez votre client vers session.verification_url
console.log(session.id, session.verification_url);
```

**Python**

```python
# pip install requests
import os, requests

res = requests.post(
    "https://nouko.openclouds.fr/api/public/v1/sessions",
    headers={
        "Authorization": f"Bearer {os.environ['NOUKO_API_KEY']}",
        "Idempotency-Key": "commande-001",
    },
    json={
        "reference": "client-42",
        "country": "BJ",
        "tier": 2,
        "redirect_url": "https://votre-site.com/kyc/retour",
        "metadata": {"user_id": "42"},
    },
    timeout=15,
)
session = res.json()
if not res.ok:
    raise RuntimeError(f"{session['error']['code']}: {session['error']['message']}")
print(session["id"], session["verification_url"])
```

**PHP**

```php
<?php
$ch = curl_init('https://nouko.openclouds.fr/api/public/v1/sessions');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . getenv('NOUKO_API_KEY'),
        'Idempotency-Key: commande-001',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'reference' => 'client-42',
        'country' => 'BJ',
        'tier' => 2,
        'redirect_url' => 'https://votre-site.com/kyc/retour',
        'metadata' => ['user_id' => '42'],
    ]),
]);
$session = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status >= 400) throw new RuntimeException($session['error']['code']);
echo $session['id'], ' ', $session['verification_url'];
```

**Go**

```go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func main() {
	body, _ := json.Marshal(map[string]any{
		"reference":    "client-42",
		"country":      "BJ",
		"tier":         2,
		"redirect_url": "https://votre-site.com/kyc/retour",
		"metadata":     map[string]string{"user_id": "42"},
	})
	req, _ := http.NewRequest("POST", "https://nouko.openclouds.fr/api/public/v1/sessions", bytes.NewReader(body))
	req.Header.Set("Authorization", "Bearer "+os.Getenv("NOUKO_API_KEY"))
	req.Header.Set("Idempotency-Key", "commande-001")
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		panic(err)
	}
	defer res.Body.Close()
	var s struct {
		ID              string `json:"id"`
		VerificationURL string `json:"verification_url"`
	}
	json.NewDecoder(res.Body).Decode(&s)
	fmt.Println(res.StatusCode, s.ID, s.VerificationURL)
}
```

**Java**

```java
// Java 15+
import java.net.URI;
import java.net.http.*;

public class CreateSession {
  public static void main(String[] args) throws Exception {
    String body = """
      {"reference":"client-42","country":"BJ","tier":2,
       "redirect_url":"https://votre-site.com/kyc/retour",
       "metadata":{"user_id":"42"}}""";
    HttpRequest req = HttpRequest.newBuilder(URI.create("https://nouko.openclouds.fr/api/public/v1/sessions"))
        .header("Authorization", "Bearer " + System.getenv("NOUKO_API_KEY"))
        .header("Idempotency-Key", "commande-001")
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(body))
        .build();
    HttpResponse<String> res = HttpClient.newHttpClient()
        .send(req, HttpResponse.BodyHandlers.ofString());
    System.out.println(res.statusCode() + " " + res.body());
  }
}
```

**Ruby**

```ruby
require "net/http"
require "json"

uri = URI("https://nouko.openclouds.fr/api/public/v1/sessions")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("NOUKO_API_KEY")}"
req["Idempotency-Key"] = "commande-001"
req["Content-Type"] = "application/json"
req.body = {
  reference: "client-42", country: "BJ", tier: 2,
  redirect_url: "https://votre-site.com/kyc/retour",
  metadata: { user_id: "42" }
}.to_json

res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
session = JSON.parse(res.body)
puts session["id"], session["verification_url"]
```

**Réponse 201**

```
HTTP/1.1 201 Created
X-Request-Id: 3b1e…

{
  "id": "6f1c2a9e-…",
  "reference": "client-42",
  "status": "draft",
  "country": "BJ",
  "tier": 2,
  "environment": "sandbox",
  "redirect_url": "https://votre-site.com/kyc/retour",
  "metadata": { "user_id": "42" },
  "expires_at": "2026-10-24T18:00:00.000Z",
  "verification_url": "https://nouko.openclouds.fr/verify/…",
  "replayed": false,
  "sandbox_scenario": null
}
```

## Lire une session

```
GET /v1/sessions/{id}
```

**curl**

```bash
curl https://nouko.openclouds.fr/api/public/v1/sessions/6f1c2a9e-… \
  -H "Authorization: Bearer $NOUKO_API_KEY"
```

**Node.js**

```javascript
const res = await fetch(`https://nouko.openclouds.fr/api/public/v1/sessions/${id}`, {
  headers: { Authorization: `Bearer ${process.env.NOUKO_API_KEY}` },
});
const session = await res.json(); // session.status
```

**Python**

```python
res = requests.get(f"https://nouko.openclouds.fr/api/public/v1/sessions/{session_id}",
    headers={"Authorization": f"Bearer {os.environ['NOUKO_API_KEY']}"}, timeout=15)
print(res.json()["status"])
```

**PHP**

```php
$ch = curl_init("https://nouko.openclouds.fr/api/public/v1/sessions/" . urlencode($id));
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NOUKO_API_KEY')]]);
$session = json_decode(curl_exec($ch), true);
```

**Go**

```go
req, _ := http.NewRequest("GET", "https://nouko.openclouds.fr/api/public/v1/sessions/"+id, nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("NOUKO_API_KEY"))
res, err := http.DefaultClient.Do(req)
```

**Java**

```java
HttpRequest req = HttpRequest.newBuilder(URI.create("https://nouko.openclouds.fr/api/public/v1/sessions/" + id))
    .header("Authorization", "Bearer " + System.getenv("NOUKO_API_KEY")).GET().build();
```

**Ruby**

```ruby
uri = URI("https://nouko.openclouds.fr/api/public/v1/sessions/#{id}")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{ENV.fetch("NOUKO_API_KEY")}"
```

## Lister les sessions

```
GET /v1/sessions?status=&reference=&limit=&cursor=
```

| Paramètre | Description |
| --- | --- |
| `status` | Filtre sur un statut |
| `reference` | Filtre sur votre référence |
| `limit` | 1 à 100 (défaut 20) |
| `cursor` | Valeur `next_cursor` de la page précédente |

La réponse contient `data` (les sessions, les plus récentes d’abord), `has_more` et `next_cursor`.

**curl**

```bash
# Nécessite jq
cursor=""
while :; do
  page=$(curl -s "https://nouko.openclouds.fr/api/public/v1/sessions?limit=100&status=pending${cursor:+&cursor=$cursor}" \
    -H "Authorization: Bearer $NOUKO_API_KEY")
  echo "$page" | jq -r '.data[] | "\(.id) \(.reference)"'
  [ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break
  cursor=$(echo "$page" | jq -r '.next_cursor')
done
```

**Node.js**

```javascript
async function* allSessions(params = {}) {
  let cursor;
  do {
    const qs = new URLSearchParams({ limit: '100', ...params, ...(cursor ? { cursor } : {}) });
    const res = await fetch(`https://nouko.openclouds.fr/api/public/v1/sessions?${qs}`, {
      headers: { Authorization: `Bearer ${process.env.NOUKO_API_KEY}` },
    });
    const page = await res.json();
    yield* page.data;
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);
}

for await (const s of allSessions({ status: 'pending' })) console.log(s.id, s.reference);
```

**Python**

```python
def all_sessions(**params):
    cursor = None
    while True:
        q = {"limit": 100, **params, **({"cursor": cursor} if cursor else {})}
        page = requests.get("https://nouko.openclouds.fr/api/public/v1/sessions", params=q,
            headers={"Authorization": f"Bearer {os.environ['NOUKO_API_KEY']}"}, timeout=15).json()
        yield from page["data"]
        if not page["has_more"]:
            break
        cursor = page["next_cursor"]

for s in all_sessions(status="pending"):
    print(s["id"], s["reference"])
```

**PHP**

```php
$cursor = null;
do {
    $q = http_build_query(array_filter(['limit' => 100, 'status' => 'pending', 'cursor' => $cursor]));
    $ch = curl_init("https://nouko.openclouds.fr/api/public/v1/sessions?$q");
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('NOUKO_API_KEY')]]);
    $page = json_decode(curl_exec($ch), true);
    foreach ($page['data'] as $s) echo $s['id'], PHP_EOL;
    $cursor = $page['has_more'] ? $page['next_cursor'] : null;
} while ($cursor);
```

## Obtenir la décision

```
GET /v1/sessions/{id}/decision
```

Renvoie la décision, son motif et les contrôles effectués, sans aucune image. `final` vaut `true` quand la décision est définitive ; `decision` vaut `approved`, `declined` ou `null`.

**curl**

```bash
curl https://nouko.openclouds.fr/api/public/v1/sessions/6f1c2a9e-…/decision \
  -H "Authorization: Bearer $NOUKO_API_KEY"
```

**Node.js**

```javascript
const res = await fetch(`https://nouko.openclouds.fr/api/public/v1/sessions/${id}/decision`, {
  headers: { Authorization: `Bearer ${process.env.NOUKO_API_KEY}` },
});
const { final, decision, reason } = await res.json();
if (final) console.log(decision, reason); // "approved" | "declined"
```

**Python**

```python
d = requests.get(f"https://nouko.openclouds.fr/api/public/v1/sessions/{session_id}/decision",
    headers={"Authorization": f"Bearer {os.environ['NOUKO_API_KEY']}"}, timeout=15).json()
if d["final"]:
    print(d["decision"], d["reason"])
```

**Réponse 200**

```
{
  "id": "6f1c2a9e-…",
  "reference": "client-42",
  "status": "approved",
  "final": true,
  "decision": "approved",
  "tier": 2,
  "achieved_tier": 2,
  "reason": "Pièce lisible, visage concordant",
  "decided_at": "2026-10-17T18:12:00.000Z",
  "checks": [
    { "check_type": "document", "provider": "agent", "outcome": "pass", "risk": "low", "created_at": "…" }
  ]
}
```

## Annuler une session

```
POST /v1/sessions/{id}/cancel
```

Possible uniquement pour une session `draft` ou `complement` ; le lien est désactivé. Une session déjà annulée renvoie `200`. Sinon : `409 not_cancellable`.

**curl**

```bash
curl -X POST https://nouko.openclouds.fr/api/public/v1/sessions/6f1c2a9e-…/cancel \
  -H "Authorization: Bearer $NOUKO_API_KEY"
```

**Node.js**

```javascript
await fetch(`https://nouko.openclouds.fr/api/public/v1/sessions/${id}/cancel`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.NOUKO_API_KEY}` },
});
```

**Python**

```python
requests.post(f"https://nouko.openclouds.fr/api/public/v1/sessions/{session_id}/cancel",
    headers={"Authorization": f"Bearer {os.environ['NOUKO_API_KEY']}"}, timeout=15)
```

## Statuts

| Statut | Signification | Définitif |
| --- | --- | --- |
| `draft` | Créée, en attente du client | Non |
| `pending` | Pièces soumises, examen en cours (automatique puis humain si besoin) | Non |
| `complement` | Pièces à refaire : le client peut reprendre son lien | Non |
| `approved` | Identité vérifiée | Oui |
| `rejected` | Vérification refusée (événement `session.declined`) | Oui |
| `expired` | Lien expiré avant soumission | Oui |
| `cancelled` | Annulée par vous | Oui |


---

# 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.


---

# Vérifier la signature des webhooks

> Vérifiez l’en-tête Nouko-Signature (HMAC-SHA256) en Node.js, Python, PHP, Go, Java et Ruby, avec les pièges à éviter.

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

Chaque webhook porte l’en-tête `Nouko-Signature: t=<horodatage>,v1=<signature>`. Vérifiez-le avant de faire confiance au contenu.

```
v1 = hex( HMAC-SHA256( secret_du_webhook, t + "." + corps_brut ) )
```

1. Séparez `t` et `v1` dans l’en-tête.
2. Refusez si `t` est éloigné de plus de 300 secondes (5 minutes) de l’heure actuelle.
3. Calculez le HMAC sur `t + "." + corps_brut` avec votre secret.
4. Comparez en temps constant avec `v1`.

**Node.js**

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

// rawBody : le corps EXACT reçu (Buffer ou string), avant tout JSON.parse
export function verifyNouko(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map(p => p.trim().split('=')));
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret)
    .update(parts.t + '.').update(rawBody).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}
```

**Python**

```python
import hmac, hashlib, time

def verify_nouko(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    if "t" not in parts or "v1" not in parts:
        return False
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body,
                        hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

**PHP**

```php
function verify_nouko(string $raw, string $header, string $secret): bool {
    $p = [];
    foreach (explode(',', $header) as $part) {
        [$k, $v] = array_pad(explode('=', trim($part), 2), 2, null);
        $p[$k] = $v;
    }
    if (empty($p['t']) || empty($p['v1'])) return false;
    if (abs(time() - (int) $p['t']) > 300) return false;
    $expected = hash_hmac('sha256', $p['t'] . '.' . $raw, $secret);
    return hash_equals($expected, $p['v1']);
}
```

**Go**

```go
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"strconv"
	"strings"
	"time"
)

func VerifyNouko(raw []byte, header, secret string) bool {
	p := map[string]string{}
	for _, part := range strings.Split(header, ",") {
		if kv := strings.SplitN(strings.TrimSpace(part), "=", 2); len(kv) == 2 {
			p[kv[0]] = kv[1]
		}
	}
	ts, err := strconv.ParseInt(p["t"], 10, 64)
	if err != nil {
		return false
	}
	if d := time.Now().Unix() - ts; d > 300 || d < -300 {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(p["t"] + "."))
	mac.Write(raw)
	return hmac.Equal([]byte(hex.EncodeToString(mac.Sum(nil))), []byte(p["v1"]))
}
```

**Java**

```java
// Java 17+
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.*;

public final class NoukoSignature {
  public static boolean verify(byte[] rawBody, String header, String secret) throws Exception {
    Map<String, String> p = new HashMap<>();
    for (String part : header.split(",")) {
      String[] kv = part.trim().split("=", 2);
      if (kv.length == 2) p.put(kv[0], kv[1]);
    }
    String t = p.get("t"), v1 = p.get("v1");
    if (t == null || v1 == null) return false;
    if (Math.abs(System.currentTimeMillis() / 1000 - Long.parseLong(t)) > 300) return false;
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    mac.update((t + ".").getBytes(StandardCharsets.UTF_8));
    String expected = HexFormat.of().formatHex(mac.doFinal(rawBody));
    return MessageDigest.isEqual(expected.getBytes(StandardCharsets.UTF_8),
                                 v1.getBytes(StandardCharsets.UTF_8));
  }
}
```

**Ruby**

```ruby
require "openssl"

def verify_nouko(raw_body, header, secret)
  parts = header.split(",").map { |p| p.strip.split("=", 2) }.select { |kv| kv.size == 2 }.to_h
  return false unless parts["t"] && parts["v1"]
  return false if (Time.now.to_i - parts["t"].to_i).abs > 300
  expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{parts["t"]}.#{raw_body}")
  OpenSSL.secure_compare(expected, parts["v1"])
end
```

## Pièges fréquents

- **Corps re-sérialisé** : signez le corps exact reçu, pas `JSON.stringify(JSON.parse(body))`. Désactivez le parseur JSON automatique sur cette route.
- **Horloge décalée** : synchronisez votre serveur (NTP), sinon les messages valides sont refusés.
- **Mauvais secret** : chaque adresse webhook a son propre secret ; sandbox et production sont différents.
- **Comparaison simple** : `==` laisse fuiter des informations ; utilisez la comparaison en temps constant.


---

# Sandbox et scénarios de test

> Testez votre intégration nouko sans données réelles : scénarios test_approve, test_decline, test_review, test_expire et parcours client de test.

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

Avec une clé `nk_test_`, tout est réel (sessions, liens, webhooks) mais isolé de la production. Deux façons de tester :

## Scénarios instantanés

Utilisez une de ces références (ou une variante comme `test_approve_42`) : la session avance immédiatement et émet les vrais webhooks. `verification_url` vaut alors `null` et `sandbox_scenario` indique le résultat.

| Référence | Statut final | Événements |
| --- | --- | --- |
| `test_approve` | `approved` | created → started → submitted → approved |
| `test_decline` | `rejected` | created → started → submitted → declined |
| `test_review` | `pending` | created → started → submitted (reste en revue) |
| `test_expire` | `expired` | created → expired |

**curl**

```bash
curl -X POST https://nouko.openclouds.fr/api/public/v1/sessions \
  -H "Authorization: Bearer $NOUKO_TEST_KEY" \
  -H "Idempotency-Key: essai-approve-1" \
  -H "Content-Type: application/json" \
  -d '{"reference":"test_approve_1","country":"CI"}'
```

**Node.js**

```javascript
const res = await fetch('https://nouko.openclouds.fr/api/public/v1/sessions', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NOUKO_TEST_KEY}`, // nk_test_…
    'Idempotency-Key': 'essai-approve-1',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ reference: 'test_approve_1', country: 'CI' }),
});
console.log(await res.json()); // sandbox_scenario: "approved"
```

**Python**

```python
r = requests.post("https://nouko.openclouds.fr/api/public/v1/sessions",
    headers={"Authorization": f"Bearer {os.environ['NOUKO_TEST_KEY']}",
             "Idempotency-Key": "essai-approve-1"},
    json={"reference": "test_approve_1", "country": "CI"}, timeout=15)
print(r.json()["sandbox_scenario"])  # "approved"
```

## Parcours client de test

Avec toute autre référence, ouvrez `verification_url` : le parcours affiche « Parcours de test — aucune donnée réelle », propose de remplir avec des données fictives et vous laisse choisir le résultat à la dernière étape. Idéal pour tester votre `redirect_url`.

## Passer en production

Demandez l’activation depuis **Console → Paramètres**. Une fois validée par nouko, créez une clé `nk_live_` et une adresse webhook pour la production. Votre code ne change pas, seule la clé change.


---

# Erreurs, idempotence et limites

> Codes d’erreur de l’API nouko, idempotence avec Idempotency-Key et limite de 120 requêtes par minute.

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

Toutes les erreurs ont le même format :

```
HTTP/1.1 400 Bad Request
X-Request-Id: 9a7c…

{ "error": { "code": "invalid_request", "message": "country: Invalid enum value" } }
```

| HTTP | Code | Cause | Que faire |
| --- | --- | --- | --- |
| 400 | `invalid_request` | Champ manquant ou invalide (détail dans `message`) | Corrigez la requête, ne réessayez pas telle quelle |
| 400 | `idempotency_key_required` | En-tête `Idempotency-Key` absent ou trop long | Ajoutez une clé stable de 120 caractères max. |
| 401 | `unauthorized` | Clé absente, invalide ou révoquée | Vérifiez la clé et son environnement |
| 403 | `account_suspended` | Compte suspendu par nouko | Contactez le support |
| 404 | `not_found` | Session inexistante ou d’un autre environnement | Vérifiez l’id et la clé utilisée |
| 409 | `not_cancellable` | Session déjà soumise ou terminée | Lisez son statut |
| 429 | `rate_limited` | Plus de 120 requêtes par minute | Attendez puis réessayez avec un délai croissant |
| 500 | `internal_error` | Erreur de nouko | Réessayez avec la **même** `Idempotency-Key` |

## Idempotence

Utilisez une clé liée à votre objet métier (ex. `commande-001`) et stockez-la. En cas de coupure réseau ou d’erreur `5xx`, renvoyez exactement la même requête avec la même clé : vous obtenez la session existante, jamais un doublon.

**Node.js**

```javascript
// Même Idempotency-Key à chaque tentative : jamais de doublon
async function createWithRetry(payload, key, attempts = 4) {
  for (let i = 0; i < attempts; i++) {
    try {
      const res = await fetch('https://nouko.openclouds.fr/api/public/v1/sessions', {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.NOUKO_API_KEY}`,
          'Idempotency-Key': key,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(payload),
      });
      if (res.status !== 429 && res.status < 500) return res.json(); // 200 si rejeu
    } catch { /* réseau : on réessaie */ }
    await new Promise(r => setTimeout(r, 2 ** i * 1000));
  }
  throw new Error('Nouko indisponible');
}
```

**Python**

```python
import time, uuid

def create_with_retry(payload, key=None, attempts=4):
    key = key or str(uuid.uuid4())  # stockez-la avec votre commande
    for i in range(attempts):
        try:
            r = requests.post("https://nouko.openclouds.fr/api/public/v1/sessions", json=payload, timeout=15,
                headers={"Authorization": f"Bearer {os.environ['NOUKO_API_KEY']}",
                         "Idempotency-Key": key})
            if r.status_code != 429 and r.status_code < 500:
                return r.json()
        except requests.RequestException:
            pass
        time.sleep(2 ** i)
    raise RuntimeError("Nouko indisponible")
```

## Limites

| Limite | Valeur |
| --- | --- |
| Requêtes | 120 par minute et par clé |
| Page de liste | 100 sessions max. |
| Métadonnées | 20 clés max. |
| Validité d’un lien | 15 minutes à 30 jours |
| Réponse webhook attendue | moins de 8 secondes |


---

# Intégrer nouko avec un assistant IA

> Ressources lisibles par les modèles (llms.txt, Markdown, OpenAPI) et prompt prêt à coller pour intégrer nouko avec un assistant de code.

Source : https://nouko.openclouds.fr/docs/guide-ia

Cette documentation est conçue pour être lue aussi bien par vous que par un assistant de code (Cursor, Claude, ChatGPT, Copilot…).

| Ressource | Contenu |
| --- | --- |
| [/llms.txt](https://nouko.openclouds.fr/llms.txt) | Index court de la documentation |
| [/llms-full.txt](https://nouko.openclouds.fr/llms-full.txt) | Toute la documentation en un seul fichier Markdown |
| `/docs/<page>.md` | Chaque page en Markdown, par exemple [/docs/sessions.md](https://nouko.openclouds.fr/docs/sessions.md) |
| [/nouko-openapi.json](https://nouko.openclouds.fr/nouko-openapi.json) | Contrat OpenAPI 3.1 (points d’entrée, schémas, webhooks) |

Sur chaque page, le bouton **Copier pour une IA** copie la page en Markdown.

## Prompt prêt à coller

```
Tu vas intégrer l'API de vérification d'identité nouko dans mon application.
Lis d'abord la documentation complète : https://nouko.openclouds.fr/llms-full.txt
et le contrat OpenAPI : https://nouko.openclouds.fr/nouko-openapi.json

Règles à respecter :
- La clé API (nk_test_… / nk_live_…) reste côté serveur, dans une variable d'environnement NOUKO_API_KEY.
- Chaque POST /v1/sessions envoie un en-tête Idempotency-Key stable (ex. l'identifiant de commande).
- Redirige l'utilisateur vers verification_url ; au retour sur redirect_url, n'accorde rien :
  la décision vient uniquement du webhook signé ou de GET /v1/sessions/{id}/decision.
- Le webhook lit le corps brut, vérifie Nouko-Signature (HMAC-SHA256, tolérance 300 s),
  répond 2xx en moins de 8 s, puis traite en arrière-plan en dédoublonnant sur event.id.
- Commence en sandbox avec les références test_approve / test_decline / test_review / test_expire.
- N'invente aucun point d'entrée ou champ absent de la documentation.

Mon stack : <décris ton langage, ton framework et ta base de données>
```

> **Attention :** Relisez toujours le code généré, en particulier la vérification de signature et l’endroit où la clé API est stockée.
