إشعارات 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 |
إنشاء الدفعة لا يرسل أي حدث. وكذلك طلب الاسترداد الفعلي لا يرسل حدثاً، بل الاسترداد المؤكَّد فقط. راجع الاسترداد.
ما الذي نرسله
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} لحظة التغيير:
{
"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 باستخدام سرّ التوقيع الخاص
بك.
- اقرأ جسم الطلب الخام قبل أي تحويل من JSON.
- استخرج
tوv1من الترويسة. - احسب HMAC للنص
t.bodyبسرّك وقارنه بـv1بمقارنة ثابتة الزمن. - ارفض الطلب إن كان
tيبعد أكثر من 5 دقائق عن ساعة خادمك.
بلغة 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:
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:
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، أبقِ جسم هذا المسار خاماً:
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}عند الشك.
إعادة الإرسال من لوحة التحكم
كل محاولة تسليم مدرجة في لوحة تحكم الإدارة ولوحة تحكم التطبيق، مع رمز الرد وبداية جسمه. اضغط «إعادة الإرسال» لإرسال الحدث نفسه مجدداً بتوقيع جديد، وهذا مفيد مثلاً بعد إصلاح خلل في معالجك.