المطورون/التكامل
Webhook
رسالة Webhook هي إشعار ترسله CardV إلى خادمك عند انتهاء الطلب.
وهي تغنيك عن الاستعلام المتكرر. لا تحتوي أبدًا على الأكواد: بعد وصولها، اقرأ الطلب عبر
GET/api/v1/orders/{order_id}.
تضبط روابط Webhook من البوابة (Owner، ثم Integrations → Webhooks)، بشكل منفصل لكل من Live وSandbox. لا توجد API لهذا الإعداد.
ذات صلة: المنتجات والطلبات · الأمان
#الأحداث
| الحدث | يُرسل عندما تصبح حالة الطلب |
|---|---|
order.succeeded | succeeded |
order.partially_succeeded | partially_succeeded |
order.failed | failed (وهذا لا يعني أن المبلغ أُعيد بعد) |
order.refunded | refunded (عاد المبلغ إلى محفظتك) |
- يُرسل كل حدث مرة واحدة على الأكثر لكل طلب ولكل رابط Webhook (إضافةً إلى إعادة المحاولة).
- لا توجد أحداث للحالتين
acceptedوprocessing، ولا لشحن المحفظة.
#محتوى الرسالة
ترسل CardV طلب POST إلى رابط HTTPS الخاص بك:
POST /cardv/webhook HTTP/1.1
Content-Type: application/json
User-Agent: CardV-B2B-Webhook/1.0
X-CardV-Event: order.succeeded
X-CardV-Delivery: 5521
X-CardV-Timestamp: 1790000100
X-CardV-Signature: t=1790000100,v1=2baac51da272ee8d4b7b1382d48037976c7272f1749c9399a50e758604de8a27المحتوى، معروضًا هنا بتنسيق مقروء (ترسله CardV في سطر واحد):
{
"event": "order.succeeded",
"order": {
"id": "O-00000001",
"order_id": "O-00000001",
"external_order_id": "TEST-0001",
"status": "succeeded",
"currency": "USD",
"total_amount": "9.2500",
"items": [
{
"sku_id": "S000001",
"product_name": "Example Card",
"quantity": 1,
"delivery_count": 1
}
]
}
}order.order_idهو رقم طلب CardV، وorder.idيحمل القيمة نفسها.order.external_order_idهو رقم طلبك.order.statusهي الحالة لحظة وقوع الحدث، وقد لا تكون محدّثة.X-CardV-Deliveryهو رقم هذه الرسالة، وتعرضه البوابة في سجل الإرسال.
#التحقق من التوقيع
كل رسالة Webhook موقّعة بسر التوقيع الخاص بها (whsec_...).
تعرض البوابة هذا السر مرة واحدة فقط، عند إنشاء Webhook أو عند إعادة تعيين السر.
طريقة عمل التوقيع:
X-CardV-Signature: t=<unix seconds>,v1=<lowercase hex>
v1 = HMAC-SHA256(key = signing secret, message = "<t>" + "." + raw body bytes)الخطوات:
- اقرأ بايتات المحتوى الخام قبل تحليل JSON.
- استخرج
tوv1من الترويسة. - ارفض الرسالة إذا كان
tيبعد عن ساعتك أكثر من 300 ثانية. - احسب قيمة
v1المتوقعة، وقارنها بدالة مقارنة ثابتة الزمن (constant-time). - بعد ذلك فقط حلّل JSON.
التوقيع يشمل t والمحتوى فقط. لذا خذ نوع الحدث من الحقل event في المحتوى،
لا من الترويسة X-CardV-Event.
قيم اختبار (السر وهمي):
secret whsec_TEST_ONLY_not_a_real_secret_000000000000
t 1790000100
v1 2baac51da272ee8d4b7b1382d48037976c7272f1749c9399a50e758604de8a27المحتوى الخاص بهذه القيم هو هذا السطر بالضبط (266 بايت، دون فاصل سطر في النهاية):
{"event":"order.succeeded","order":{"id":"O-00000001","order_id":"O-00000001","external_order_id":"TEST-0001","status":"succeeded","currency":"USD","total_amount":"9.2500","items":[{"sku_id":"S000001","product_name":"Example Card","quantity":1,"delivery_count":1}]}}Python (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["CARDV_WEBHOOK_SECRET"].encode()
def verify(raw_body: bytes, header: str, tolerance: int = 300) -> bool:
try:
parts = dict(item.split("=", 1) for item in header.split(","))
t, v1 = parts["t"].strip(), parts["v1"].strip()
except (KeyError, ValueError):
return False
if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
signed = t.encode() + b"." + raw_body
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected.encode(), v1.encode()) # constant time
@app.post("/cardv/webhook")
def cardv_webhook():
raw = request.get_data() # raw bytes, before JSON parsing
if not verify(raw, request.headers.get("X-CardV-Signature", "")):
abort(400)
event = request.get_json()
store_event(request.headers["X-CardV-Delivery"], event) # your code
return "", 204 # answer fast; do the work laterNode.js (Express)
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.CARDV_WEBHOOK_SECRET;
function verify(rawBody, header, toleranceSeconds = 300) {
const parts = {};
for (const p of String(header || "").split(",")) {
const i = p.indexOf("=");
parts[p.slice(0, i).trim()] = p.slice(i + 1).trim();
}
const { t, v1 } = parts;
if (!t || !v1 || !/^\d+$/.test(t)) return false;
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(t)) > toleranceSeconds) return false;
const expected = crypto.createHmac("sha256", SECRET)
.update(Buffer.concat([Buffer.from(`${t}.`, "utf8"), rawBody]))
.digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(v1, "utf8");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// express.raw keeps the exact bytes. Do not use express.json() on this route.
const rawJson = express.raw({ type: "application/json" });
app.post("/cardv/webhook", rawJson, async (req, res) => {
if (!verify(req.body, req.get("X-CardV-Signature"))) return res.sendStatus(400);
const event = JSON.parse(req.body.toString("utf8"));
await storeEvent(req.get("X-CardV-Delivery"), event); // your code
res.sendStatus(204);
});كلا المثالين ينجح مع قيم الاختبار.
#إعادة المحاولة
- أجب بأي رمز حالة من فئة 2xx خلال 15 ثانية، بعد أن تحفظ الحدث. وأجّل الأعمال البطيئة (قراءة الطلب، وإرسال الأكواد) إلى ما بعد الرد.
- أي رد آخر يُعدّ فشلًا: رمز حالة مختلف، أو انتهاء المهلة، أو خطأ في الاتصال.
- CardV لا تتبع عمليات إعادة التوجيه، والرد بـ 3xx يُعدّ فشلًا. سجّل الرابط النهائي الدقيق.
- تُعاد محاولة إرسال الرسالة الفاشلة، حتى 6 محاولات إجمالًا:
| المحاولة | التوقيت |
|---|---|
| 1 | فور وقوع الحدث |
| 2 | بعد نحو دقيقة من فشل المحاولة 1 |
| 3 | بعد نحو 5 دقائق من فشل المحاولة 2 |
| 4 | بعد نحو 15 دقيقة من فشل المحاولة 3 |
| 5 | بعد نحو 30 دقيقة من فشل المحاولة 4 |
| 6 | بعد نحو 60 دقيقة من فشل المحاولة 5 |
قد تتأخر هذه التوقيتات حتى دقيقة واحدة. وبعد فشل المحاولة السادسة تتوقف CardV عن الإرسال. يمكنك رؤية كل رسالة وإعادة إرسالها من سجل الإرسال في البوابة.
#الرسائل المكررة
قد يصل الحدث نفسه أكثر من مرة، وقد تصل الأحداث بغير ترتيبها.
- تجاهل أي حدث عالجته من قبل.
طابِق على
order.order_idمعeventمن المحتوى بعد التحقق من توقيعه. الرسالة المعاد إرسالها من البوابة تحمل رقمX-CardV-Deliveryجديدًا، لذا لا يكفي هذا الرقم وحده. - المحتوى صورة للطلب لحظة وقوع الحدث. لذا اقرأ الطلب دائمًا من جديد، وتصرّف بناءً على حالته الحالية.
- قد تضيع بعض رسائل Webhook. لذا شغّل أيضًا مهمة دورية تفحص الطلبات غير المكتملة التي مضت عليها بضع دقائق.
#شروط رابط الاستقبال
- يجب أن يبدأ الرابط بـ
https://وأن يشير إلى عنوان عام على الإنترنت. العناوين الخاصة والمحلية مرفوضة. - يمكنك الاشتراك في الأحداث الأربعة كلها أو اختيار بعضها.
- إعادة تعيين سر التوقيع تسري فورًا، ويتوقف السر القديم عن العمل. وللتبديل دون توقف الخدمة، أضف Webhook ثانيًا بسر جديد، وانشره، ثم عطّل القديم.
- تحتفظ CardV بأول 1,000 حرف من ردك لأغراض تشخيص المشكلات. لا تضع في ردك أسرارًا أو بيانات شخصية.
هل لديك سؤال حول التكامل؟ راسلنا على [email protected] مع ذكر Merchant ID ومعرّف الطلب أو الاستدعاء.