Fileverity API 文档
受理流程中的文档检测
在受理流程中加入文档检测:上传、接收 Webhook、按风险等级分流、排入人工复核队列、保存报告编号并在之后核验。
本示例把文档检测置于接收外部文档的流程之前,例如贷款申请、保险理赔、供应商入驻或费用报销。每份文档只检测一次,按风险等级分流,其报告编号作为审计记录保存。API 返回风险等级和处置建议,最终由您的策略和复核人员作出决定。
它使用模型 fvdoc-3,并使用文件、分析和 Webhooks 中的调用。使用测试密钥时,上传和检测会立即返回示例结果,但不会发送 Webhook,也没有示例 PDF。如需试用您的接收程序,请在控制台中对 Webhook 端点使用发送测试。
流程
- 用
POST /files上传文件,用POST /analyses发起检测。将您的案件 ID 放入metadata,并发送Idempotency-Key。 - 立即响应您的用户。文档可能需要排队,因此不要让其请求一直挂起;检测完成时由 Webhook 通知您。
- 接收
analysis.completed或analysis.failed,校验签名,并将任务加入队列。 - 在任务中获取该分析,按其摘要分流,并将结果和 PDF 报告与案件一并保存。
- 将需要人工处理的案件送入复核队列,并附上 PDF。
- 之后,用保存的文件核验该报告。
1. 上传并发起检测
最大 4 MB 的文件以 multipart 方式上传。更大的文件(最大 50 MB)请按文件中的说明使用预签名上传。创建分析会返回 202 和 status: processing(使用测试密钥时返回 200 和已完成的示例结果),您的 metadata 会随分析一同返回,并出现在该分析的每个 Webhook 中。
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\"}}"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 } }), });}import osimport 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}}, ){ "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}请将 id 与案件一并保存。由于 Idempotency-Key 由案件 ID 派生,超时后再次运行 submit 会返回第一次的响应,而不会重复上传文件,也不会重复计费。Idempotency-Key 保留 24 小时;参见幂等性。
2. 接收结果
使用 Webhooks 中的 verify 函数校验签名,并将其保存为 verify.js 或 verify.py。请快速返回 2xx,并在任务中完成实际工作。enqueue 代表您自己的任务队列。请以事件 ID 为键存储每个任务,这样重复投递就不会创建第二个任务。
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);});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. 按风险等级分流
summary 是文档检测稳定且机器可读的判定结果。level 的取值从 L1 到 L5,action 是建议的下一步。请同时依据两者分流,并将任何无法识别的值交给人工处理。
| 摘要 | 含义 | 分流示例 |
|---|---|---|
L1 或 L2,accept | 未发现篡改迹象。L1 表示来源已验证。 | 自动继续处理。保存报告编号。 |
L1 或 L2,note | 没有篡改迹象,但报告中标出了 AI 撰写的文本或 AI 标识。 | 继续处理;如果您的策略限制 AI 撰写的内容,请标记该案件。 |
L3, review | 需要人工复核。 | 常规复核队列。 |
L4, review | 高风险。 | 优先复核队列。 |
L5, reject | 报告中已确认存在问题。 | 优先复核队列。拒绝任何案件之前,须由人工确认。 |
status 为 failed 或 canceled | 检测未完成,未扣费。 | 重新上传已保存的原始文件,或将案件交给人工处理。 |
这种对应关系体现的是您自己的策略。修改该函数,即可把 L3 放入优先队列,或让 note 进入复核。action 的取值集合还包括 resubmit,函数将其视为索取文件另一份副本的请求。摘要中的每个字段都在分析中有说明。
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";}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. 保存报告编号和 PDF
该任务获取分析、对其分流,并保存之后需要用到的内容。它复用前面步骤中的 call、route 和凭据,saveCase 和 saveReport 代表您自己的存储。请在同一个任务中下载 PDF:分析结果保留 7 天,文档的页面图片和文本摘录保留 3 小时,此后 Fileverity 只保留核验记录,不保留完整报告。
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;}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 decisionreport_id(FV-XXXX-XXXX-XXXX)是查找审计记录的依据。请将其与案件一并保存,存入您的数据库,并写在所有决定通知书上。model是产生该结果的带日期快照,因此您之后可以准确说明是哪个版本检测了该文档。- 请把原始文件保存在您自己的存储中。分析开始时,Fileverity 会删除已上传的文件,检测失败后需要重新上传。参见数据保留与安全。
5. 运行人工复核队列
为每个已分流的案件创建一个任务。向复核人员提供无需再次调用 API 即可作出决定所需的内容:
- 案件 ID、
level、action、reason以及findings中的发现 ID,并附上 PDF 报告。PDF 会列出每项发现及其位置和证据。 - 您保存的原始文件。
report_url,即公开核验页。它确认报告记录和文件指纹,但不能代替阅读报告本身。
在您自己的系统中,将复核人员、决定和时间记录在 report_id 旁边。
检测未发现问题,并不能证明文档是真实的;检测发现了问题,也不能证明存在欺诈。请勿仅凭 API 结果拒绝案件。对于收入、身份或所有权类文档,请通过您自行查得的联系方式向出具方核实。文档伪造检测指南介绍了按文档类型应检查哪些内容。
6. 稍后核验报告
数月之后,审计人员或客户无需 Fileverity 账号即可核对该记录。打开保存的 report_url,页面会显示报告类型、记录的发现和下一步操作、被检测文件的 SHA-256 指纹,以及 RFC 3161 时间戳。把您保存的原始文件拖到页面上,浏览器会计算其 SHA-256 并与记录比对,不会上传任何内容。任何重新保存、转换或编辑过的副本都会得出不同的指纹。
您也可以在自己的系统中执行同样的比对:
sha256sum application.pdfimport { createHash } from "node:crypto";import { readFile } from "node:fs/promises"; const digest = createHash("sha256").update(await readFile("application.pdf")).digest("hex");console.log(digest);import hashlib print(hashlib.sha256(open("application.pdf", "rb").read()).hexdigest())核验不会再次分析文件,也不证明文档是真实的。它只记录检测了哪份文件、报告给出的结论是什么,以及检测的时间。完整报告只存在于您保存的 PDF 中。报告的各字段在报告对象中有说明。
上线之前
- 请先使用测试密钥开发。它会立即返回示例结果:风险等级
L2,处置建议accept,不会运行模型。Webhook 和 PDF 下载需要 Live 密钥,您可以用免费额度来试用。请把分析中的其他摘要传入route,以测试每个分支。 - 处理
503 service_unavailable:等待retry-after秒后,使用相同的Idempotency-Key重试。参见服务繁忙时。 - 为密钥设置每月支出上限,并添加
balance.lowWebhook,以免受理因insufficient_balance而中断。参见价格与计费。 - 请从您的服务器调用 API,并将密钥保存在密钥管理服务中,切勿放在浏览器代码里。
- 联系技术支持时,请提供
x-request-id响应头的值。