跳至正文

Fileverity API 文档

示例代码

Fileverity API 的完整 Node.js 和 Python 程序:检测图片、检测大型 PDF、检测 AI 撰写的文本以及校验 Webhook 签名。

查看 Markdown更新于 2026-11-04

本页提供 Fileverity API 的四个完整程序,每个程序都有 Node.js 18 或更高版本和 Python 3.9 或更高版本两种实现:检测图片并等待结果、检测大型 PDF、检测 AI 撰写的文本,以及接收 Webhook 并校验签名。复制其中一个,设置 FILEVERITY_API_KEY 后运行即可。

这些程序直接调用 HTTPS API,Node.js 使用内置的 fetch,Python 使用 requests 包,因此您只需要一个 HTTP 客户端。其中的调用、字段和状态与文件、分析、文本检测和 Webhooks 中的描述一致。

程序功能调用
检测图片上传图片,等待结果,并输出摘要和报告 URL。POST /files, POST /analyses, GET /analyses/{id}
检测大型 PDF通过预签名 URL 上传文档,轮询直到检测完成,并保存 PDF 报告。POST /files, GET /files/{id}, POST /analyses, GET /analyses/{id}, GET /analyses/{id}/report.pdf
检测 AI 撰写的文本发送文本文件,并输出判定结果、分数以及每个 AI 撰写的段落。POST /text/detections
接收 Webhook校验每次投递的签名,忽略重复投递,并处理事件。您的端点,由 analysis.completed、analysis.failed 和 balance.low 事件调用

运行之前

  • 在控制台的 API 密钥中创建一个 Test 密钥。测试密钥返回固定的示例结果,永不扣费,因此所有程序都可以免费运行。如需检测真实文件,请创建 Live 密钥,程序无需修改。
  • Node.js:将每个程序保存为 .mjs 文件,这样即可使用 import 和顶层 await,然后用 node 运行。无需安装任何依赖。
  • Python:运行 pip install requests。Webhook 接收程序还需要运行 pip install flask。
export FILEVERITY_API_KEY=fv_test_...

程序如何处理密钥、错误和重试

调用 API 的三个程序共用相同的 request 和 api 函数。可以原样复制到您自己的代码中。

方面程序的做法
密钥从 FILEVERITY_API_KEY 读取,绝不写入代码。请从您的服务器调用 API,不要在浏览器代码中调用。
幂等性每个 POST 请求都会发送 Idempotency-Key,每个请求使用一个新的 UUID,并在每次重试时复用它,因此重试不会让同一次检测运行或计费两次。若要让整个脚本重新运行时也安全,请改为根据您自己的记录 ID 生成该值。参见幂等性。
错误非 2xx 的响应会包含 error.type、error.code、error.message、error.param 和 error.request_id。程序会以 code: message (request request_id) 的形式报错并停止。请以 code 为判断依据,并在联系技术支持时提供请求 ID。参见错误。
重试网络故障、5xx 状态码(internal_error、analysis_failed、service_unavailable)、rate_limited(429)和 idempotency_in_progress(409)都会重试,最多尝试 5 次。若响应带有 retry-after 响应头,则等待该时长,否则依次等待 2、4、8 和 16 秒。其他所有 4xx 错误会立即停止。pending_files_limit 同样是 429,但不带 retry-after,因此不会重试。
速率限制当响应头显示 x-ratelimit-remaining-requests: 0 时,程序会先等待 x-ratelimit-reset-requests 秒再发起下一次调用,从而避免触发 429。参见速率限制与 HTTP 头。
超时Python 程序的单次请求超时时间为 90 秒,长于 Prefer: wait 最长可保持请求的 60 秒。

检测图片并等待结果

程序以 multipart 方式上传图片,并通过 Prefer: wait=60 创建分析,使请求保持等待,直到结果就绪,然后输出摘要和报告 URL。如果等待超时而结果仍未就绪,API 会返回 202 和 status: processing,程序随后每 3 秒轮询一次 GET /analyses/{id}。参见等待、轮询或使用 Webhook。

将其保存为 check-image.mjs 或 check_image.py,然后运行 node check-image.mjs photo.jpg 或 python check_image.py photo.jpg。multipart 上传最大支持 4 MB 的图片。更大的图片(最大 10 MB)请使用下一个程序中的预签名上传,并指定该图片的内容类型,例如 image/jpeg。

import { randomUUID } from "node:crypto";import { readFile } from "node:fs/promises";import { basename } from "node:path"; const base = "https://api.fileverity.com/v1";const key = process.env.FILEVERITY_API_KEY;const path = process.argv[2];if (!key || !path) throw new Error("Usage: FILEVERITY_API_KEY=... node check-image.mjs photo.jpg"); const retryable = ["rate_limited", "idempotency_in_progress"];const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); async function request(url, init) {  for (let attempt = 1; ; attempt++) {    let response = null;    let problem;    try {      response = await fetch(url, init);      if (response.ok) {        const remaining = response.headers.get("x-ratelimit-remaining-requests");        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));        if (remaining === "0") await sleep(reset);        return response;      }      problem = (await response.json().catch(() => ({}))).error ?? {};    } catch (error) {      problem = { code: "network_error", message: error.message };    }    const status = response?.status ?? 0;    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);    if (!retry || attempt === 5) {      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;      throw new Error(`${code}: ${message} (request ${id})`);    }    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);  }} async function api(method, route, { headers, body } = {}) {  const auth = { Authorization: `Bearer ${key}` };  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });  return response.json();} const form = new FormData();form.append("file", new Blob([await readFile(path)]), basename(path));const file = await api("POST", "/files", {  headers: { "Idempotency-Key": randomUUID() },  body: form,}); let analysis = await api("POST", "/analyses", {  headers: {    "Content-Type": "application/json",    "Idempotency-Key": randomUUID(),    Prefer: "wait=60",  },  body: JSON.stringify({ model: "fvimg-2", file: file.id }),});while (analysis.status === "processing") {  await sleep(3);  analysis = await api("GET", `/analyses/${analysis.id}`);} if (analysis.status !== "succeeded") {  throw new Error(`The check ${analysis.status}: ${analysis.error?.message}`);}const { level, action, assessment, ai, edit } = analysis.summary;console.log(`${analysis.id} ${analysis.status}`);console.log(`level ${level}, action ${action}, assessment ${assessment}`);console.log(`AI generation: ${ai}, editing: ${edit}`);console.log(`Report: ${analysis.report_url}`);
an_8Hk2pQ4vN9sL1xR7cW3mY6zB succeededlevel L2, action accept, assessment authentic_untouchedAI generation: very_low, editing: noneReport: https://fileverity.com/verify/FV-7K2M-9QXD-4T8R

summary 是图片检测稳定且机器可读的判定结果,包含 level、action、assessment、ai 和 edit,详见图片摘要。report_url 是该报告的公开核验页。使用测试密钥时结果是固定的示例,其报告 URL 没有对应的页面。

检测大型 PDF

50 MB 以内的文档分三步上传:用 POST /files 和 JSON 请求体申请上传 URL,用 PUT 将文件内容发送到该 URL,并等待文件状态变为 uploaded。随后程序创建分析并每 5 秒轮询一次,因为文档可能需要排队,检测也可能需要一段时间。程序会输出进度、摘要和报告 URL,并把 PDF 报告保存在脚本所在目录。

将其保存为 check-pdf.mjs 或 check_pdf.py,然后运行 node check-pdf.mjs contract.pdf 或 python check_pdf.py contract.pdf。如需检测其他类型的文档,请将 content_type 改为支持的内容类型之一。如果文档已加密,请在分析请求中添加 options: { password }。

import { randomUUID } from "node:crypto";import { readFile, writeFile } from "node:fs/promises";import { basename } from "node:path"; const base = "https://api.fileverity.com/v1";const key = process.env.FILEVERITY_API_KEY;const path = process.argv[2];if (!key || !path) throw new Error("Usage: FILEVERITY_API_KEY=... node check-pdf.mjs contract.pdf"); const retryable = ["rate_limited", "idempotency_in_progress"];const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); async function request(url, init) {  for (let attempt = 1; ; attempt++) {    let response = null;    let problem;    try {      response = await fetch(url, init);      if (response.ok) {        const remaining = response.headers.get("x-ratelimit-remaining-requests");        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));        if (remaining === "0") await sleep(reset);        return response;      }      problem = (await response.json().catch(() => ({}))).error ?? {};    } catch (error) {      problem = { code: "network_error", message: error.message };    }    const status = response?.status ?? 0;    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);    if (!retry || attempt === 5) {      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;      throw new Error(`${code}: ${message} (request ${id})`);    }    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);  }} async function api(method, route, { headers, body } = {}) {  const auth = { Authorization: `Bearer ${key}` };  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });  return response.json();} const bytes = await readFile(path);const file = await api("POST", "/files", {  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },  body: JSON.stringify({    filename: basename(path),    content_type: "application/pdf",    bytes: bytes.length,  }),}); if (file.status === "pending") {  const { url, method, headers } = file.upload;  await request(url, { method, headers, body: bytes });  let ready = false;  for (let attempt = 0; attempt < 15 && !ready; attempt++) {    ready = (await api("GET", `/files/${file.id}`)).status === "uploaded";    if (!ready) await sleep(2);  }  if (!ready) throw new Error("The upload was not confirmed.");}console.log(`${file.id} uploaded`); let analysis = await api("POST", "/analyses", {  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },  body: JSON.stringify({ model: "fvdoc-3", file: file.id }),});const deadline = Date.now() + 15 * 60 * 1000;while (analysis.status === "processing") {  if (Date.now() > deadline) {    throw new Error(`Still processing. Fetch it later with GET /analyses/${analysis.id}`);  }  console.log(`${analysis.id} processing: ${analysis.progress?.stage ?? "starting"}`);  await sleep(5);  analysis = await api("GET", `/analyses/${analysis.id}`);} if (analysis.status !== "succeeded") {  throw new Error(`The check ${analysis.status}: ${analysis.error?.message}`);}const { level, action, reason, pages, images, text, findings } = analysis.summary;console.log(`${analysis.id} ${analysis.status}`);console.log(`level ${level}, action ${action}, reason ${reason ?? "none"}`);console.log(`${pages} pages, ${images} photos examined, text: ${text ?? "not evaluated"}`);console.log(`findings: ${findings.join(", ") || "none"}`);console.log(`Report: ${analysis.report_url}`); if (analysis.livemode) {  const pdf = await request(`${base}/analyses/${analysis.id}/report.pdf`, {    headers: { Authorization: `Bearer ${key}` },  });  const name = `Fileverity-${analysis.report_id}.pdf`;  await writeFile(name, Buffer.from(await pdf.arrayBuffer()));  console.log(`Saved ${name}`);}
file_Pw3mY6zB8Hk2pQ4vN9sL1xR7 uploadedan_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: queuedan_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: analyzingan_Zc4vB7nP1sK8tL3wR6yQm2Xa processing: reportingan_Zc4vB7nP1sK8tL3wR6yQm2Xa succeededlevel L2, action accept, reason clean12 pages, 3 photos examined, text: humanfindings: noneReport: https://fileverity.com/verify/FV-3PXH-6WQ2-8NDCSaved Fileverity-FV-3PXH-6WQ2-8NDC.pdf
  • PUT 请求直接发送到存储服务,不需要 Authorization 请求头,upload.headers 中的请求头按返回的内容原样发送。参见预签名上传。
  • 使用测试密钥时,文件直接返回 uploaded 状态,无需上传任何内容,因此程序会跳过上传。分析会立即返回固定的示例结果,并且没有示例 PDF,所以程序只在使用 Live 密钥时保存报告。
  • 检测完成后,请立即下载 PDF。文档的页面图片和文本摘录只保留 3 小时,之后下载的 PDF 将不再包含这些内容。参见 PDF 报告。
  • 以 failed 或 canceled 结束的检测不会扣费。程序会以该分析的错误信息报错并停止。

检测 AI 撰写的文本

文本检测只需一次同步调用。程序读取文本文件,将其发送到 POST /text/detections,然后输出判定结果、分数、AI 撰写文本的占比,以及每个 AI 撰写的段落及其偏移量。AI 撰写文本检测支持英语、法语、德语、西班牙语和中文(en、fr、de、es、zh)。语言会自动识别;也可以将语言代码作为第二个参数传入来指定。文本不得少于 100 个词,且不得超过 100,000 个字符。

将其保存为 detect-text.mjs 或 detect_text.py,然后运行 node detect-text.mjs essay.txt 或 python detect_text.py essay.txt fr。快速入门中提供了生成示例 essay.txt 的命令。使用测试密钥时,响应是针对人类撰写文本的固定结果,因此程序不会输出任何段落。

import { randomUUID } from "node:crypto";import { readFile } from "node:fs/promises"; const base = "https://api.fileverity.com/v1";const key = process.env.FILEVERITY_API_KEY;const [, , path, language] = process.argv;if (!key || !path) {  throw new Error("Usage: FILEVERITY_API_KEY=... node detect-text.mjs essay.txt [en|fr|de|es|zh]");} const retryable = ["rate_limited", "idempotency_in_progress"];const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000)); async function request(url, init) {  for (let attempt = 1; ; attempt++) {    let response = null;    let problem;    try {      response = await fetch(url, init);      if (response.ok) {        const remaining = response.headers.get("x-ratelimit-remaining-requests");        const reset = Number(response.headers.get("x-ratelimit-reset-requests"));        if (remaining === "0") await sleep(reset);        return response;      }      problem = (await response.json().catch(() => ({}))).error ?? {};    } catch (error) {      problem = { code: "network_error", message: error.message };    }    const status = response?.status ?? 0;    const retry = status === 0 || status >= 500 || retryable.includes(problem.code);    if (!retry || attempt === 5) {      const { code = status, message = "Request failed", request_id: id = "unknown" } = problem;      throw new Error(`${code}: ${message} (request ${id})`);    }    await sleep(Number(response?.headers.get("retry-after")) || 2 ** attempt);  }} async function api(method, route, { headers, body } = {}) {  const auth = { Authorization: `Bearer ${key}` };  const response = await request(base + route, { method, headers: { ...auth, ...headers }, body });  return response.json();} const text = await readFile(path, "utf8");const detection = await api("POST", "/text/detections", {  headers: { "Content-Type": "application/json", "Idempotency-Key": randomUUID() },  body: JSON.stringify({ text, language }),}); const { verdict, score, ai_fraction: share, usage, segments } = detection;console.log(`verdict: ${verdict} (score ${score.toFixed(2)})`);console.log(`AI-written share: ${Math.round(share * 100)}% of ${usage.characters} characters`);console.log(`language: ${detection.language}, billed units: ${usage.billed_units}`);const points = Array.from(text);for (const { start, end, score: passage } of segments) {  const excerpt = points.slice(start, Math.min(end, start + 80)).join("");  console.log(`${start}-${end} score ${passage.toFixed(2)}: ${excerpt}`);}
verdict: mixed (score 0.97)AI-written share: 50% of 889 characterslanguage: en, billed units: 1267-495 score 0.97: In today's fast-paced logistics landscape, it is essential for businesses to pri590-809 score 0.93: Moving forward, it is crucial to implement robust quality assurance measures and

start 和 end 按 Unicode 码点计数,而不是字节,也不是 UTF-16 码元。Python 字符串本身就按码点索引;Node.js 程序会先用 Array.from 拆分文本。参见段落偏移量。

文本检测返回的是统计估计,不是作者身份的证明。请将其作为人工复核的参考依据之一,不要单独依据它作出影响个人的决定。

接收 Webhook 并校验签名

Webhook 会在检测完成时通知您的服务器,因此无需轮询。这个接收程序是一个最简 HTTP 服务器:Node.js 使用 http 模块,Python 使用 Flask。它会校验每次投递的签名,忽略已经处理过的投递,并输出事件。实现遵循 Webhooks 页面的说明。

  1. 让 3000 端口可通过公网 HTTPS 地址访问,例如通过隧道指向您的本机。端点 URL 必须使用 HTTPS 和默认端口 443,并指向公网地址。
  2. 在控制台中打开 Webhooks,添加一个 URL 为 https://your-host/webhooks/fileverity 的端点,并选择其事件。复制以 whsec_ 开头的签名密钥,它只显示一次。
  3. 将程序保存为 webhook-receiver.mjs 或 webhook_receiver.py,并带上密钥启动:FILEVERITY_WEBHOOK_SECRET=whsec_... node webhook-receiver.mjs 或 FILEVERITY_WEBHOOK_SECRET=whsec_... python webhook_receiver.py。
  4. 在该端点上选择发送测试。测试密钥不会发送事件,而发送测试会投递一个经过签名的示例事件。
import { createHmac, timingSafeEqual } from "node:crypto";import { createServer } from "node:http"; const secret = process.env.FILEVERITY_WEBHOOK_SECRET;if (!secret) throw new Error("Set FILEVERITY_WEBHOOK_SECRET to the endpoint's signing secret.");const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");const port = Number(process.env.PORT) || 3000;const seen = new Set(); function verify(headers, body) {  const id = headers["webhook-id"];  const timestamp = headers["webhook-timestamp"];  const signatures = headers["webhook-signature"];  if (!id || !timestamp || !signatures) return false;  if (!(Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300)) return false;  const expected = createHmac("sha256", key).update(`${id}.${timestamp}.`).update(body).digest();  return signatures.split(" ").some((entry) => {    const [version, value = ""] = entry.split(",");    const given = Buffer.from(value, "base64");    return version === "v1" && given.length === expected.length && timingSafeEqual(given, expected);  });} function handleEvent(event) {  const { type, data } = event;  if (data.test) {    console.log(`Test event ${event.id}`);  } else if (type === "analysis.completed") {    const { level, action } = data.summary;    console.log(`${data.id} succeeded: level ${level}, action ${action}, ${data.report_url}`);  } else if (type === "analysis.failed") {    console.log(`${data.id} ${data.status}: ${data.error.code}`);  } else if (type === "balance.low") {    const money = (cents) => `US$${(cents / 100).toFixed(2)}`;    console.log(`Balance ${money(data.balance)} is below your ${money(data.threshold)} alert`);  }} async function receive(req, res) {  if (req.method !== "POST" || req.url !== "/webhooks/fileverity") return res.writeHead(404).end();  const chunks = [];  let size = 0;  for await (const chunk of req) {    size += chunk.length;    if (size > 1_000_000) return res.writeHead(413).end();    chunks.push(chunk);  }  const body = Buffer.concat(chunks);  if (!verify(req.headers, body)) return res.writeHead(400).end();  const event = JSON.parse(body.toString("utf8"));  try {    if (!seen.has(event.id)) {      handleEvent(event);      seen.add(event.id);    }  } catch (error) {    console.error(`Could not handle ${event.id}`, error);    return res.writeHead(500).end();  }  res.writeHead(200).end();} createServer((req, res) => receive(req, res).catch(() => res.destroy())).listen(port, () => console.log(`Listening on port ${port}`));
Listening on port 3000Test event evt_Qm4vB7nP1sK8tL3wR6yZ2cXaan_8Hk2pQ4vN9sL1xR7cW3mY6zB succeeded: level L2, action accept, https://fileverity.com/verify/FV-7K2M-9QXD-4T8R
  • 原始请求体:签名覆盖实际发送的原始字节。程序以字节形式读取请求体,使用经 Base64 解码的签名密钥作为 HMAC 密钥,对 webhook-id、一个英文句点、webhook-timestamp、一个英文句点和这些字节计算 HMAC-SHA256,并且只在签名有效后才解析 JSON。
  • 恒定时间比较:Node.js 中的 timingSafeEqual 和 Python 中的 hmac.compare_digest 会将结果与 webhook-signature 请求头中的每个 v1 签名进行比较。
  • 时间戳容差:webhook-timestamp 与系统时钟相差超过五分钟,或者不是数字,都会被拒绝。
  • 快速响应:有效的投递在事件处理完成后立即返回 200;签名无效则返回 400。如果处理失败,程序返回 500,Fileverity 稍后会使用相同的事件 ID 重试。参见投递与重试。
  • 重复投递:投递至少发生一次,重试沿用同一个事件 ID。程序会记住已处理的事件 ID,对重复事件直接返回 200,不会重复处理。在生产环境中,请将内存中的集合替换为数据库中事件 ID 的唯一约束,并把实际工作放入队列,以保持响应迅速。
  • 事件数据:analysis.completed 和 analysis.failed 携带不含 result 的分析对象;需要完整报告时,请调用 GET /analyses/{id}。balance.low 中的 balance 和 threshold 以美分为单位。

后续步骤

  • 参照受理流程中的文档检测,运行包含人工复核的完整受理流程。
  • 在 API 参考中查阅任意字段,或将 OpenAPI 文档导入您自己的工具。
  • 正式上线时,使用 Live 密钥,并设置每月支出上限和 balance.low Webhook。参见价格与计费。
  • 需要帮助?联系技术支持时,请提供 x-request-id 响应头的值。