المطورون/التكامل

Webhook

رسالة Webhook هي إشعار ترسله CardV إلى خادمك عند انتهاء الطلب. وهي تغنيك عن الاستعلام المتكرر. لا تحتوي أبدًا على الأكواد: بعد وصولها، اقرأ الطلب عبر GET/api/v1/orders/{order_id}.

تضبط روابط Webhook من البوابة (Owner، ثم Integrations → Webhooks)، بشكل منفصل لكل من Live وSandbox. لا توجد API لهذا الإعداد.

ذات صلة: المنتجات والطلبات · الأمان

#الأحداث

الحدثيُرسل عندما تصبح حالة الطلب
order.succeededsucceeded
order.partially_succeededpartially_succeeded
order.failedfailed (وهذا لا يعني أن المبلغ أُعيد بعد)
order.refundedrefunded (عاد المبلغ إلى محفظتك)
  • يُرسل كل حدث مرة واحدة على الأكثر لكل طلب ولكل رابط Webhook (إضافةً إلى إعادة المحاولة).
  • لا توجد أحداث للحالتين accepted وprocessing، ولا لشحن المحفظة.

#محتوى الرسالة

ترسل CardV طلب POST إلى رابط HTTPS الخاص بك:

HTTP
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 في سطر واحد):

JSON
{
  "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 أو عند إعادة تعيين السر.

طريقة عمل التوقيع:

Text
X-CardV-Signature: t=<unix seconds>,v1=<lowercase hex>
v1 = HMAC-SHA256(key = signing secret, message = "<t>" + "." + raw body bytes)

الخطوات:

  1. اقرأ بايتات المحتوى الخام قبل تحليل JSON.
  2. استخرج t وv1 من الترويسة.
  3. ارفض الرسالة إذا كان t يبعد عن ساعتك أكثر من 300 ثانية.
  4. احسب قيمة v1 المتوقعة، وقارنها بدالة مقارنة ثابتة الزمن (constant-time).
  5. بعد ذلك فقط حلّل JSON.

التوقيع يشمل t والمحتوى فقط. لذا خذ نوع الحدث من الحقل event في المحتوى، لا من الترويسة X-CardV-Event.

قيم اختبار (السر وهمي):

Text
secret   whsec_TEST_ONLY_not_a_real_secret_000000000000
t        1790000100
v1       2baac51da272ee8d4b7b1382d48037976c7272f1749c9399a50e758604de8a27

المحتوى الخاص بهذه القيم هو هذا السطر بالضبط (266 بايت، دون فاصل سطر في النهاية):

JSON
{"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)

Python
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 later

Node.js (Express)

Node.js
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 ومعرّف الطلب أو الاستدعاء.