Documentación de la API de Fileverity
Código de ejemplo
Programas completos en Node.js y Python para la API de Fileverity: comprobar una imagen, comprobar un PDF grande, detectar texto escrito con IA y verificar firmas de webhooks.
Esta página contiene cuatro programas completos para la API de Fileverity, cada uno en Node.js 18 o posterior y en Python 3.9 o posterior: comprobar una imagen y esperar el resultado, comprobar un PDF grande, detectar texto escrito con IA y recibir webhooks con verificación de firma. Copie uno, defina FILEVERITY_API_KEY y ejecútelo.
Los programas llaman directamente a la API HTTPS, con el fetch integrado en Node.js y el paquete requests en Python, así que solo necesita un cliente HTTP. Las llamadas, los campos y los estados son los descritos en Archivos, Análisis, Detecciones de texto y Webhooks.
| Programa | Qué hace | Llamadas |
|---|---|---|
| Comprobar una imagen | Sube una imagen, espera el resultado e imprime el resumen y la URL del informe. | POST /files, POST /analyses, GET /analyses/{id} |
| Comprobar un PDF grande | Sube un documento con una URL prefirmada, consulta periódicamente hasta que termina la comprobación y guarda el informe en PDF. | POST /files, GET /files/{id}, POST /analyses, GET /analyses/{id}, GET /analyses/{id}/report.pdf |
| Detectar texto escrito con IA | Envía un archivo de texto e imprime el veredicto, la puntuación y cada pasaje escrito con IA. | POST /text/detections |
| Recibir webhooks | Verifica la firma de cada envío, ignora los duplicados y gestiona los eventos. | Su endpoint, llamado con analysis.completed, analysis.failed y balance.low |
Antes de ejecutar
- Cree una clave Test en Claves de API de la Consola. Una clave de prueba devuelve resultados de ejemplo fijos y nunca se cobra, así que todos los programas se ejecutan gratis. Para comprobar archivos reales, cree una clave Live. Los programas no cambian.
- Node.js: guarde cada programa como un archivo
.mjs, lo que le permite usarimportyawaitde nivel superior, y ejecútelo connode. No hay nada que instalar. - Python: ejecute
pip install requests. El receptor de webhooks necesita ademáspip install flask.
export FILEVERITY_API_KEY=fv_test_...$env:FILEVERITY_API_KEY = "fv_test_..."Cómo gestionan los programas las claves, los errores y los reintentos
Los tres programas que llaman a la API comparten las mismas funciones request y api. Cópielas en su propio código tal cual.
| Aspecto | Qué hacen los programas |
|---|---|
| Clave | Se lee de FILEVERITY_API_KEY y nunca se escribe en el código. Llame a la API desde su servidor, no desde código del navegador. |
| Idempotencia | Cada POST envía una Idempotency-Key, un UUID nuevo por solicitud, y lo reutiliza en cada reintento, de modo que un reintento nunca ejecuta ni cobra una comprobación dos veces. Para que sea seguro volver a ejecutar todo el script, derive en su lugar la clave del ID de su propio registro. Consulte Idempotencia. |
| Errores | Una respuesta que no es 2xx incluye error.type, error.code, error.message, error.param y error.request_id. Los programas se detienen con code: message (request request_id). Base su lógica en code e indique el ID de solicitud a soporte. Consulte Errores. |
| Reintentos | Se reintentan los fallos de red, un estado 5xx (internal_error, analysis_failed, service_unavailable), rate_limited (429) e idempotency_in_progress (409), hasta 5 intentos. La espera es la del encabezado retry-after cuando la respuesta lo incluye; si no, 2, 4, 8 y 16 segundos. Cualquier otro 4xx se detiene de inmediato. pending_files_limit también es un 429, pero no tiene retry-after, así que no se reintenta. |
| Límite de frecuencia | Cuando una respuesta indica x-ratelimit-remaining-requests: 0, el programa espera x-ratelimit-reset-requests segundos antes de la siguiente llamada, para no recibir un 429. Consulte Límites de frecuencia y encabezados. |
| Tiempos de espera | Los programas de Python agotan el tiempo de espera a los 90 segundos por solicitud, un plazo superior a los 60 segundos que Prefer: wait puede mantener abierta una solicitud. |
Comprobar una imagen y esperar el resultado
El programa sube la imagen como multipart, crea un análisis con Prefer: wait=60 para que la solicitud se mantenga abierta hasta que el resultado esté listo, e imprime el resumen y la URL del informe. Si la espera termina antes, la API responde 202 con status: processing y el programa consulta GET /analyses/{id} cada 3 segundos. Consulte Esperar, consultar periódicamente o usar webhooks.
Guárdelo como check-image.mjs o check_image.py y ejecute node check-image.mjs photo.jpg o python check_image.py photo.jpg. Multipart admite imágenes de hasta 4 MB. Para una imagen más grande, de hasta 10 MB, use la subida prefirmada del programa siguiente con el tipo de contenido de la imagen, por ejemplo 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 es el veredicto estable y legible por máquinas de una comprobación de imagen: level, action, assessment, ai y edit, descritos en Resumen de imagen. report_url es la página de verificación pública del informe. Con una clave de prueba el resultado es un ejemplo fijo, y su URL del informe no lleva a ninguna página.
Comprobar un PDF grande
Los documentos de hasta 50 MB se suben en tres pasos: pida una URL de subida con POST /files y un cuerpo JSON, envíe los bytes a esa URL con PUT y espere hasta que el archivo muestre uploaded. Después, el programa crea el análisis y lo consulta cada 5 segundos, porque los documentos pueden quedar en cola y una comprobación puede tardar. Imprime el progreso, el resumen y la URL del informe, y guarda el informe en PDF junto al script.
Guárdelo como check-pdf.mjs o check_pdf.py y ejecute node check-pdf.mjs contract.pdf o python check_pdf.py contract.pdf. Para otro tipo de documento, cambie content_type por uno de los tipos de contenido admitidos. Para un documento cifrado, añada options: { password } a la solicitud de análisis.
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- El
PUTva directamente al almacenamiento. No lleva encabezadoAuthorization, y los encabezados deupload.headersse envían exactamente como se devuelven. Consulte Subida prefirmada. - Con una clave de prueba el archivo se devuelve como
uploadedy no hay nada que enviar, así que el programa omite la subida. El análisis devuelve un ejemplo fijo de inmediato y no hay un PDF de ejemplo, por lo que el programa guarda el informe solo con una clave Live. - Descargue el PDF en cuanto termine la comprobación. Las imágenes de páginas y los extractos de texto de un documento se conservan 3 horas, y un PDF descargado más tarde los omite. Consulte Informe en PDF.
- Una comprobación que termina con
failedocanceledno se cobró. El programa se detiene con el mensaje de error del análisis.
Detectar texto escrito con IA
La detección de texto requiere una sola llamada síncrona. El programa lee un archivo de texto, lo envía a POST /text/detections e imprime el veredicto, la puntuación, la proporción de texto escrito con IA y cada pasaje escrito con IA con sus posiciones. La detección de texto escrito con IA admite inglés, francés, alemán, español y chino (en, fr, de, es, zh). El idioma se detecta automáticamente; pase un código como segundo argumento para fijarlo. El texto debe tener al menos 100 palabras y como máximo 100 000 caracteres.
Guárdelo como detect-text.mjs o detect_text.py y ejecute node detect-text.mjs essay.txt o python detect_text.py essay.txt fr. El Inicio rápido incluye un comando que escribe un essay.txt de ejemplo. Con una clave de prueba la respuesta es un resultado fijo para un texto escrito por una persona, por lo que el programa no imprime ningún pasaje.
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 y end cuentan puntos de código Unicode, no bytes ni unidades UTF-16. Las cadenas de Python ya se indexan por punto de código; el programa de Node.js divide antes el texto con Array.from. Consulte Posiciones de los segmentos.
La detección de texto devuelve una estimación estadística, no una prueba de autoría. Úsela como un elemento más de una revisión humana y no se base solo en ella para tomar decisiones que afecten a una persona.
Recibir webhooks y verificar firmas
Un webhook avisa a su servidor cuando termina una comprobación, para que no tenga que consultar la API periódicamente. Este receptor es un servidor HTTP mínimo: el módulo http de Node.js y Flask para Python. Verifica la firma de cada envío, ignora un envío que ya ha gestionado e imprime el evento. Sigue lo descrito en la página Webhooks.
- Haga que el puerto 3000 sea accesible en una dirección HTTPS pública, por ejemplo con un túnel hacia su máquina. Las URL de los endpoints deben usar HTTPS en el puerto predeterminado 443 y apuntar a una dirección pública.
- En la Consola, abra Webhooks, añada un endpoint con la URL
https://your-host/webhooks/fileverityy elija sus eventos. Copie el secreto de firma, que empieza porwhsec_. Se muestra una sola vez. - Guarde el programa como
webhook-receiver.mjsowebhook_receiver.pye inícielo con el secreto:FILEVERITY_WEBHOOK_SECRET=whsec_... node webhook-receiver.mjsoFILEVERITY_WEBHOOK_SECRET=whsec_... python webhook_receiver.py. - Elija Enviar prueba en el endpoint. Las claves de prueba nunca envían eventos; Enviar prueba envía en su lugar un evento de ejemplo firmado.
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- Cuerpo sin procesar. La firma cubre los bytes exactos que se enviaron. Los programas leen el cuerpo como bytes, calculan un HMAC-SHA256 sobre
webhook-id, un punto,webhook-timestamp, un punto y esos bytes, con el secreto decodificado desde base64 como clave, y analizan el JSON solo cuando la firma es válida. - Comparación en tiempo constante.
timingSafeEqualen Node.js yhmac.compare_digesten Python comparan el resultado con cada firmav1del encabezadowebhook-signature. - Tolerancia de la marca de tiempo. Se rechaza un
webhook-timestampque difiera en más de cinco minutos del reloj, o que no sea un número. - Respuestas rápidas. Un envío válido recibe
200en cuanto se gestiona el evento. Una firma incorrecta recibe400. Si la gestión falla, el programa responde500y Fileverity reintenta más tarde con el mismo ID de evento. Consulte Envío y reintentos. - Duplicados. Los eventos se envían al menos una vez y un reintento conserva el ID del evento. Los programas recuerdan los ID que ya han gestionado y responden
200sin gestionar un evento dos veces. En producción, sustituya el conjunto en memoria por una restricción de unicidad sobre el ID del evento en su base de datos y ponga el trabajo en una cola para que la respuesta siga siendo rápida. - Datos del evento.
analysis.completedyanalysis.failedincluyen el análisis sinresult; llame aGET /analyses/{id}cuando necesite el informe completo.balanceythresholdenbalance.lowestán en centavos de dólar estadounidense.
Siguientes pasos
- Ejecute un flujo de recepción completo con revisión humana en Comprobaciones de documentos en un flujo de recepción.
- Consulte cualquier campo en la referencia de la API o cargue el documento OpenAPI en sus propias herramientas.
- Pase a producción con una clave Live, un límite de gasto mensual y un webhook
balance.low. Consulte Precios y facturación. - ¿Necesita ayuda? Indique el valor del encabezado
x-request-idcuando contacte con soporte.