توثيق واجهة Fileverity API
فحص المستندات في سير عمل الاستقبال
أضف فحص المستندات إلى مسار الاستقبال: الرفع، واستقبال Webhook، والتوجيه بحسب مستوى المخاطر، وإدراج الحالات في قائمة المراجعة البشرية، وتخزين رقم التقرير، والتحقق منه لاحقًا.
يضع هذا الدليل التطبيقي فحص المستندات أمام أي عملية تستقبل مستندات من خارج مؤسستك: طلبات القروض أو مطالبات التأمين أو تسجيل الموردين الجدد أو تقارير المصروفات. يُفحص كل مستند مرة واحدة، ويُوجَّه بحسب مستوى مخاطره، ويُحتفظ برقم تقريره بوصفه سجل التدقيق. تعيد API مستوى مخاطر وإجراءً موصى به، أما القرار فتتخذه سياستك ومراجعوك.
يستخدم النموذج fvdoc-3 مع الاستدعاءات الواردة في الملفات والتحليلات وWebhooks. ومع مفتاح الاختبار يعيد الرفع والفحص نتيجة نموذجية فورية، لكن لا يُرسَل أي Webhook ولا يوجد ملف PDF نموذجي. استخدم أرسل اختبارًا على نقطة نهاية Webhook في لوحة التحكم لتجربة المستقبِل لديك.
المسار
- ارفع الملف عبر
POST /filesوابدأ الفحص عبرPOST /analyses. ضع معرّف القضية لديك فيmetadataوأرسلIdempotency-Key. - أجب مستخدمك فورًا. قد تدخل المستندات في قائمة انتظار، فلا تُبقِ طلبه مفتوحًا؛ سيُخبرك Webhook عند انتهاء الفحص.
- استقبل
analysis.completedأوanalysis.failed، وتحقق من التوقيع، وأدرج مهمة في قائمة الانتظار. - في المهمة، اجلب التحليل، ووجّهه بحسب ملخصه، واحفظ النتيجة وتقرير PDF مع القضية.
- أرسل القضايا التي تحتاج إلى شخص إلى قائمة المراجعة، مع إرفاق ملف PDF.
- لاحقًا، تحقق من التقرير بمقارنته بالملف المخزَّن.
1. الرفع وبدء الفحص
تُرفع الملفات حتى 4 ميغابايت بصيغة multipart. أما الملفات الأكبر، حتى 50 ميغابايت، فاستخدم الرفع بعنوان موقّع مسبقًا كما هو موصوف في الملفات. يعيد إنشاء التحليل الاستجابة 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 مع القضية. ولأن المفاتيح مشتقة من معرّف القضية، فإن تشغيل submit مرة أخرى بعد انتهاء المهلة يعيد الاستجابات الأولى بدلًا من رفع ملف ثانٍ أو احتساب فحص ثانٍ. تُحفظ المفاتيح 24 ساعة؛ راجع Idempotency.
2. استقبال النتيجة
تحقق من التوقيع بالدالة verify من Webhooks، المحفوظة باسم verify.js أو verify.py. ردّ بالحالة 2xx بسرعة، وأنجز العمل في مهمة. وتمثّل enqueue قائمة المهام الخاصة بك. خزّن كل مهمة تحت معرّف الحدث، حتى لا تؤدي عملية تسليم مكررة إلى إنشاء مهمة ثانية.
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 | لا توجد علامات تلاعب، لكن التقرير يشير إلى نص مكتوب بالذكاء الاصطناعي أو إلى وسم ذكاء اصطناعي. | تابع، وعلّم القضية إذا كانت سياستك تقيّد المحتوى المكتوب بالذكاء الاصطناعي. |
L3, review | تلزم مراجعة يدوية. | قائمة المراجعة العادية. |
L4, review | مخاطر عالية. | قائمة المراجعة ذات الأولوية. |
L5, reject | ثبتت المشكلات في التقرير. | قائمة المراجعة ذات الأولوية. يؤكد شخص ذلك قبل رفض أي قضية. |
قيمة status هي failed أو canceled | لم ينتهِ الفحص ولم تُحتسب عليك رسوم. | ارفع الأصل المخزَّن مرة أخرى، أو أرسل القضية إلى شخص. |
تحدّد سياستك هذا التوجيه. انقل L3 إلى قائمة الأولوية، أو اجعل note يذهب إلى المراجعة، بتعديل الدالة. وتضم مجموعة الإجراءات أيضًا 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 مرة أخرى:
- معرّف القضية و
levelوactionوreasonومعرّفات المؤشرات فيfindings، مع إرفاق تقرير 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دون تشغيل النموذج. أما Webhooks وتنزيل ملف PDF فيتطلبان مفتاحًا حقيقيًا، ويمكنك تجربته برصيد البدء المجاني. ومرّر الملخصات الأخرى من التحليلات عبرrouteلاختبار كل فرع. - عالج
503 service_unavailable: انتظر عدد الثواني الوارد فيretry-afterثم أعد المحاولة بقيمةIdempotency-Keyنفسها. راجع عندما تكون السعة المتاحة مستخدمة بالكامل. - حدّد حد إنفاق شهريًا على المفتاح، وأضف Webhook للحدث
balance.low، حتى لا يتوقف الاستقبال بسبب الخطأinsufficient_balance. راجع الأسعار والفوترة. - استدعِ API من خادمك واحتفظ بالمفاتيح في خدمة لإدارة الأسرار، ولا تضعها أبدًا في شيفرة المتصفح.
- اذكر قيمة الترويسة
x-request-idعند التواصل مع الدعم.