Webhooks
قروش يُسلّم الأحداث الصادرة إلى رابط الويبهوك المسجَّل عندك. جسم الطلب هو نفس مظروف الحدث الذي ترسله إلى قروش — اقرأeventType لتعرف أي payload استقبلت. كل عملية تسليم مُوقَّعة بـ HMAC-SHA256 باستخدام نفس السر الموقِّع الذي تستخدمه في الطلبات الصادرة.
تسجيل endpoint المستقبِل
روابط المستقبِل لديك — رابط لكل بيئة تشغّلها (اختبارية وإنتاجية) — تُسجَّل لدى قروش مرة واحدة عند تأسيس الشراكة؛ التاجر لا يُدخل أي رابط. عند التفعيل يختار التاجر أي بيئة من بيئاتك يستهدفها التكامل، وكل تسليم لهذا التاجر يذهب إلى الرابط المطابق. المستقبِل مشترك بين كل التجار لديك: اقرأ الـ header X-TenantId لتعرف لأي تاجر يعود التسليم. يجب أن يكون الرابط HTTPS ويستجيب بـ 2xx بسرعة. تُعاد المحاولة على الإخفاقات وفق سلّم التباعد (أدناه)؛ والتسليم الذي يستنفد السلّم يُوسَم فاشلاً ويُعرَض على لوحة تكاملات التاجر.
عند تفعيل التاجر للتكامل، يُصدر قروش مفاتيح الربط لكنه لا يرسل Webhooks بعد — ينتظر إثباتاً أنك تملك السر الموقِّع. أرسل أولPOST /events موقّع (المزامنة الأولية تقوم بذلك) فيُفعَّل التكامل؛ وأي أحداث نشأت في هذه الأثناء تُحفظ في قائمة الانتظار وتُسلَّم عندها ولا تُفقد. أما GET /connection غير الموقّع فيؤكّد المفتاح لكنه لا يبدأ التسليم.
Headers التسليم
يرافق كل تسليم ثلاثة headers. أما البقية التي تحتاجها —eventId وeventType وcorrelationId — فموجودة داخل جسم الـ JSON.
| Header | الغرض |
|---|---|
X-Signature | HMAC-SHA256 موقّت للجسم الخام. التنسيق: t=<unix>,v1=<hex>[,v1=<hex>...]. راجع المصادقة ← حساب التوقيع. |
X-TenantId | معرّف حساب التاجر على منصتك، كما أُدخل عند التفعيل. استخدمه لتوجيه التسليم إلى الحساب الصحيح، ثم تحقّق من التوقيع بسرّ ذلك الحساب. |
X-Correlation-Id | يربط هذا التسليم بإجراء العمل الذي أنتجه. سجّله للدعم. |
التحقق من التوقيع
تحقّق من كل طلب وارد قبل الوثوق بجسمه. استخدم مقارنة بزمن ثابت لتجنّب هجمات التوقيت.
using System.IO;
using System.Security.Cryptography;
using System.Text;
app.MapPost("/webhooks/quroosh", async (HttpContext ctx) =>
{
// اقرأ البايتات الخام — لا تعتمد على model binding الخاص بـ JSON هنا.
using var buffer = new MemoryStream();
await ctx.Request.Body.CopyToAsync(buffer);
var rawBody = buffer.ToArray();
var header = ctx.Request.Headers["X-Signature"].ToString();
if (!TryParse(header, out var timestamp, out var v1List))
{
ctx.Response.StatusCode = 401;
return;
}
// ارفض الطلبات القديمة أو المستقبلية (±٥ دقائق).
var age = DateTimeOffset.UtcNow - DateTimeOffset.FromUnixTimeSeconds(timestamp);
if (Math.Abs(age.TotalSeconds) > 300)
{
ctx.Response.StatusCode = 401;
return;
}
// احسب HMAC المتوقع على "{timestamp}.{raw_body}".
using var hmac = IncrementalHash.CreateHMAC(
HashAlgorithmName.SHA256,
Encoding.UTF8.GetBytes(signingSecret));
hmac.AppendData(Encoding.UTF8.GetBytes($"{timestamp}."));
hmac.AppendData(rawBody);
var expected = hmac.GetHashAndReset();
// مقارنة بزمن ثابت لكل توقيع مرشح.
var ok = v1List.Any(hex =>
CryptographicOperations.FixedTimeEquals(expected, Convert.FromHexString(hex)));
if (!ok)
{
ctx.Response.StatusCode = 401;
return;
}
// ... deserialize rawBody ومعالجة الحدث ...
ctx.Response.StatusCode = 200;
});
// "t=1752573871,v1=abcd...,v1=1234..." → (timestamp, [hex, hex])
static bool TryParse(string header, out long timestamp, out List<string> v1s)
{
timestamp = 0;
v1s = new List<string>();
var hasT = false;
foreach (var part in header.Split(','))
{
var t = part.AsSpan().Trim();
if (t.StartsWith("t=") && long.TryParse(t[2..], out timestamp)) hasT = true;
else if (t.StartsWith("v1=") && t.Length > 3) v1s.Add(t[3..].ToString());
}
return hasT && v1s.Count > 0;
}إعادة المحاولة
أي 5xx، أو 429، أو انتهاء مهلة، أو فشل شبكة، تُعاد محاولته وفق سلّم تباعد: المحاولة الأولى فورية، ثمدقيقة، ٥ دقائق، ١٥ دقيقة، ساعة، ٦ ساعات. بعد المحاولة السادسة يُوسَم التسليم فاشلاً ويُعرَض على لوحة تكاملات التاجر. أما الـ4xx فيُعامَل قراراً دائماً ولا يُعاد — الاستثناء الوحيد401 يحمل جسمه SIGNATURE_EXPIRED، فيُعاد ليتعافى من انحراف الساعة.
إعادات المحاولة تحمل نفس eventId داخل مظروف الـ JSON. يجب أن يكون handler المستقبل عندك آمن التكرار (Idempotent) — أبسط طريقة: فهرس فريد على (event_id) في جدول الوارد.
كتالوج الأحداث
كل تسليم هو مظروف حدث؛ وأشكال الـ payload موثّقة في صفحة الأحداث. ما يرسله قروش اليوم:
| نوع الحدث | الحالة | متى يُطلَق |
|---|---|---|
LoyaltyRuleChanged | متاح | التاجر غيّر قواعد الكسب (النقاط لكل ريال، حدود الأختام). طبّقه فقط عندما يكون ruleVersion أكبر من الإصدار الذي خزّنته آخر مرة. |
IntegrationStatusChanged | متاح | التاجر فعّل أو أوقف أو فصل التكامل. توقّف عن إرسال الأحداث بعد Disabled أو Disconnected. |
RefundReversalCompleted | مُخطط | يُبلغ بنتيجة عكس الإرجاع. لم يُطلَق بعد. |
RedemptionExpiredOrCancelled | مُخطط | استرداد معلّق انتهت مدته أو أُلغي. لم يُطلَق بعد. |
أشكال الـ payload الكاملة لكلٍّ منها موجودة في صفحةالأحداث وفي مرجع API.