Aller au contenu

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.

Voir en MarkdownMis à jour le 2026-11-04

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.

ProgrammeCe qu’il faitAppels
Vérifier une imageTé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 volumineuxTé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 IAEnvoie un fichier texte et affiche le verdict, le score et chaque passage rédigé par IA.POST /text/detections
Recevoir des webhooksVé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’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.
export 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.

SujetCe 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.
IdempotenceChaque 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.
ErreursUne 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 tentativesUne 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ébitLorsqu’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’attenteLes 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}`);
an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeededlevel L2, action accept, assessment authentic_untouchedAI generation: very_low, editing: noneReport: 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. 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}`);}
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 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.
  • 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.
  • 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 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}`);}
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 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.

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.

  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é.
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}`));
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. 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.
  • 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