جسر ألفابتللمدفوعات التوثيق
المفاهيم

إشعارات Webhook

أحداث موقّعة عند كل تغيّر في حالة الدفعة، وكيف تتحقق منها، وإعادة الإرسال لمدة 24 ساعة.

في كل مرة تتغير فيها حالة دفعة، يرسل الجسر طلب POST موقّعاً إلى عنوان Webhook الخاص بتطبيقك. اضبط العنوان من لوحة تحكم التطبيق. أما سرّ التوقيع، الذي يبدأ بـ whsec_، فيظهر هناك مرة واحدة ويمكن إعادة توليده. الأحداث التجريبية والفعلية تصل إلى العنوان نفسه، فاقرأ data.mode لتميّز بينها.

الأحداث

الحدث يُرسل عندما تصبح الدفعة
payment.paid paid
payment.failed failed
payment.cancelled cancelled
payment.expired expired
payment.partially_refunded partially_refunded (ويُرسل مجدداً مع كل استرداد جزئي إضافي)
payment.refunded refunded

إنشاء الدفعة لا يرسل أي حدث. وكذلك طلب الاسترداد الفعلي لا يرسل حدثاً، بل الاسترداد المؤكَّد فقط. راجع الاسترداد.

ما الذي نرسله

text
POST /webhooks/bridge HTTP/1.1
Host: shop.example
Content-Type: application/json
User-Agent: AlphabetPaymentBridge-Webhooks/1.0
X-Bridge-Event: payment.paid
X-Bridge-Signature: t=1792017204,v1=17cfa378e474d1c57e8707e60fcbc369329a5bf15981d2ed81472a8d5188cfa2

يحتوي الجسم على معرّف الحدث id، واسمه event، ووقته created_at، وتحت data الدفعة تماماً كما كان سيعيدها GET /api/v1/payments/{id} لحظة التغيير:

json
{
  "id": "evt_0b8f7c52-4a51-4f0e-9a3e-2c7d1e9b6a10",
  "event": "payment.paid",
  "created_at": "2026-10-14T22:35:02+00:00",
  "data": {
    "id": "01a13c8c-8592-70f4-9ef9-d3767e74d54c",
    "status": "paid",
    "mode": "test",
    "amount": 25000,
    "refunded_amount": 0,
    "currency": "IQD",
    "method": "aps_card",
    "order_reference": "ORD-1001",
    "description": "Two coffees",
    "customer": {"name": "Ali", "phone": "+9647700000000", "email": "ali@example.com"},
    "payer": {"name": "Ali Hassan", "phone": null, "email": null, "ip": "203.0.113.10", "user_agent": "GuzzleHttp/7"},
    "locale": "en",
    "return_url": "https://shop.example/return",
    "metadata": {"cart_id": "c-42"},
    "checkout_url": "https://<your-bridge-domain>/checkout/01a13c8c-8592-70f4-9ef9-d3767e74d54c",
    "expires_at": "2026-10-14T23:03:24+00:00",
    "paid_at": "2026-10-14T22:35:02+00:00",
    "created_at": "2026-10-14T22:33:24+00:00",
    "refunds": []
  }
}

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

ترويسة X-Bridge-Signature تبدو هكذا: t=1792017204,v1=17cf…. القيمة t هي وقت التوقيع بثواني Unix، والقيمة v1 هي HMAC-SHA256 بصيغة hex للنص t + "." + raw body باستخدام سرّ التوقيع الخاص بك.

  1. اقرأ جسم الطلب الخام قبل أي تحويل من JSON.
  2. استخرج t وv1 من الترويسة.
  3. احسب HMAC للنص t.body بسرّك وقارنه بـ v1 بمقارنة ثابتة الزمن.
  4. ارفض الطلب إن كان t يبعد أكثر من 5 دقائق عن ساعة خادمك.

بلغة PHP:

php
<?php

function verifyBridgeSignature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
    if (! preg_match('/(?:^|,)t=(\d+)/', $header, $t) || ! preg_match('/(?:^|,)v1=([0-9a-f]{64})/', $header, $v1)) {
        return false;
    }

    if (abs(time() - (int) $t[1]) > $tolerance) {
        return false;
    }

    $expected = hash_hmac('sha256', $t[1].'.'.$rawBody, $secret);

    return hash_equals($expected, $v1[1]);
}

داخل مسار في Laravel:

php
Route::post('/webhooks/bridge', function (Illuminate\Http\Request $request) {
    $valid = verifyBridgeSignature(
        $request->getContent(),
        (string) $request->header('X-Bridge-Signature'),
        config('services.bridge.webhook_secret'),
    );

    abort_unless($valid, 400);

    $event = $request->json()->all();
    // Skip $event['id'] if you have handled it already, then queue the real work.

    return response()->noContent();
});

بلغة Node:

js
const crypto = require('node:crypto');

function verifyBridgeSignature(rawBody, header, secret, tolerance = 300) {
  const t = /(?:^|,)t=(\d+)/.exec(header || '')?.[1];
  const v1 = /(?:^|,)v1=([0-9a-f]{64})/.exec(header || '')?.[1];
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(t)) > tolerance) return false;

  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

مع Express، أبقِ جسم هذا المسار خاماً:

js
app.post('/webhooks/bridge', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyBridgeSignature(req.body, req.get('X-Bridge-Signature'), process.env.BRIDGE_WEBHOOK_SECRET);
  if (!ok) return res.sendStatus(400);

  const event = JSON.parse(req.body);
  // Skip event.id if you have handled it already, then queue the real work.
  res.sendStatus(204);
});

تحقق باستخدام الجسم الخام. إعادة ترميز JSON بعد تحويله تغيّر المسافات وطريقة الترميز، فلن يتطابق التوقيع.

رد بسرعة

رد بأي رمز 2xx خلال 10 ثوانٍ. أي رد آخر، أو حلقة إعادة توجيه، أو عدم الرد، يُعدّ فشلاً في التسليم. ضع العمل البطيء، كإرسال البريد وإصدار الفواتير، في طابور ورد أولاً.

إعادة الإرسال التلقائية

تُرسل المحاولة الأولى فوراً. إن فشلت، يعيد الجسر المحاولة بعد كل مهلة من المهل التالية، في 7 محاولات خلال نحو 24 ساعة:

المحاولة تُرسل
1 فوراً
2 بعد دقيقة من المحاولة 1
3 بعد 5 دقائق من المحاولة 2
4 بعد 30 دقيقة من المحاولة 3
5 بعد ساعتين من المحاولة 4
6 بعد 6 ساعات من المحاولة 5
7 بعد 15 ساعة من المحاولة 6، أي بعد نحو 23.6 ساعة من الأولى

بعد فشل المحاولة السابعة، أو بعد مرور 24 ساعة، يُعلَّم التسليم بأنه فاشل ويتوقف الجسر. كل إعادة ترسل الجسم نفسه بمعرّف الحدث id نفسه، موقّعاً من جديد بقيمة t جديدة. ويُقرأ العنوان والسر عند إرسال كل محاولة، فتصحيحهما في لوحة التحكم ينفع المحاولات التي لم تُرسل بعد.

التكرار والترتيب

  • قد يصل الحدث نفسه أكثر من مرة. احفظ معرّف كل حدث id تعالجه وتجاوز ما سبق أن رأيته.
  • قد تصل الأحداث بغير ترتيبها، كأن يحدث ذلك بعد إعادة إرسال. اعتمد على data.status، أو استدعِ GET /api/v1/payments/{id} عند الشك.

إعادة الإرسال من لوحة التحكم

كل محاولة تسليم مدرجة في لوحة تحكم الإدارة ولوحة تحكم التطبيق، مع رمز الرد وبداية جسمه. اضغط «إعادة الإرسال» لإرسال الحدث نفسه مجدداً بتوقيع جديد، وهذا مفيد مثلاً بعد إصلاح خلل في معالجك.