# Exemples de code

> Programmes complets en Node.js et en Python pour l’API Fileverity : vérifier une image, vérifier un PDF volumineux, détecter un texte rédigé par IA et vérifier les signatures de webhook.

Source : https://fileverity.com/fr/docs/sample-code

Cette page présente quatre programmes complets pour l’API Fileverity, chacun en Node.js 18 ou version ultérieure et en Python 3.9 ou version ultérieure : vérifier une image et attendre le résultat, vérifier un PDF volumineux, détecter un texte rédigé par IA, et recevoir des webhooks avec vérification de la signature. Copiez-en un, définissez `FILEVERITY_API_KEY` et exécutez-le.

Les programmes appellent directement l’API HTTPS, avec `fetch`, intégré à Node.js, et la bibliothèque `requests` en Python : il vous suffit donc d’un client HTTP. Les appels, les champs et les statuts sont ceux décrits dans [Fichiers](https://fileverity.com/fr/docs/files.md), [Analyses](https://fileverity.com/fr/docs/analyses.md), [Détections de texte](https://fileverity.com/fr/docs/text-detections.md) et [Webhooks](https://fileverity.com/fr/docs/webhooks.md).

| Programme | Ce qu’il fait | Appels |
| --- | --- | --- |
| [Vérifier une image](https://fileverity.com/fr/docs/sample-code.md#image) | Téléverse une image, attend le résultat et affiche le résumé et l’URL du rapport. | `POST /files`, `POST /analyses`, `GET /analyses/{id}` |
| [Vérifier un PDF volumineux](https://fileverity.com/fr/docs/sample-code.md#pdf) | Téléverse un document avec une URL présignée, interroge l’API jusqu’à la fin de la vérification et enregistre le rapport PDF. | `POST /files`, `GET /files/{id}`, `POST /analyses`, `GET /analyses/{id}`, `GET /analyses/{id}/report.pdf` |
| [Détecter un texte rédigé par IA](https://fileverity.com/fr/docs/sample-code.md#text) | Envoie un fichier texte et affiche le verdict, le score et chaque passage rédigé par IA. | `POST /text/detections` |
| [Recevoir des webhooks](https://fileverity.com/fr/docs/sample-code.md#webhooks) | Vérifie la signature de chaque envoi, ignore les doublons et traite les événements. | Votre point de terminaison, appelé avec `analysis.completed`, `analysis.failed` et `balance.low` |

## Avant de lancer les programmes

- Créez une clé **Test** sous [Clés API](https://fileverity.com/console/keys?lang=fr) dans la Console. Une clé de test renvoie des exemples de résultats fixes et n’est jamais facturée : chaque programme s’exécute donc sans frais. Pour vérifier de vrais fichiers, créez une clé **Live**. Les programmes ne changent pas.
- Node.js : enregistrez chaque programme dans un fichier `.mjs`, ce qui lui permet d’utiliser `import` et `await` au niveau supérieur, puis exécutez-le avec `node`. Rien à installer.
- Python : exécutez `pip install requests`. Le récepteur de webhooks nécessite aussi `pip install flask`.

**Définir la clé: Shell**

```bash
export FILEVERITY_API_KEY=fv_test_...
```

**Définir la clé: PowerShell**

```powershell
$env:FILEVERITY_API_KEY = "fv_test_..."
```

## Comment les programmes gèrent les clés, les erreurs et les nouvelles tentatives

Les trois programmes qui appellent l’API partagent les mêmes fonctions `request` et `api`. Copiez-les telles quelles dans votre propre code.

| Sujet | Ce que font les programmes |
| --- | --- |
| Clé | Lue depuis `FILEVERITY_API_KEY` et jamais écrite dans le code. Appelez l’API depuis votre serveur, et non depuis du code exécuté dans un navigateur. |
| Idempotence | Chaque POST envoie une `Idempotency-Key`, un nouvel UUID par requête, et la réutilise à chaque nouvelle tentative : une vérification n’est ainsi jamais lancée ni facturée deux fois. Pour pouvoir relancer le script entier sans risque, dérivez plutôt la clé de l’ID de votre propre enregistrement. Voir [Idempotence](https://fileverity.com/fr/docs/idempotency.md). |
| Erreurs | Une réponse qui n’est pas `2xx` contient `error.type`, `error.code`, `error.message`, `error.param` et `error.request_id`. Les programmes s’arrêtent avec `code: message (request request_id)`. Basez votre logique sur `code` et indiquez l’ID de requête à l’assistance. Voir [Erreurs](https://fileverity.com/fr/docs/errors.md). |
| Nouvelles tentatives | Une erreur réseau, un statut `5xx` (`internal_error`, `analysis_failed`, `service_unavailable`), `rate_limited` (429) et `idempotency_in_progress` (409) donnent lieu à de nouvelles tentatives, dans la limite de 5 tentatives. Le délai d’attente est celui de l’en-tête `retry-after` lorsque la réponse en contient un, sinon 2, 4, 8 puis 16 secondes. Tout autre statut `4xx` arrête le programme immédiatement. `pending_files_limit` est lui aussi un 429, mais sans `retry-after` : il n’est donc pas réessayé. |
| Limite de débit | Lorsqu’une réponse indique `x-ratelimit-remaining-requests: 0`, le programme attend `x-ratelimit-reset-requests` secondes avant l’appel suivant, afin de ne pas recevoir un 429. Voir [Limites de débit et en-têtes](https://fileverity.com/fr/docs/rate-limits.md#headers). |
| Délais d’attente | Les programmes Python abandonnent une requête après 90 secondes, soit plus que les 60 secondes pendant lesquelles `Prefer: wait` peut maintenir une requête ouverte. |

## Vérifier une image et attendre le résultat

Le programme téléverse l’image en multipart, crée une analyse avec `Prefer: wait=60` pour que la requête reste ouverte jusqu’à ce que le résultat soit prêt, puis affiche le résumé et l’URL du rapport. Si l’attente se termine avant, l’API répond `202` avec `status: processing`, et le programme interroge `GET /analyses/{id}` toutes les 3 secondes. Voir [Attendre, interroger ou utiliser des webhooks](https://fileverity.com/fr/docs/analyses.md#wait).

Enregistrez-le sous le nom `check-image.mjs` ou `check_image.py`, puis exécutez `node check-image.mjs photo.jpg` ou `python check_image.py photo.jpg`. L’envoi multipart accepte les images de 4 Mo maximum. Pour une image plus volumineuse, jusqu’à 10 Mo, utilisez le téléversement par URL présignée du programme suivant, avec le type de contenu de l’image, par exemple `image/jpeg`.

**Vérifier une image: JavaScript**

```javascript
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
import { basename } from "node:path";

const base = "https://api.fileverity.com/v1";
const key = process.env.FILEVERITY_API_KEY;
const path = process.argv[2];
if (!key || !path) throw new Error("Usage: FILEVERITY_API_KEY=... node check-image.mjs photo.jpg");

const retryable = ["rate_limited", "idempotency_in_progress"];
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function request(url, init) {
  for (let attempt = 1; ; attempt++) {
    let response = null;
    let problem;
    try {
      response = await fetch(url, init);
      if (response.ok) {
        const remaining = response.headers.get("x-ratelimit-remaining-requests");
        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));
        if (remaining === "0") await sleep(reset);
        return response;
      }
      problem = (await response.json().catch(() => ({}))).error ?? {};
    } catch (error) {
      problem = { code: "network_error", message: error.message };
    }
    const status = response?.status ?? 0;
    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);
    if (!retry || attempt === 5) {
      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;
      throw new Error(`${code}: ${message} (request ${id})`);
    }
    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);
  }
}

async function api(method, route, { headers, body } = {}) {
  const auth = { Authorization: `Bearer ${key}` };
  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });
  return response.json();
}

const form = new FormData();
form.append("file", new Blob([await readFile(path)]), basename(path));
const file = await api("POST", "/files", {
  headers: { "Idempotency-Key": randomUUID() },
  body: form,
});

let analysis = await api("POST", "/analyses", {
  headers: {
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
    Prefer: "wait=60",
  },
  body: JSON.stringify({ model: "fvimg-2", file: file.id }),
});
while (analysis.status === "processing") {
  await sleep(3);
  analysis = await api("GET", `/analyses/${analysis.id}`);
}

if (analysis.status !== "succeeded") {
  throw new Error(`The check ${analysis.status}: ${analysis.error?.message}`);
}
const { level, action, assessment, ai, edit } = analysis.summary;
console.log(`${analysis.id} ${analysis.status}`);
console.log(`level ${level}, action ${action}, assessment ${assessment}`);
console.log(`AI generation: ${ai}, editing: ${edit}`);
console.log(`Report: ${analysis.report_url}`);
```

**Vérifier une image: Python**

```python
import os
import sys
import time
import uuid

import requests

BASE = "https://api.fileverity.com/v1"
KEY = os.environ.get("FILEVERITY_API_KEY")
if not KEY or len(sys.argv) < 2:
    sys.exit("Usage: FILEVERITY_API_KEY=... python check_image.py photo.jpg")

RETRYABLE = ("rate_limited", "idempotency_in_progress")


def request(method, url, **kwargs):
    for attempt in range(1, 6):
        try:
            response = requests.request(method, url, timeout=90, **kwargs)
        except requests.RequestException as error:
            response, problem = None, {"code": "network_error", "message": str(error)}
        else:
            if response.ok:
                if response.headers.get("x-ratelimit-remaining-requests") == "0":
                    time.sleep(int(response.headers["x-ratelimit-reset-requests"]))
                return response
            try:
                problem = response.json()["error"]
            except (ValueError, KeyError, TypeError):
                problem = {}
        status = response.status_code if response is not None else 0
        retry = status == 0 or status >= 500 or problem.get("code") in RETRYABLE
        if not retry or attempt == 5:
            code = problem.get("code", status)
            message = problem.get("message", "Request failed")
            request_id = problem.get("request_id", "unknown")
            raise RuntimeError(f"{code}: {message} (request {request_id})")
        wait = response.headers.get("retry-after") if response is not None else None
        time.sleep(int(wait or 2**attempt))


def api(method, route, headers=None, **kwargs):
    headers = {"Authorization": f"Bearer {KEY}", **(headers or {})}
    return request(method, BASE + route, headers=headers, **kwargs).json()


with open(sys.argv[1], "rb") as source:
    data = source.read()
upload = {"file": (os.path.basename(sys.argv[1]), data)}
file = api("POST", "/files", headers={"Idempotency-Key": str(uuid.uuid4())}, files=upload)

analysis = api(
    "POST",
    "/analyses",
    headers={"Idempotency-Key": str(uuid.uuid4()), "Prefer": "wait=60"},
    json={"model": "fvimg-2", "file": file["id"]},
)
while analysis["status"] == "processing":
    time.sleep(3)
    analysis = api("GET", f"/analyses/{analysis['id']}")

if analysis["status"] != "succeeded":
    problem = analysis["error"] or {}
    sys.exit(f"The check {analysis['status']}: {problem.get('message')}")
summary = analysis["summary"]
print(f"{analysis['id']} {analysis['status']}")
print(f"level {summary['level']}, action {summary['action']}, assessment {summary['assessment']}")
print(f"AI generation: {summary['ai']}, editing: {summary['edit']}")
print(f"Report: {analysis['report_url']}")
```

**Sortie de la vérification d’image**

```text
an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeeded
level L2, action accept, assessment authentic_untouched
AI generation: very_low, editing: none
Report: https://fileverity.com/verify/FV-7K2M-9QXD-4T8R
```

`summary` est le verdict stable et lisible par machine d’une vérification d’image : `level`, `action`, `assessment`, `ai` et `edit`, décrits dans [Résumé d’image](https://fileverity.com/fr/docs/analyses.md#summary-image). `report_url` est la page de vérification publique du rapport. Avec une clé de test, le résultat est un exemple fixe, et son URL de rapport ne mène à aucune page.

## Vérifier un PDF volumineux

Les documents de 50 Mo maximum sont téléversés en trois étapes : demandez une URL de téléversement avec `POST /files` et un corps JSON, envoyez les octets à cette URL avec `PUT`, puis attendez que le fichier indique `uploaded`. Le programme crée ensuite l’analyse et l’interroge toutes les 5 secondes, car les documents peuvent être mis en file d’attente et une vérification peut prendre du temps. Il affiche la progression, le résumé et l’URL du rapport, et enregistre le rapport PDF à côté du script.

Enregistrez-le sous le nom `check-pdf.mjs` ou `check_pdf.py`, puis exécutez `node check-pdf.mjs contract.pdf` ou `python check_pdf.py contract.pdf`. Pour un autre type de document, remplacez `content_type` par l’un des [types de contenu acceptés](https://fileverity.com/fr/docs/files.md#content-types). Pour un document chiffré, ajoutez `options: { password }` à la requête d’analyse.

**Vérifier un PDF volumineux: JavaScript**

```javascript
import { randomUUID } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { basename } from "node:path";

const base = "https://api.fileverity.com/v1";
const key = process.env.FILEVERITY_API_KEY;
const path = process.argv[2];
if (!key || !path) throw new Error("Usage: FILEVERITY_API_KEY=... node check-pdf.mjs contract.pdf");

const retryable = ["rate_limited", "idempotency_in_progress"];
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function request(url, init) {
  for (let attempt = 1; ; attempt++) {
    let response = null;
    let problem;
    try {
      response = await fetch(url, init);
      if (response.ok) {
        const remaining = response.headers.get("x-ratelimit-remaining-requests");
        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));
        if (remaining === "0") await sleep(reset);
        return response;
      }
      problem = (await response.json().catch(() => ({}))).error ?? {};
    } catch (error) {
      problem = { code: "network_error", message: error.message };
    }
    const status = response?.status ?? 0;
    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);
    if (!retry || attempt === 5) {
      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;
      throw new Error(`${code}: ${message} (request ${id})`);
    }
    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);
  }
}

async function api(method, route, { headers, body } = {}) {
  const auth = { Authorization: `Bearer ${key}` };
  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });
  return response.json();
}

const bytes = await readFile(path);
const file = await api("POST", "/files", {
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({
    filename: basename(path),
    content_type: "application/pdf",
    bytes: bytes.length,
  }),
});

if (file.status === "pending") {
  const { url, method, headers } = file.upload;
  await request(url, { method, headers, body: bytes });
  let ready = false;
  for (let attempt = 0; attempt < 15 && !ready; attempt++) {
    ready = (await api("GET", `/files/${file.id}`)).status === "uploaded";
    if (!ready) await sleep(2);
  }
  if (!ready) throw new Error("The upload was not confirmed.");
}
console.log(`${file.id} uploaded`);

let analysis = await api("POST", "/analyses", {
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({ model: "fvdoc-3", file: file.id }),
});
const deadline = Date.now() + 15 * 60 * 1000;
while (analysis.status === "processing") {
  if (Date.now() > deadline) {
    throw new Error(`Still processing. Fetch it later with GET /analyses/${analysis.id}`);
  }
  console.log(`${analysis.id} processing: ${analysis.progress?.stage ?? "starting"}`);
  await sleep(5);
  analysis = await api("GET", `/analyses/${analysis.id}`);
}

if (analysis.status !== "succeeded") {
  throw new Error(`The check ${analysis.status}: ${analysis.error?.message}`);
}
const { level, action, reason, pages, images, text, findings } = analysis.summary;
console.log(`${analysis.id} ${analysis.status}`);
console.log(`level ${level}, action ${action}, reason ${reason ?? "none"}`);
console.log(`${pages} pages, ${images} photos examined, text: ${text ?? "not evaluated"}`);
console.log(`findings: ${findings.join(", ") || "none"}`);
console.log(`Report: ${analysis.report_url}`);

if (analysis.livemode) {
  const pdf = await request(`${base}/analyses/${analysis.id}/report.pdf`, {
    headers: { Authorization: `Bearer ${key}` },
  });
  const name = `Fileverity-${analysis.report_id}.pdf`;
  await writeFile(name, Buffer.from(await pdf.arrayBuffer()));
  console.log(`Saved ${name}`);
}
```

**Vérifier un PDF volumineux: Python**

```python
import os
import sys
import time
import uuid

import requests

BASE = "https://api.fileverity.com/v1"
KEY = os.environ.get("FILEVERITY_API_KEY")
if not KEY or len(sys.argv) < 2:
    sys.exit("Usage: FILEVERITY_API_KEY=... python check_pdf.py contract.pdf")

RETRYABLE = ("rate_limited", "idempotency_in_progress")


def request(method, url, **kwargs):
    for attempt in range(1, 6):
        try:
            response = requests.request(method, url, timeout=90, **kwargs)
        except requests.RequestException as error:
            response, problem = None, {"code": "network_error", "message": str(error)}
        else:
            if response.ok:
                if response.headers.get("x-ratelimit-remaining-requests") == "0":
                    time.sleep(int(response.headers["x-ratelimit-reset-requests"]))
                return response
            try:
                problem = response.json()["error"]
            except (ValueError, KeyError, TypeError):
                problem = {}
        status = response.status_code if response is not None else 0
        retry = status == 0 or status >= 500 or problem.get("code") in RETRYABLE
        if not retry or attempt == 5:
            code = problem.get("code", status)
            message = problem.get("message", "Request failed")
            request_id = problem.get("request_id", "unknown")
            raise RuntimeError(f"{code}: {message} (request {request_id})")
        wait = response.headers.get("retry-after") if response is not None else None
        time.sleep(int(wait or 2**attempt))


def api(method, route, headers=None, **kwargs):
    headers = {"Authorization": f"Bearer {KEY}", **(headers or {})}
    return request(method, BASE + route, headers=headers, **kwargs).json()


with open(sys.argv[1], "rb") as source:
    data = source.read()
file = api(
    "POST",
    "/files",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={
        "filename": os.path.basename(sys.argv[1]),
        "content_type": "application/pdf",
        "bytes": len(data),
    },
)

if file["status"] == "pending":
    request("PUT", file["upload"]["url"], headers=file["upload"]["headers"], data=data)
    for _ in range(15):
        if api("GET", f"/files/{file['id']}")["status"] == "uploaded":
            break
        time.sleep(2)
    else:
        sys.exit("The upload was not confirmed.")
print(f"{file['id']} uploaded")

analysis = api(
    "POST",
    "/analyses",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={"model": "fvdoc-3", "file": file["id"]},
)
deadline = time.time() + 15 * 60
while analysis["status"] == "processing":
    if time.time() > deadline:
        sys.exit(f"Still processing. Fetch it later with GET /analyses/{analysis['id']}")
    stage = (analysis["progress"] or {}).get("stage", "starting")
    print(f"{analysis['id']} processing: {stage}")
    time.sleep(5)
    analysis = api("GET", f"/analyses/{analysis['id']}")

if analysis["status"] != "succeeded":
    problem = analysis["error"] or {}
    sys.exit(f"The check {analysis['status']}: {problem.get('message')}")
summary = analysis["summary"]
reason = summary["reason"] or "none"
text = summary["text"] or "not evaluated"
findings = ", ".join(summary["findings"]) or "none"
print(f"{analysis['id']} {analysis['status']}")
print(f"level {summary['level']}, action {summary['action']}, reason {reason}")
print(f"{summary['pages']} pages, {summary['images']} photos examined, text: {text}")
print(f"findings: {findings}")
print(f"Report: {analysis['report_url']}")

if analysis["livemode"]:
    url = f"{BASE}/analyses/{analysis['id']}/report.pdf"
    pdf = request("GET", url, headers={"Authorization": f"Bearer {KEY}"})
    name = f"Fileverity-{analysis['report_id']}.pdf"
    with open(name, "wb") as target:
        target.write(pdf.content)
    print(f"Saved {name}")
```

**Sortie de la vérification du PDF**

```text
file_Pw3mY6zB8Hk2pQ4vN9sL1xR7 uploaded
an_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: queued
an_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: analyzing
an_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: reporting
an_Zc4vB7nP1sK8tL3wR6yQm2Xa succeeded
level L2, action accept, reason clean
12 pages, 3 photos examined, text: human
findings: none
Report: https://fileverity.com/verify/FV-3PXH-6WQ2-8NDC
Saved Fileverity-FV-3PXH-6WQ2-8NDC.pdf
```

- Le `PUT` va directement au stockage. Il ne comporte aucun en-tête `Authorization`, et les en-têtes de `upload.headers` sont envoyés exactement tels qu’ils ont été renvoyés. Voir [Téléversement par URL présignée](https://fileverity.com/fr/docs/files.md#presigned).
- Avec une clé de test, le fichier est renvoyé avec le statut `uploaded` et il n’y a rien à envoyer : le programme ignore donc le téléversement. L’analyse renvoie immédiatement un exemple fixe, et il n’existe pas de PDF d’exemple : le programme n’enregistre donc le rapport que pour une clé Live.
- Téléchargez le PDF dès la fin de la vérification. Les images des pages et les extraits de texte d’un document sont conservés pendant 3 heures, et un PDF téléchargé plus tard les omet. Voir [Rapport PDF](https://fileverity.com/fr/docs/analyses.md#pdf).
- Une vérification qui se termine par `failed` ou `canceled` n’a pas été facturée. Le programme s’arrête avec le message d’erreur de l’analyse.

## Détecter un texte rédigé par IA

La détection de texte tient en un seul appel synchrone. Le programme lit un fichier texte, l’envoie à `POST /text/detections` et affiche le verdict, le score, la part de texte rédigée par IA et chaque passage rédigé par IA avec ses positions. La détection de texte rédigé par IA prend en charge l’anglais, le français, l’allemand, l’espagnol et le chinois (`en`, `fr`, `de`, `es`, `zh`). La langue est détectée automatiquement ; indiquez un code en second argument pour la définir. Le texte doit comporter au moins 100 mots et au plus 100 000 caractères.

Enregistrez-le sous le nom `detect-text.mjs` ou `detect_text.py`, puis exécutez `node detect-text.mjs essay.txt` ou `python detect_text.py essay.txt fr`. Le [Démarrage rapide](https://fileverity.com/fr/docs/quickstart.md#text) contient une commande qui écrit un `essay.txt` d’exemple. Avec une clé de test, la réponse est un résultat fixe pour un texte rédigé par une personne : le programme n’affiche donc aucun passage.

**Détecter un texte rédigé par IA: JavaScript**

```javascript
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";

const base = "https://api.fileverity.com/v1";
const key = process.env.FILEVERITY_API_KEY;
const [, , path, language] = process.argv;
if (!key || !path) {
  throw new Error("Usage: FILEVERITY_API_KEY=... node detect-text.mjs essay.txt [en|fr|de|es|zh]");
}

const retryable = ["rate_limited", "idempotency_in_progress"];
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));

async function request(url, init) {
  for (let attempt = 1; ; attempt++) {
    let response = null;
    let problem;
    try {
      response = await fetch(url, init);
      if (response.ok) {
        const remaining = response.headers.get("x-ratelimit-remaining-requests");
        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));
        if (remaining === "0") await sleep(reset);
        return response;
      }
      problem = (await response.json().catch(() => ({}))).error ?? {};
    } catch (error) {
      problem = { code: "network_error", message: error.message };
    }
    const status = response?.status ?? 0;
    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);
    if (!retry || attempt === 5) {
      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;
      throw new Error(`${code}: ${message} (request ${id})`);
    }
    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);
  }
}

async function api(method, route, { headers, body } = {}) {
  const auth = { Authorization: `Bearer ${key}` };
  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });
  return response.json();
}

const text = await readFile(path, "utf8");
const detection = await api("POST", "/text/detections", {
  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },
  body: JSON.stringify({ text, language }),
});

const { verdict, score, ai_fraction: share, usage, segments } = detection;
console.log(`verdict: ${verdict} (score ${score.toFixed(2)})`);
console.log(`AI-written share: ${Math.round(share * 100)}% of ${usage.characters} characters`);
console.log(`language: ${detection.language}, billed units: ${usage.billed_units}`);
const points = Array.from(text);
for (const { start, end, score: passage } of segments) {
  const excerpt = points.slice(start, Math.min(end, start + 80)).join("");
  console.log(`${start}-${end} score ${passage.toFixed(2)}: ${excerpt}`);
}
```

**Détecter un texte rédigé par IA: Python**

```python
import os
import sys
import time
import uuid

import requests

BASE = "https://api.fileverity.com/v1"
KEY = os.environ.get("FILEVERITY_API_KEY")
if not KEY or len(sys.argv) < 2:
    sys.exit("Usage: FILEVERITY_API_KEY=... python detect_text.py essay.txt [en|fr|de|es|zh]")
sys.stdout.reconfigure(encoding="utf-8")

RETRYABLE = ("rate_limited", "idempotency_in_progress")


def request(method, url, **kwargs):
    for attempt in range(1, 6):
        try:
            response = requests.request(method, url, timeout=90, **kwargs)
        except requests.RequestException as error:
            response, problem = None, {"code": "network_error", "message": str(error)}
        else:
            if response.ok:
                if response.headers.get("x-ratelimit-remaining-requests") == "0":
                    time.sleep(int(response.headers["x-ratelimit-reset-requests"]))
                return response
            try:
                problem = response.json()["error"]
            except (ValueError, KeyError, TypeError):
                problem = {}
        status = response.status_code if response is not None else 0
        retry = status == 0 or status >= 500 or problem.get("code") in RETRYABLE
        if not retry or attempt == 5:
            code = problem.get("code", status)
            message = problem.get("message", "Request failed")
            request_id = problem.get("request_id", "unknown")
            raise RuntimeError(f"{code}: {message} (request {request_id})")
        wait = response.headers.get("retry-after") if response is not None else None
        time.sleep(int(wait or 2**attempt))


def api(method, route, headers=None, **kwargs):
    headers = {"Authorization": f"Bearer {KEY}", **(headers or {})}
    return request(method, BASE + route, headers=headers, **kwargs).json()


with open(sys.argv[1], encoding="utf-8") as source:
    text = source.read()
language = sys.argv[2] if len(sys.argv) > 2 else None
detection = api(
    "POST",
    "/text/detections",
    headers={"Idempotency-Key": str(uuid.uuid4())},
    json={"text": text, "language": language},
)

usage = detection["usage"]
share = round(detection["ai_fraction"] * 100)
print(f"verdict: {detection['verdict']} (score {detection['score']:.2f})")
print(f"AI-written share: {share}% of {usage['characters']} characters")
print(f"language: {detection['language']}, billed units: {usage['billed_units']}")
for segment in detection["segments"]:
    start, end = segment["start"], segment["end"]
    excerpt = text[start : min(end, start + 80)]
    print(f"{start}-{end} score {segment['score']:.2f}: {excerpt}")
```

**Sortie pour l’exemple du Démarrage rapide**

```text
verdict: mixed (score 0.97)
AI-written share: 50% of 889 characters
language: en, billed units: 1
267-495 score 0.97: In today's fast-paced logistics landscape, it is essential for businesses to pri
590-809 score 0.93: Moving forward, it is crucial to implement robust quality assurance measures and
```

`start` et `end` comptent des points de code Unicode, et non des octets ni des unités UTF-16. Les chaînes Python sont déjà indexées par point de code ; le programme Node.js découpe d’abord le texte avec `Array.from`. Voir [Positions des segments](https://fileverity.com/fr/docs/text-detections.md#offsets).

> **Remarque.** La détection de texte renvoie une estimation statistique, et non une preuve de l’identité de l’auteur. Utilisez-la comme un élément parmi d’autres d’un examen humain, et ne vous appuyez pas uniquement dessus pour prendre une décision concernant une personne.

## Recevoir des webhooks et vérifier les signatures

Un webhook prévient votre serveur lorsqu’une vérification se termine, ce qui vous évite d’interroger l’API. Ce récepteur est un serveur HTTP minimal : le module `http` de Node.js, et Flask pour Python. Il vérifie la signature de chaque envoi, ignore un envoi qu’il a déjà traité et affiche l’événement. Il suit la page [Webhooks](https://fileverity.com/fr/docs/webhooks.md#signature).

1. Rendez le port 3000 accessible à une adresse HTTPS publique, par exemple avec un tunnel vers votre machine. Les URL de point de terminaison doivent utiliser HTTPS sur le port par défaut 443 et pointer vers une adresse publique.
2. Dans la Console, ouvrez **Webhooks**, ajoutez un point de terminaison avec l’URL `https://your-host/webhooks/fileverity` et choisissez ses événements. Copiez le secret de signature, qui commence par `whsec_`. Il n’est affiché qu’une seule fois.
3. Enregistrez le programme sous le nom `webhook-receiver.mjs` ou `webhook_receiver.py` et lancez-le avec le secret : `FILEVERITY_WEBHOOK_SECRET=whsec_... node webhook-receiver.mjs` ou `FILEVERITY_WEBHOOK_SECRET=whsec_... python webhook_receiver.py`.
4. Choisissez **Envoyer un test** sur le point de terminaison. Les clés de test n’envoient jamais d’événements ; **Envoyer un test** livre à la place un événement d’exemple signé.

**Recevoir des webhooks: JavaScript**

```javascript
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";

const secret = process.env.FILEVERITY_WEBHOOK_SECRET;
if (!secret) throw new Error("Set FILEVERITY_WEBHOOK_SECRET to the endpoint's signing secret.");
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const port = Number(process.env.PORT) || 3000;
const seen = new Set();

function verify(headers, body) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"];
  if (!id || !timestamp || !signatures) return false;
  if (!(Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300)) return false;
  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.`).update(body).digest();
  return signatures.split(" ").some((entry) => {
    const [version, value = ""] = entry.split(",");
    const given = Buffer.from(value, "base64");
    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);
  });
}

function handleEvent(event) {
  const { type, data } = event;
  if (data.test) {
    console.log(`Test event ${event.id}`);
  } else if (type === "analysis.completed") {
    const { level, action } = data.summary;
    console.log(`${data.id} succeeded: level ${level}, action ${action}, ${data.report_url}`);
  } else if (type === "analysis.failed") {
    console.log(`${data.id} ${data.status}: ${data.error.code}`);
  } else if (type === "balance.low") {
    const money = (cents) => `US$${(cents / 100).toFixed(2)}`;
    console.log(`Balance ${money(data.balance)} is below your ${money(data.threshold)} alert`);
  }
}

async function receive(req, res) {
  if (req.method !== "POST" || req.url !== "/webhooks/fileverity") return res.writeHead(404).end();
  const chunks = [];
  let size = 0;
  for await (const chunk of req) {
    size += chunk.length;
    if (size > 1_000_000) return res.writeHead(413).end();
    chunks.push(chunk);
  }
  const body = Buffer.concat(chunks);
  if (!verify(req.headers, body)) return res.writeHead(400).end();
  const event = JSON.parse(body.toString("utf8"));
  try {
    if (!seen.has(event.id)) {
      handleEvent(event);
      seen.add(event.id);
    }
  } catch (error) {
    console.error(`Could not handle ${event.id}`, error);
    return res.writeHead(500).end();
  }
  res.writeHead(200).end();
}

createServer((req, res) => receive(req, res).catch(() => res.destroy())).listen(port, () => console.log(`Listening on port ${port}`));
```

**Recevoir des webhooks: Python**

```python
import base64
import hashlib
import hmac
import json
import os
import threading
import time

from flask import Flask, request

SECRET = os.environ.get("FILEVERITY_WEBHOOK_SECRET")
if not SECRET:
    raise SystemExit("Set FILEVERITY_WEBHOOK_SECRET to the endpoint's signing secret.")
KEY = base64.b64decode(SECRET.removeprefix("whsec_"))
seen = set()
lock = threading.Lock()
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 1_000_000


def verify(headers, body):
    message_id = headers.get("webhook-id")
    timestamp = headers.get("webhook-timestamp", "")
    signatures = headers.get("webhook-signature", "")
    if not message_id or not timestamp.isdecimal() or abs(time.time() - int(timestamp)) > 300:
        return False
    signed = f"{message_id}.{timestamp}.".encode() + body
    expected = hmac.new(KEY, signed, hashlib.sha256).digest()
    for entry in signatures.split(" "):
        version, _, value = entry.partition(",")
        try:
            given = base64.b64decode(value, validate=True)
        except ValueError:
            continue
        if version == "v1" and hmac.compare_digest(given, expected):
            return True
    return False


def handle_event(event):
    data = event["data"]
    if data.get("test"):
        print(f"Test event {event['id']}")
    elif event["type"] == "analysis.completed":
        level, action = data["summary"]["level"], data["summary"]["action"]
        print(f"{data['id']} succeeded: level {level}, action {action}, {data['report_url']}")
    elif event["type"] == "analysis.failed":
        print(f"{data['id']} {data['status']}: {data['error']['code']}")
    elif event["type"] == "balance.low":
        balance, threshold = data["balance"] / 100, data["threshold"] / 100
        print(f"Balance US${balance:.2f} is below your US${threshold:.2f} alert")


@app.post("/webhooks/fileverity")
def receive():
    body = request.get_data()
    if not verify(request.headers, body):
        return "", 400
    event = json.loads(body)
    with lock:
        try:
            if event["id"] not in seen:
                handle_event(event)
                seen.add(event["id"])
        except Exception:
            app.logger.exception("Could not handle %s", event["id"])
            return "", 500
    return "", 200


if __name__ == "__main__":
    port = int(os.environ.get("PORT", "3000"))
    print(f"Listening on port {port}")
    app.run(port=port)
```

**Sortie du récepteur**

```text
Listening on port 3000
Test event evt_Qm4vB7nP1sK8tL3wR6yZ2cXa
an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeeded: level L2, action accept, https://fileverity.com/verify/FV-7K2M-9QXD-4T8R
```

- **Corps brut.** La signature porte sur les octets exacts qui ont été envoyés. Les programmes lisent le corps sous forme d’octets, calculent un HMAC-SHA256 sur `webhook-id`, un point, `webhook-timestamp`, un point et ces octets, avec le secret décodé depuis le base64 comme clé, et ne désérialisent le JSON qu’une fois la signature validée.
- **Comparaison en temps constant.** `timingSafeEqual` en Node.js et `hmac.compare_digest` en Python comparent le résultat à chaque signature `v1` de l’en-tête `webhook-signature`.
- **Tolérance sur l’horodatage.** Un `webhook-timestamp` qui s’écarte de plus de cinq minutes de l’horloge, ou qui n’est pas un nombre, est rejeté.
- **Réponses rapides.** Un envoi valide reçoit `200` dès que l’événement est traité. Une signature incorrecte reçoit `400`. Si le traitement échoue, le programme répond `500` et Fileverity réessaie plus tard avec le même ID d’événement. Voir [Envoi et nouvelles tentatives](https://fileverity.com/fr/docs/webhooks.md#delivery).
- **Doublons.** Les événements sont livrés au moins une fois, et une nouvelle tentative conserve l’ID de l’événement. Les programmes mémorisent les ID déjà traités et répondent `200` sans traiter un événement deux fois. En production, remplacez l’ensemble en mémoire par une contrainte d’unicité sur l’ID de l’événement dans votre base de données, et placez le travail dans une file d’attente pour que la réponse reste rapide.
- **Données de l’événement.** `analysis.completed` et `analysis.failed` contiennent l’analyse sans `result` ; appelez `GET /analyses/{id}` lorsque vous avez besoin du rapport complet. `balance` et `threshold` dans `balance.low` sont exprimés en cents US.

## Pour aller plus loin

- Exécutez un processus de réception complet avec examen humain dans [Vérifications de documents dans un processus de réception](https://fileverity.com/fr/docs/intake-workflow.md).
- Retrouvez n’importe quel champ dans la [référence de l’API](https://fileverity.com/fr/docs/api-reference.md), ou chargez le document OpenAPI dans vos propres outils.
- Passez en production avec une clé Live, une limite de dépense mensuelle et un webhook `balance.low`. Voir [Tarifs et facturation](https://fileverity.com/fr/docs/pricing.md#alerts).
- Besoin d’aide ? Indiquez la valeur de l’en-tête `x-request-id` lorsque vous [contactez l’assistance](https://fileverity.com/fr/contact).

