انتقل إلى المحتوى

توثيق واجهة Fileverity API

أمثلة برمجية

برامج كاملة على Node.js وPython لواجهة Fileverity API: فحص صورة، وفحص ملف PDF كبير، وكشف النص المكتوب بالذكاء الاصطناعي، والتحقق من توقيعات Webhook.

عرض بصيغة Markdownآخر تحديث: 2026-11-04

تضم هذه الصفحة أربعة برامج كاملة لواجهة Fileverity API، يعمل كل منها على Node.js (الإصدار 18 أو أحدث) وعلى Python (الإصدار 3.9 أو أحدث). وهي: فحص صورة وانتظار النتيجة، وفحص ملف PDF كبير، وكشف النص المكتوب بالذكاء الاصطناعي، واستقبال Webhooks مع التحقق من التوقيع. انسخ أحدها، وعيّن FILEVERITY_API_KEY، ثم شغّله.

تستدعي البرامج API عبر HTTPS مباشرة، باستخدام fetch المضمّنة في Node.js وحزمة requests في Python، فلا تحتاج إلا إلى عميل 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
كشف النص المكتوب بالذكاء الاصطناعييرسل ملفًا نصيًا ويطبع الحكم والدرجة وكل فقرة مكتوبة بالذكاء الاصطناعي.POST /text/detections
استقبال Webhooksيتحقق من توقيع كل عملية تسليم، ويتجاهل التكرارات، ويعالج الأحداث.نقطة النهاية لديك، التي تُستدعى بالأحداث analysis.completed وanalysis.failed وbalance.low

قبل التشغيل

  • أنشئ مفتاحًا من نوع اختبار ضمن مفاتيح API في لوحة التحكم. يعيد مفتاح الاختبار نتائج نموذجية ثابتة ولا تُحتسب عليه رسوم أبدًا، فيعمل كل برنامج دون أي تكلفة. ولفحص ملفات حقيقية، أنشئ مفتاحًا من نوع حقيقي. ولا تتغير البرامج.
  • 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 من خادمك، لا من شيفرة المتصفح.
Idempotencyيرسل كل طلب POST الترويسة Idempotency-Key بقيمة UUID جديدة لكل طلب، ويعيد استخدامها في كل إعادة محاولة، فلا يُنفَّذ الفحص ولا يُحتسب سعره مرتين عند إعادة المحاولة. ولكي تكون إعادة تشغيل السكربت بالكامل آمنة، اشتق المفتاح من معرّف السجل لديك بدلًا من ذلك. راجع Idempotency.
الأخطاءتحمل الاستجابة التي ليست 2xx الحقول error.type وerror.code وerror.message وerror.param وerror.request_id. وتتوقف البرامج مع الرسالة code: message (request request_id). اعتمد على code في معالجة الخطأ، واذكر معرّف الطلب عند التواصل مع الدعم. راجع الأخطاء.
إعادة المحاولةتُعاد المحاولة عند تعطّل الشبكة، وعند أي حالة 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. راجع حدود المعدّل والترويسات.
المهلاتتنتهي مهلة برامج Python بعد 90 ثانية لكل طلب، وهي أطول من 60 ثانية، أقصى مدة يستطيع Prefer: wait إبقاء الطلب مفتوحًا خلالها.

فحص صورة وانتظار النتيجة

يرفع البرنامج الصورة بصيغة multipart، وينشئ تحليلًا مع Prefer: wait=60 ليبقى الطلب مفتوحًا حتى تجهز النتيجة، ثم يطبع الملخص وعنوان URL للتقرير. فإذا انتهت مدة الانتظار أولًا، ترد API بالاستجابة 202 مع status: processing، ويكرر البرنامج استدعاء GET /analyses/{id} كل 3 ثوانٍ. راجع الانتظار أو الاستعلام المتكرر أو استخدام Webhooks.

احفظه باسم check-image.mjs أو check_image.py وشغّل node check-image.mjs photo.jpg أو python check_image.py photo.jpg. يقبل multipart صورًا حتى 4 ميغابايت. أما الصورة الأكبر، حتى 10 ميغابايت، فاستخدم الرفع بعنوان موقّع مسبقًا كما في البرنامج التالي، مع نوع محتوى الصورة، مثل 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 ميغابايت على ثلاث خطوات: اطلب عنوان رفع عبر POST /files مع محتوى طلب بصيغة JSON، ثم أرسل البايتات إلى ذلك العنوان باستخدام PUT، ثم انتظر حتى تظهر حالة الملف 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 نموذجي، لذا لا يحفظ البرنامج التقرير إلا مع مفتاح حقيقي.
  • نزّل ملف PDF فور انتهاء الفحص. تُحفظ صور صفحات المستند ومقتطفات نصه مدة 3 ساعات، وسيخلو ملف PDF الذي يُنزَّل بعد ذلك منها. راجع تقرير PDF.
  • الفحص الذي ينتهي بالحالة failed أو canceled لا تُحتسب عليه رسوم. ويتوقف البرنامج مع رسالة الخطأ الخاصة بالتحليل.

كشف النص المكتوب بالذكاء الاصطناعي

يتطلب كشف النص استدعاءً متزامنًا واحدًا. يقرأ البرنامج ملفًا نصيًا ويرسله إلى POST /text/detections ويطبع الحكم والدرجة ونسبة النص المكتوب بالذكاء الاصطناعي وكل فقرة مكتوبة بالذكاء الاصطناعي مع إزاحاتها. يدعم كشف النص المكتوب بالذكاء الاصطناعي اللغات الإنجليزية والفرنسية والألمانية والإسبانية والصينية (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. راجع إزاحات المقاطع.

يعيد كشف النص تقديرًا إحصائيًا، وليس دليلًا على هوية كاتب النص. استخدمه مُدخلًا واحدًا ضمن مراجعة بشرية، ولا تعتمد عليه وحده في اتخاذ قرارات تمس شخصًا.

استقبال Webhooks والتحقق من التوقيعات

يُخبر Webhook خادمك عند انتهاء الفحص، فلا تحتاج إلى الاستعلام المتكرر. وهذا المستقبِل خادم HTTP مبسّط: وحدة http في Node.js، وFlask في Python. يتحقق من توقيع كل عملية تسليم، ويتجاهل أي عملية تسليم عالجها من قبل، ويطبع الحدث. وهو يتبع صفحة Webhooks.

  1. اجعل المنفذ 3000 متاحًا عبر عنوان HTTPS عام، مثلًا بواسطة نفق (tunnel) إلى جهازك. ويجب أن تستخدم عناوين نقاط النهاية بروتوكول 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
  • المحتوى الخام للطلب. يغطي التوقيع البايتات نفسها التي أُرسلت. تقرأ البرامج محتوى الطلب بوصفه بايتات، وتحسب HMAC-SHA256 على webhook-id ثم نقطة ثم webhook-timestamp ثم نقطة ثم تلك البايتات، مع استخدام المفتاح السري بعد فك ترميز base64 مفتاحًا لها، ولا تحلّل JSON إلا بعد التأكد من صحة التوقيع.
  • المقارنة في زمن ثابت. تقارن timingSafeEqual في Node.js وhmac.compare_digest في Python النتيجة بكل توقيع v1 في الترويسة webhook-signature.
  • هامش تفاوت الطابع الزمني. يُرفض أي webhook-timestamp يبعد عن الساعة أكثر من خمس دقائق، أو لا يكون رقمًا.
  • الاستجابة السريعة. تحصل عملية التسليم الصحيحة على 200 بمجرد معالجة الحدث، ويحصل التوقيع الخاطئ على 400. وإذا فشلت المعالجة يرد البرنامج بالرمز 500، وتعيد Fileverity المحاولة لاحقًا بمعرّف الحدث نفسه. راجع التسليم وإعادة المحاولة.
  • التكرارات. يجري التسليم مرة واحدة على الأقل، وتحتفظ إعادة المحاولة بمعرّف الحدث. تتذكر البرامج المعرّفات التي عالجتها وترد بالرمز 200 دون معالجة الحدث مرتين. وفي بيئة الإنتاج، استبدل المجموعة (set) الموجودة في الذاكرة بقيد فريد على معرّف الحدث في قاعدة بياناتك، وأسند العمل إلى قائمة انتظار لتبقى الاستجابة سريعة.
  • بيانات الحدث. يحمل الحدثان analysis.completed وanalysis.failed التحليل دون result؛ استدعِ GET /analyses/{id} عندما تحتاج إلى التقرير الكامل. وbalance وthreshold في balance.low بسنتات الدولار الأمريكي.

الخطوات التالية