Documentation de l’API Fileverity
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.
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, Analyses, Détections de texte et Webhooks.
| Programme | Ce qu’il fait | Appels |
|---|---|---|
| Vérifier une 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 | 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 | Envoie un fichier texte et affiche le verdict, le score et chaque passage rédigé par IA. | POST /text/detections |
| Recevoir des 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 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’utiliserimportetawaitau niveau supérieur, puis exécutez-le avecnode. Rien à installer. - Python : exécutez
pip install requests. Le récepteur de webhooks nécessite aussipip install flask.
export FILEVERITY_API_KEY=fv_test_...$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. |
| 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. |
| 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. |
| 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.
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.
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}`);import osimport sysimport timeimport 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']}")an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeededlevel L2, action accept, assessment authentic_untouchedAI generation: very_low, editing: noneReport: https://fileverity.com/verify/FV-7K2M-9QXD-4T8Rsummary 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. 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. Pour un document chiffré, ajoutez options: { password } à la requête d’analyse.
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}`);}import osimport sysimport timeimport 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 * 60while 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}")file_Pw3mY6zB8Hk2pQ4vN9sL1xR7 uploadedan_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: queuedan_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: analyzingan_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: reportingan_Zc4vB7nP1sK8tL3wR6yQm2Xa succeededlevel L2, action accept, reason clean12 pages, 3 photos examined, text: humanfindings: noneReport: https://fileverity.com/verify/FV-3PXH-6WQ2-8NDCSaved Fileverity-FV-3PXH-6WQ2-8NDC.pdf- Le
PUTva directement au stockage. Il ne comporte aucun en-têteAuthorization, et les en-têtes deupload.headerssont envoyés exactement tels qu’ils ont été renvoyés. Voir Téléversement par URL présignée. - Avec une clé de test, le fichier est renvoyé avec le statut
uploadedet 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.
- Une vérification qui se termine par
failedoucanceledn’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 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.
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}`);}import osimport sysimport timeimport 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 Nonedetection = 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}")verdict: mixed (score 0.97)AI-written share: 50% of 889 characterslanguage: en, billed units: 1267-495 score 0.97: In today's fast-paced logistics landscape, it is essential for businesses to pri590-809 score 0.93: Moving forward, it is crucial to implement robust quality assurance measures andstart 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.
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.
- 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.
- Dans la Console, ouvrez Webhooks, ajoutez un point de terminaison avec l’URL
https://your-host/webhooks/fileverityet choisissez ses événements. Copiez le secret de signature, qui commence parwhsec_. Il n’est affiché qu’une seule fois. - Enregistrez le programme sous le nom
webhook-receiver.mjsouwebhook_receiver.pyet lancez-le avec le secret :FILEVERITY_WEBHOOK_SECRET=whsec_... node webhook-receiver.mjsouFILEVERITY_WEBHOOK_SECRET=whsec_... python webhook_receiver.py. - 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é.
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}`));import base64import hashlibimport hmacimport jsonimport osimport threadingimport 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)Listening on port 3000Test event evt_Qm4vB7nP1sK8tL3wR6yZ2cXaan_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.
timingSafeEqualen Node.js ethmac.compare_digesten Python comparent le résultat à chaque signaturev1de l’en-têtewebhook-signature. - Tolérance sur l’horodatage. Un
webhook-timestampqui 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
200dès que l’événement est traité. Une signature incorrecte reçoit400. Si le traitement échoue, le programme répond500et Fileverity réessaie plus tard avec le même ID d’événement. Voir Envoi et nouvelles tentatives. - 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
200sans 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.completedetanalysis.failedcontiennent l’analyse sansresult; appelezGET /analyses/{id}lorsque vous avez besoin du rapport complet.balanceetthresholddansbalance.lowsont 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.
- Retrouvez n’importe quel champ dans la référence de l’API, 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. - Besoin d’aide ? Indiquez la valeur de l’en-tête
x-request-idlorsque vous contactez l’assistance.