# Webhooks

> استقبل أحداث analysis.completed وanalysis.failed وbalance.low. التحقق من توقيع Standard Webhooks بلغتي JavaScript وPython، وإعادة المحاولة، والتعطيل التلقائي، وحدث الاختبار.

المصدر: https://fileverity.com/ar/docs/webhooks

ترسل Webhooks طلب HTTPS إلى خادمك عند وقوع حدث ما، فلا تحتاج إلى الاستعلام المتكرر. وهي تتبع مواصفة [Standard Webhooks](https://www.standardwebhooks.com).

أضف نقاط النهاية في لوحة التحكم ضمن **Webhooks**. اختر عنوان URL والأحداث التي تستقبلها. يظهر المفتاح السري للتوقيع، الذي يبدأ بالبادئة `whsec_`، مرة واحدة عند إنشاء نقطة النهاية. ويمكنك تغيير أحداث نقطة النهاية في أي وقت دون تغيير مفتاحها السري.

لاستبدال مفتاح سري ضائع أو مُسرَّب، اختر **جدّد المفتاح السري** على نقطة النهاية. يظهر المفتاح الجديد مرة واحدة ويتوقف القديم عن العمل فورًا، لذا حدّث شيفرة التحقق لديك في الحال.

- يجب أن يستخدم عنوان URL بروتوكول HTTPS على المنفذ الافتراضي 443 وأن يشير إلى عنوان عام. تُرفض العناوين التي تتضمن بيانات اعتماد أو التي تستخدم منفذًا آخر.
- يمكن أن تضم مساحة العمل حتى 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 بالثواني.

**analysis.completed**

```json
{
  "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
  }
}
```

**analysis.failed**

```json
{
  "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` بسنتات الدولار الأمريكي.

**balance.low**

```json
{
  "id": "evt_Lm8sK3tR6yZ2cXaQ9vB4nP1w",
  "object": "event",
  "type": "balance.low",
  "created": 1793700500,
  "data": {
    "balance": 640,
    "threshold": 1000
  }
}
```

## التحقق من التوقيع

يحمل كل طلب ثلاث ترويسات، كما تحددها مواصفة Standard Webhooks:

| الترويسة | القيمة |
| --- | --- |
| `webhook-id` | معرّف الحدث. وهو نفسه في كل إعادة محاولة؛ استخدمه لتجاهل التكرارات. |
| `webhook-timestamp` | وقت Unix بالثواني لمحاولة التسليم هذه. ويتغير مع كل إعادة محاولة. |
| `webhook-signature` | `v1,` متبوعة بقيمة HMAC-SHA256 بترميز base64. يمكن أن تحمل الترويسة عدة توقيعات تفصل بينها مسافات. |

للتحقق من طلب:

1. اقرأ محتوى الطلب الخام بوصفه سلسلة نصية، قبل أي تحليل لـ JSON.
2. كوّن المحتوى الموقَّع: `webhook-id` ثم نقطة ثم `webhook-timestamp` ثم نقطة ثم محتوى الطلب الخام.
3. خذ المفتاح السري للتوقيع، وأزل البادئة `whsec_`، ثم فكّ ترميز base64 للباقي. هذا هو مفتاح HMAC.
4. احسب HMAC-SHA256 للمحتوى الموقَّع ثم رمّزه بصيغة base64.
5. قارنه، في زمن ثابت، بكل توقيع `v1` في الترويسة.
6. ارفض الطوابع الزمنية التي تبعد أكثر من خمس دقائق عن ساعة خادمك.

**التحقق من Webhook: JavaScript**

```javascript
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);
  }
});
```

**التحقق من Webhook: Python**

```python
import base64
import binascii
import hashlib
import hmac
import json
import os
import 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 ثم إعادة تسلسله يغيّرانها ويُبطلان التوقيع.

## التسليم وإعادة المحاولة

- استجب بأي حالة `2xx` خلال 10 ثوانٍ. لا تُتّبع عمليات إعادة التوجيه وتُعدّ فشلًا.
- أي استجابة أخرى، بما فيها انتهاء المهلة أو خطأ الاتصال، تجدول إعادة محاولة بفواصل زمنية متزايدة: 5 و10 و20 و40 و80 و160 و320 و640 دقيقة، ثم كل 12 ساعة. وتتوقف إعادة المحاولة بعد 3 أيام من وقوع الحدث. وتعمل إعادة المحاولة وفق جدول زمني، فقد تأتي بعد موعدها، لكنها لا تأتي قبله أبدًا.
- يجري التسليم مرة واحدة على الأقل، ولا يُضمن الترتيب. استخدم `webhook-id` لتجاهل التكرارات، واجلب التحليل إذا احتجت إلى أحدث حالة له.
- تعرض لوحة التحكم آخر 50 عملية تسليم مع حالتها وأخطائها، وتحتفظ بها 30 يومًا، وتتيح لك إعادة إرسال أي منها. افتح عملية تسليم لترى محتوى الطلب وزمن الاستجابة وأول 2 كيلوبايت من استجابة نقطة النهاية لديك، لذا لا تُرجع أسرارًا أو بيانات شخصية في تلك الاستجابة. وتحتفظ إعادة الإرسال بمعرّف الحدث الأصلي.
- تحمل الطلبات وكيل المستخدم `Fileverity-Webhooks/1.0`.

### التعطيل التلقائي

تُعطَّل نقطة النهاية التي تفشل باستمرار مدة 3 أيام. ولا تستقبل بعد ذلك أي أحداث جديدة، وتُوسَم أحداثها التي لم تُسلَّم بأنها فاشلة. وأي عملية تسليم ناجحة قبل ذلك تُزيل حالة الفشل. يُرسَل بريد إلكتروني إلى مالكي مساحة العمل ومسؤوليها عند تعطيل نقطة نهاية. أعد تفعيل نقطة النهاية في لوحة التحكم بعد إصلاح المشكلة.

## حدث الاختبار

استخدم **أرسل اختبارًا** على نقطة نهاية في لوحة التحكم لتسليم حدث تجريبي فورًا. وهو حدث `analysis.completed` موقَّع تكون فيه قيمة `data.test` هي `true` ولا يتضمن تقريرًا، فيمكنك التحقق من عنوان URL وشيفرة التوقيع قبل وصول الأحداث الحقيقية.

**حدث اختبار**

```json
{
  "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
  }
}
```

ذات صلة: [التحليلات](https://fileverity.com/ar/docs/analyses.md) و[Idempotency](https://fileverity.com/ar/docs/idempotency.md) و[الأخطاء](https://fileverity.com/ar/docs/errors.md).

