# Comprobaciones de documentos en un flujo de recepción

> Añada comprobaciones de documentos a un flujo de recepción: suba el archivo, reciba el webhook, derive cada caso según el nivel de riesgo, ponga la revisión humana en cola, guarde el ID del informe y verifíquelo después.

Fuente: https://fileverity.com/es/docs/intake-workflow

Esta guía práctica añade comprobaciones de documentos antes de un proceso que recibe documentos de fuentes externas: solicitudes de préstamo, siniestros de seguros, alta de proveedores o informes de gastos. Cada documento se comprueba una vez, se deriva según su nivel de riesgo y su ID de informe se conserva como registro de auditoría. La API devuelve un nivel y una acción recomendada. La decisión la toman su política y sus revisores.

Usa el modelo `fvdoc-3` con las llamadas de [Archivos](https://fileverity.com/es/docs/files.md), [Análisis](https://fileverity.com/es/docs/analyses.md) y [Webhooks](https://fileverity.com/es/docs/webhooks.md). Con una clave de prueba, la subida y la comprobación devuelven un ejemplo al instante, pero no se envía ningún webhook y no hay PDF de ejemplo. Use **Enviar prueba** en un endpoint de webhook de la Consola para probar su receptor.

## El flujo

1. Suba el archivo con `POST /files` e inicie la comprobación con `POST /analyses`. Ponga el ID de su caso en `metadata` y envíe una `Idempotency-Key`.
2. Responda a su usuario de inmediato. Los documentos pueden quedar en cola, así que no mantenga abierta su solicitud; el webhook le avisa cuando termina la comprobación.
3. Reciba `analysis.completed` o `analysis.failed`, verifique la firma y ponga un trabajo en cola.
4. En el trabajo, obtenga el análisis, derívelo según su resumen y guarde el resultado y el informe en PDF con el caso.
5. Envíe a una cola de revisión, con el PDF adjunto, los casos que necesiten a una persona.
6. Más tarde, verifique el informe con el archivo almacenado.

## 1. Subir el archivo e iniciar la comprobación

Los archivos de hasta 4 MB se suben como multipart. Para archivos más grandes, de hasta 50 MB, use una subida prefirmada como se describe en [Archivos](https://fileverity.com/es/docs/files.md#presigned). Al crear el análisis se devuelve `202` con `status: processing` (una clave de prueba devuelve `200` con un ejemplo terminado), y su `metadata` vuelve en el análisis y en cada webhook correspondiente.

**Enviar un documento: cURL**

```bash
FILE_ID=$(curl -s https://api.fileverity.com/v1/files \
  -H "Authorization: Bearer $FILEVERITY_API_KEY" \
  -H "Idempotency-Key: intake-A-10492-file" \
  -F file=@application.pdf | jq -r .id)

curl https://api.fileverity.com/v1/analyses \
  -H "Authorization: Bearer $FILEVERITY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: intake-A-10492-check" \
  -d "{\"model\": \"fvdoc-3\", \"file\": \"$FILE_ID\", \"metadata\": {\"case_id\": \"A-10492\"}}"
```

**Enviar un documento: JavaScript**

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

const base = "https://api.fileverity.com/v1";
const auth = { Authorization: "Bearer " + process.env.FILEVERITY_API_KEY };

async function call(path, init) {
  const response = await fetch(base + path, init);
  const body = await response.json();
  if (!response.ok) throw new Error(body.error.code + " (" + body.error.request_id + ")");
  return body;
}

export async function submit(caseId, path) {
  const form = new FormData();
  form.append("file", new Blob([await readFile(path)]), basename(path));
  const file = await call("/files", {
    method: "POST",
    headers: { ...auth, "Idempotency-Key": `intake-${caseId}-file` },
    body: form,
  });
  return call("/analyses", {
    method: "POST",
    headers: { ...auth, "Content-Type": "application/json", "Idempotency-Key": `intake-${caseId}-check` },
    body: JSON.stringify({ model: "fvdoc-3", file: file.id, metadata: { case_id: caseId } }),
  });
}
```

**Enviar un documento: Python**

```python
import os
import requests

BASE = "https://api.fileverity.com/v1"
HEADERS = {"Authorization": "Bearer " + os.environ["FILEVERITY_API_KEY"]}

def call(method, path, **kwargs):
    headers = {**HEADERS, **kwargs.pop("headers", {})}
    response = requests.request(method, BASE + path, headers=headers, timeout=90, **kwargs)
    body = response.json()
    if not response.ok:
        raise RuntimeError(body["error"]["code"] + " (" + body["error"]["request_id"] + ")")
    return body

def submit(case_id, path):
    with open(path, "rb") as source:
        file = call("POST", "/files", headers={"Idempotency-Key": f"intake-{case_id}-file"}, files={"file": source})
    return call(
        "POST",
        "/analyses",
        headers={"Idempotency-Key": f"intake-{case_id}-check"},
        json={"model": "fvdoc-3", "file": file["id"], "metadata": {"case_id": case_id}},
    )
```

**202 Accepted**

```json
{
  "id": "an_8Hk2pQ4vN9sL1xR7cW3mY6zB",
  "object": "analysis",
  "created": 1793700120,
  "model": "fvdoc-3-2026-11-03",
  "livemode": true,
  "status": "processing",
  "file": {
    "name": "application.pdf",
    "bytes": 18874368,
    "format": "pdf"
  },
  "report_id": null,
  "report_url": null,
  "summary": null,
  "result": null,
  "error": null,
  "progress": {
    "stage": "queued",
    "done": 0,
    "total": 0,
    "position": 1
  },
  "usage": {
    "amount": 599,
    "currency": "usd",
    "funding": "balance"
  },
  "metadata": {
    "case_id": "A-10492"
  },
  "completed_at": null,
  "expires_at": null
}
```

Guarde `id` con el caso. Como las claves se derivan del ID del caso, volver a ejecutar `submit` tras un tiempo de espera agotado devuelve las primeras respuestas en lugar de subir un segundo archivo o cobrar una segunda comprobación. Las claves se conservan 24 horas; consulte [Idempotencia](https://fileverity.com/es/docs/idempotency.md).

## 2. Recibir el resultado

Verifique la firma con la función `verify` de [Webhooks](https://fileverity.com/es/docs/webhooks.md#signature), guardada como `verify.js` o `verify.py`. Responda `2xx` con rapidez y haga el procesamiento en un trabajo. `enqueue` representa su propia cola de trabajos. Guarde cada trabajo bajo el ID del evento, para que un envío duplicado no cree un segundo trabajo.

**Receptor de webhooks: JavaScript**

```javascript
import express from "express";
import { verify } from "./verify.js";

const app = express();

app.post("/webhooks/fileverity", express.raw({ type: "application/json" }), async (req, res) => {
  let event;
  try {
    event = verify(process.env.FILEVERITY_WEBHOOK_SECRET, req.headers, req.body.toString("utf8"));
  } catch {
    return res.sendStatus(400);
  }
  if (event.type === "analysis.completed" || event.type === "analysis.failed") {
    await enqueue({ eventId: event.id, analysisId: event.data.id });
  }
  res.sendStatus(200);
});
```

**Receptor de webhooks: Python**

```python
import os

from fastapi import FastAPI, HTTPException, Request

from verify import verify

app = FastAPI()

@app.post("/webhooks/fileverity")
async def receive(request: Request):
    body = await request.body()
    try:
        event = verify(os.environ["FILEVERITY_WEBHOOK_SECRET"], request.headers, body)
    except (KeyError, ValueError):
        raise HTTPException(status_code=400)
    if event["type"] in ("analysis.completed", "analysis.failed"):
        enqueue(event["id"], event["data"]["id"])
    return {"ok": True}
```

## 3. Derivar según el nivel de riesgo

`summary` es el veredicto estable y legible por máquinas de una comprobación de documento. `level` va de `L1` a `L5` y `action` es el siguiente paso recomendado. Derive según ambos y trate cualquier valor que no reconozca como un caso para una persona.

| Resumen | Qué significa | Derivación de ejemplo |
| --- | --- | --- |
| `L1` o `L2`, `accept` | No se encontraron indicios de manipulación. `L1` significa que se verificó el origen. | Continúe automáticamente. Guarde el ID del informe. |
| `L1` o `L2`, `note` | Sin indicios de manipulación, pero el informe marca texto escrito con IA o una etiqueta de IA. | Continúe y marque el caso si su política limita el contenido escrito con IA. |
| `L3`, `review` | Requiere revisión manual. | Cola de revisión estándar. |
| `L4`, `review` | Riesgo alto. | Cola de revisión prioritaria. |
| `L5`, `reject` | El informe acredita problemas. | Cola de revisión prioritaria. Una persona confirma antes de rechazar nada. |
| `status` es `failed` o `canceled` | La comprobación no terminó y no se le cobró. | Suba de nuevo el original almacenado o envíe el caso a una persona. |

La asignación la define su política. Mueva `L3` a la cola prioritaria, o haga que `note` pase a revisión, cambiando la función. El conjunto de acciones también contiene `resubmit`, que la función trata como una solicitud de otra copia del archivo. Cada campo del resumen se describe en [Análisis](https://fileverity.com/es/docs/analyses.md#summary-document).

**Derivar una comprobación terminada: JavaScript**

```javascript
export function route(analysis) {
  if (analysis.status !== "succeeded") return "manual";
  const { action, level } = analysis.summary;
  if (action === "accept") return "accept";
  if (action === "note") return "accept_with_note";
  if (action === "resubmit") return "request_new_copy";
  if (action === "reject" || level === "L4" || level === "L5") return "priority_review";
  return "review";
}
```

**Derivar una comprobación terminada: Python**

```python
def route(analysis):
    if analysis["status"] != "succeeded":
        return "manual"
    action = analysis["summary"]["action"]
    level = analysis["summary"]["level"]
    if action == "accept":
        return "accept"
    if action == "note":
        return "accept_with_note"
    if action == "resubmit":
        return "request_new_copy"
    if action == "reject" or level in ("L4", "L5"):
        return "priority_review"
    return "review"
```

## 4. Guardar el ID del informe y el PDF

El trabajo obtiene el análisis, lo deriva y guarda lo que necesitará más adelante. Reutiliza `call`, `route` y las credenciales de los pasos anteriores, y `saveCase` y `saveReport` representan su propio almacenamiento. Descargue el PDF en el mismo trabajo: el resultado del análisis se conserva 7 días, las imágenes de páginas y los extractos de texto de un documento, 3 horas, y después Fileverity conserva solo el registro de verificación, no el informe completo.

**Procesar una comprobación terminada: JavaScript**

```javascript
export async function handleResult(analysisId) {
  const analysis = await call(`/analyses/${analysisId}`, { headers: auth });
  const decision = route(analysis);
  await saveCase(analysis.metadata.case_id, {
    analysisId: analysis.id,
    reportId: analysis.report_id,
    reportUrl: analysis.report_url,
    summary: analysis.summary,
    model: analysis.model,
    decision,
  });
  if (analysis.status === "succeeded") {
    const pdf = await fetch(`${base}/analyses/${analysis.id}/report.pdf`, { headers: auth });
    await saveReport(analysis.report_id, Buffer.from(await pdf.arrayBuffer()));
  }
  return decision;
}
```

**Procesar una comprobación terminada: Python**

```python
def handle_result(analysis_id):
    analysis = call("GET", f"/analyses/{analysis_id}")
    decision = route(analysis)
    save_case(analysis["metadata"]["case_id"], {
        "analysis_id": analysis["id"],
        "report_id": analysis["report_id"],
        "report_url": analysis["report_url"],
        "summary": analysis["summary"],
        "model": analysis["model"],
        "decision": decision,
    })
    if analysis["status"] == "succeeded":
        pdf = requests.get(f"{BASE}/analyses/{analysis['id']}/report.pdf", headers=HEADERS, timeout=90)
        pdf.raise_for_status()
        save_report(analysis["report_id"], pdf.content)
    return decision
```

- `report_id` (`FV-XXXX-XXXX-XXXX`) es la clave del registro de auditoría. Guárdelo con el caso, en su base de datos y en cualquier carta de decisión.
- `model` es la versión fechada que produjo el resultado, de modo que más adelante pueda indicar con exactitud qué versión comprobó el documento.
- Conserve el archivo original en su propio almacenamiento. Fileverity elimina el archivo subido cuando empieza el análisis, y una comprobación fallida requiere una subida nueva. Consulte [Conservación de datos y seguridad](https://fileverity.com/es/docs/security.md).

## 5. Gestionar la cola de revisión humana

Cree una tarea por cada caso derivado. Dé al revisor lo que necesita para decidir sin volver a llamar a la API:

- El ID del caso, `level`, `action`, `reason` y los ID de hallazgo de `findings`, con el informe en PDF adjunto. El PDF enumera cada hallazgo con su ubicación y su evidencia.
- El archivo original que almacenó.
- `report_url`, la página de verificación pública. Confirma el registro del informe y la huella del archivo, pero no sustituye la lectura del informe.

Registre al revisor, la decisión y la hora en su propio sistema, junto a `report_id`.

> **Nota.** Una comprobación sin hallazgos no prueba que un documento sea auténtico, y un hallazgo no prueba un fraude. No rechace un caso solo por el resultado de la API. En documentos de ingresos, de identidad o de propiedad, confirme con el emisor a través de un contacto que haya encontrado usted mismo. La [guía de detección de fraude documental](https://fileverity.com/es/guias/fraude-documental-con-ia) explica qué comprobar según el tipo de documento.

## 6. Verificar el informe más tarde

Meses después, un auditor o un cliente puede comprobar el registro sin una cuenta de Fileverity. Abra el `report_url` almacenado. Muestra el tipo de informe, el hallazgo y el siguiente paso registrados, la huella SHA-256 del archivo examinado y la marca de tiempo RFC 3161. Suelte su original almacenado en la página y su navegador calcula su SHA-256 y lo compara con el registro; no se sube nada. Cualquier copia vuelta a guardar, convertida o editada da una huella distinta.

Puede ejecutar la misma comparación en su propio sistema:

**Calcular la huella del original almacenado: cURL**

```bash
sha256sum application.pdf
```

**Calcular la huella del original almacenado: JavaScript**

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

const digest = createHash("sha256").update(await readFile("application.pdf")).digest("hex");
console.log(digest);
```

**Calcular la huella del original almacenado: Python**

```python
import hashlib

print(hashlib.sha256(open("application.pdf", "rb").read()).hexdigest())
```

La verificación no vuelve a analizar el archivo ni certifica que un documento sea auténtico. Registra qué archivo se examinó, qué dijo el informe y cuándo. El informe completo solo existe en el PDF que almacenó. Los campos del informe se describen en [el objeto de informe](https://fileverity.com/es/docs/analyses.md#report).

## Antes de pasar a producción

- Desarrolle con una clave de prueba. Devuelve un ejemplo instantáneo, nivel `L2` con acción `accept`, sin ejecutar el modelo. Los webhooks y la descarga del PDF requieren una clave Live, que puede probar con su inicio gratuito. Pase los demás resúmenes de [Análisis](https://fileverity.com/es/docs/analyses.md#summary-document) por `route` para probar cada rama.
- Gestione `503 service_unavailable`: espere los segundos indicados en `retry-after` y reintente con la misma `Idempotency-Key`. Consulte [Cuando el servicio está ocupado](https://fileverity.com/es/docs/analyses.md#busy).
- Fije un límite de gasto mensual en la clave y añada un webhook `balance.low`, para que la recepción no se detenga con `insufficient_balance`. Consulte [Precios y facturación](https://fileverity.com/es/docs/pricing.md#alerts).
- Llame a la API desde su servidor y guarde las claves en un gestor de secretos, nunca en código del navegador.
- Indique el valor del encabezado `x-request-id` cuando [contacte con soporte](https://fileverity.com/es/contacto).

