Fileverity API 文档
Webhooks
接收 analysis.completed、analysis.failed 和 balance.low 事件。包括 JavaScript 和 Python 中的 Standard Webhooks 签名校验、重试、自动停用和测试事件。
有事件发生时,Webhook 会向您的服务器发送 HTTPS 请求,因此无需轮询。它们遵循 Standard Webhooks 规范。
在控制台的 Webhooks 中添加端点,选择其 URL 和要接收的事件。签名密钥以 whsec_ 开头,仅在创建端点时显示一次。您可以随时修改端点接收的事件,而不会更改其密钥。
如需替换丢失或已泄露的密钥,请在该端点上选择轮换密钥。新密钥只显示一次,旧密钥会立即失效,因此请立即更新您的校验代码。
- URL 必须使用 HTTPS 和默认端口 443,并指向公网地址。包含凭据或使用其他端口的 URL 会被拒绝。
- 每个工作区最多可添加 10 个端点。
- 测试密钥永远不会发送事件。
事件
| 事件 | 发送时机 |
|---|---|
analysis.completed | 图片或文档分析成功。 |
analysis.failed | 分析失败或被取消,未扣费。已取消的分析带有 status: canceled 和 error: canceled。 |
balance.low | 余额低于您在控制台中设置的提醒阈值。 |
支持的事件类型有 analysis.completed、analysis.failed、balance.low。
请求内容
所有事件的外层结构相同:id、object(固定为 event)、type、created(Unix 时间戳)和 data。id 同时也通过 webhook-id 请求头发送。
analysis.completed 和 analysis.failed
data 与 GET /analyses/{id} 返回的分析对象相同,字段名称和类型也相同,唯一的区别是 result 始终为 null:请通过该接口获取检测结果。status 为 succeeded、failed 或 canceled;error 是包含 code 和 message 的对象,或为 null。时间戳为 Unix 秒数。
{ "id": "evt_Lm8sK3tR6yZ2cXaQ9vB4nP1w", "object": "event", "type": "analysis.completed", "created": 1793700131, "data": { "id": "an_8Hk2pQ4vN9sL1xR7cW3mY6zB", "object": "analysis", "created": 1793700120, "model": "fvimg-2-2026-10-30", "livemode": true, "status": "succeeded", "file": { "name": "claim-photo.jpg", "bytes": 2481920, "format": "jpeg" }, "report_id": "FV-7K2M-9QXD-4T8R", "report_url": "https://fileverity.com/verify/FV-7K2M-9QXD-4T8R", "summary": { "level": "L2", "assessment": "authentic_untouched", "action": "accept", "ai": "very_low", "edit": "none" }, "result": null, "error": null, "progress": null, "usage": { "amount": 199, "currency": "usd", "funding": "balance" }, "metadata": { "order_id": "A-10492" }, "completed_at": 1793700131, "expires_at": 1794304931 }}{ "id": "evt_Zc4vB7nP1sK8tL3wR6yQm2Xa", "object": "event", "type": "analysis.failed", "created": 1793700131, "data": { "id": "an_8Hk2pQ4vN9sL1xR7cW3mY6zB", "object": "analysis", "created": 1793700120, "model": "fvimg-2-2026-10-30", "livemode": true, "status": "failed", "file": { "name": "claim-photo.jpg", "bytes": 2481920, "format": "jpeg" }, "report_id": null, "report_url": null, "summary": null, "result": null, "error": { "code": "analysis_failed", "message": "The analysis could not be completed. You were not charged; retry it, and contact support with the request ID if it fails again." }, "progress": null, "usage": { "amount": 0, "currency": "usd", "funding": null }, "metadata": null, "completed_at": 1793700135, "expires_at": null }}balance.low
balance 和 threshold 以美分为单位。
{ "id": "evt_Lm8sK3tR6yZ2cXaQ9vB4nP1w", "object": "event", "type": "balance.low", "created": 1793700500, "data": { "balance": 640, "threshold": 1000 }}校验签名
每个请求都带有 Standard Webhooks 规范定义的三个请求头:
| 请求头 | 值 |
|---|---|
webhook-id | 事件 ID。重试时保持不变;可用它忽略重复投递。 |
webhook-timestamp | 本次投递尝试的 Unix 时间(秒)。每次重试都会变化。 |
webhook-signature | v1, 后接 Base64 编码的 HMAC-SHA256。一个请求头可以包含多个以空格分隔的签名。 |
校验请求的步骤:
- 在进行任何 JSON 解析之前,先将原始请求体读取为文本。
- 构造待签名内容:
webhook-id、一个英文句点、webhook-timestamp、一个英文句点,再加上原始请求体。 - 取出您的签名密钥,去掉
whsec_前缀,对其余部分进行 Base64 解码,结果即为 HMAC 密钥。 - 对待签名内容计算 HMAC-SHA256,并进行 Base64 编码。
- 使用恒定时间比较,将其与请求头中的每个
v1签名进行比对。 - 拒绝与您的系统时钟相差超过五分钟的时间戳。
import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(secret, headers, body) { const id = headers["webhook-id"]; const timestamp = headers["webhook-timestamp"]; const signatures = headers["webhook-signature"]; if (!id || !timestamp || !signatures) throw new Error("Missing webhook headers"); if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) throw new Error("Timestamp outside tolerance"); const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64"); const expected = createHmac("sha256", key).update(id + "." + timestamp + "." + body).digest(); const valid = 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); }); if (!valid) throw new Error("Invalid signature"); return JSON.parse(body);} app.post("/webhooks/fileverity", express.raw({ type: "application/json" }), (req, res) => { try { const event = verify(process.env.FILEVERITY_WEBHOOK_SECRET, req.headers, req.body.toString("utf8")); handle(event); res.sendStatus(200); } catch { res.sendStatus(400); }});import base64import binasciiimport hashlibimport hmacimport jsonimport osimport time from fastapi import FastAPI, HTTPException, Request app = FastAPI() def verify(secret, headers, body): message_id = headers["webhook-id"] timestamp = headers["webhook-timestamp"] if abs(time.time() - int(timestamp)) > 300: raise ValueError("Timestamp outside tolerance") key = base64.b64decode(secret.removeprefix("whsec_")) signed = (message_id + "." + timestamp + ".").encode() + body expected = hmac.new(key, signed, hashlib.sha256).digest() for entry in headers["webhook-signature"].split(" "): version, _, value = entry.partition(",") try: given = base64.b64decode(value, validate=True) except binascii.Error: continue if version == "v1" and hmac.compare_digest(given, expected): return json.loads(body) raise ValueError("Invalid signature") @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) handle(event) return {"ok": True}请针对实际发送的原始字节进行校验。先解析再重新序列化 JSON 会改变这些字节,导致签名校验失败。
投递与重试
- 请在 10 秒内返回任意
2xx状态码。不会跟随重定向,重定向视为失败。 - 其他任何情况,包括超时或连接错误,都会安排重试,间隔逐步增大:5、10、20、40、80、160、320 和 640 分钟,之后每 12 小时一次。事件发生 3 天后停止重试。重试按计划调度,因此实际重试可能晚于既定间隔,但绝不会提前。
- 投递至少发生一次,且不保证顺序。请使用
webhook-id忽略重复投递;如需最新状态,请获取该分析。 - 控制台会列出最近 50 次投递及其状态和错误,保留 30 天,并支持重新发送某一次投递。打开某次投递,可以查看其请求内容、响应时间以及您的端点所返回响应的前 2 KB,因此请勿在该响应中返回密钥或个人数据。重新发送时沿用原来的事件 ID。
- 请求的 User-Agent 为
Fileverity-Webhooks/1.0。
自动停用
连续失败 3 天的端点会被停用。停用后不再接收新事件,其未送达的事件会标记为失败。在此之前只要有一次投递成功,失败状态即被清除。端点被停用时,工作区所有者和管理员会收到邮件通知。修复问题后,请在控制台中重新启用该端点。
测试事件
在控制台中,对端点使用发送测试,即可立即投递一个示例事件。它是经过签名的 analysis.completed 事件,data.test 为 true,且不含报告,因此您可以在真实事件到达之前检查您的 URL 和签名校验代码。
{ "id": "evt_Qm4vB7nP1sK8tL3wR6yZ2cXa", "object": "event", "type": "analysis.completed", "created": 1793700500, "data": { "id": "an_Pw3mY6zB8Hk2pQ4vN9sL1xR7", "object": "analysis", "created": 1793700120, "model": "fvimg-2-2026-10-30", "livemode": true, "status": "succeeded", "file": { "name": "claim-photo.jpg", "bytes": 2481920, "format": "jpeg" }, "report_id": null, "report_url": null, "summary": null, "result": null, "error": null, "progress": null, "usage": { "amount": 199, "currency": "usd", "funding": "balance" }, "metadata": { "order_id": "A-10492" }, "completed_at": 1793700131, "expires_at": 1794304931, "test": true }}