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

Fuente: https://fileverity.com/es/docs/sample-code

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](https://fileverity.com/es/docs/files.md), [Análisis](https://fileverity.com/es/docs/analyses.md), [Detecciones de texto](https://fileverity.com/es/docs/text-detections.md) y [Webhooks](https://fileverity.com/es/docs/webhooks.md).

| Programa | Qué hace | Llamadas |
| --- | --- | --- |
| [Comprobar una imagen](https://fileverity.com/es/docs/sample-code.md#image) | 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](https://fileverity.com/es/docs/sample-code.md#pdf) | 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](https://fileverity.com/es/docs/sample-code.md#text) | Envía un archivo de texto e imprime el veredicto, la puntuación y cada pasaje escrito con IA. | `POST /text/detections` |
| [Recibir webhooks](https://fileverity.com/es/docs/sample-code.md#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](https://fileverity.com/console/keys?lang=es) 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 usar `import` y `await` de nivel superior, y ejecútelo con `node`. No hay nada que instalar.
- Python: ejecute `pip install requests`. El receptor de webhooks necesita además `pip install flask`.

**Definir la clave: Shell**

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

**Definir la clave: PowerShell**

```powershell
$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](https://fileverity.com/es/docs/idempotency.md). |
| 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](https://fileverity.com/es/docs/errors.md). |
| 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](https://fileverity.com/es/docs/rate-limits.md#headers). |
| 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](https://fileverity.com/es/docs/analyses.md#wait).

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

**Comprobar una imagen: 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}`);
```

**Comprobar una imagen: 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']}")
```

**Salida de la comprobación de imagen**

```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` 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](https://fileverity.com/es/docs/analyses.md#summary-image). `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](https://fileverity.com/es/docs/files.md#content-types). Para un documento cifrado, añada `options: { password }` a la solicitud de análisis.

**Comprobar un PDF grande: 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}`);
}
```

**Comprobar un PDF grande: 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}")
```

**Salida de la comprobación del 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
```

- El `PUT` va directamente al almacenamiento. No lleva encabezado `Authorization`, y los encabezados de `upload.headers` se envían exactamente como se devuelven. Consulte [Subida prefirmada](https://fileverity.com/es/docs/files.md#presigned).
- Con una clave de prueba el archivo se devuelve como `uploaded` y 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](https://fileverity.com/es/docs/analyses.md#pdf).
- Una comprobación que termina con `failed` o `canceled` no 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](https://fileverity.com/es/docs/quickstart.md#text) 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.

**Detectar texto escrito con 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}`);
}
```

**Detectar texto escrito con 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}")
```

**Salida para el ejemplo del Inicio rápido**

```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` 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](https://fileverity.com/es/docs/text-detections.md#offsets).

> **Nota.** 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](https://fileverity.com/es/docs/webhooks.md#signature).

1. 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.
2. En la Consola, abra **Webhooks**, añada un endpoint con la URL `https://your-host/webhooks/fileverity` y elija sus eventos. Copie el secreto de firma, que empieza por `whsec_`. Se muestra una sola vez.
3. Guarde el programa como `webhook-receiver.mjs` o `webhook_receiver.py` e inícielo con el secreto: `FILEVERITY_WEBHOOK_SECRET=whsec_... node webhook-receiver.mjs` o `FILEVERITY_WEBHOOK_SECRET=whsec_... python webhook_receiver.py`.
4. 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.

**Recibir 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}`));
```

**Recibir 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)
```

**Salida del receptor**

```text
Listening on port 3000
Test event evt_Qm4vB7nP1sK8tL3wR6yZ2cXa
an_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.** `timingSafeEqual` en Node.js y `hmac.compare_digest` en Python comparan el resultado con cada firma `v1` del encabezado `webhook-signature`.
- **Tolerancia de la marca de tiempo.** Se rechaza un `webhook-timestamp` que 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 `200` en cuanto se gestiona el evento. Una firma incorrecta recibe `400`. Si la gestión falla, el programa responde `500` y Fileverity reintenta más tarde con el mismo ID de evento. Consulte [Envío y reintentos](https://fileverity.com/es/docs/webhooks.md#delivery).
- **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 `200` sin 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.completed` y `analysis.failed` incluyen el análisis sin `result`; llame a `GET /analyses/{id}` cuando necesite el informe completo. `balance` y `threshold` en `balance.low` está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](https://fileverity.com/es/docs/intake-workflow.md).
- Consulte cualquier campo en la [referencia de la API](https://fileverity.com/es/docs/api-reference.md) 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](https://fileverity.com/es/docs/pricing.md#alerts).
- ¿Necesita ayuda? Indique el valor del encabezado `x-request-id` cuando [contacte con soporte](https://fileverity.com/es/contacto).

