التوثيق
الدمجأنماط الدمج

الدفع المستضاف

دمج من ناحية السيرفر بس. أنشئ جلسة دفع، حوّل العميل لصفحة XPay المستضافة، وأكّد بـ webhook.

الدفع المستضاف هو أبسط دمج فيه أي كود. السيرفر بتاعك بينشئ جلسة دفع، ويحوّل العميل لصفحة الدفع المستضافة بتاعة XPay، والـ webhook بتاعك بيستلم checkout.session.completed لما الدفعة تنزل. ومش بتكتب أي كود واجهة.

اختار النمط ده لما تكون عايز دمج من ناحية الـ backend بس، ومش هتمانع إن العميل يتحوّل بعيد عن موقعك للتلت ثواني اللي بياخدها الدفع.

الدليل ده بيعرض الحد الأدنى من المحتوى المطلوب علشان تبدأ جلسة بنمط التحويل. لمجموعة الحقول الكاملة اللي تقدر تظبطها على POST /checkout/sessions (البنود، وتحصيل بيانات العميل، والعلامة التجارية، وقيود وسائل الدفع، والرسوم، والبيانات الوصفية، إلخ)، شوف مرجع جلسة الدفع.

إزاي بيشتغل

  1. السيرفر بتاعك بيستدعي POST /checkout/sessions بالبنود ومكان توديه العميل بعد الدفع. XPay بيرجّع كائن جلسة فيه url.
  2. السيرفر بتاعك بيحوّل متصفّح العميل للـ url ده.
  3. العميل بيوصل https://checkout.xpay.app/c/cs_test_...، ويدخّل بطاقته، ويكمّل الـ 3D Secure لو لازم، والدفعة بتشتغل.
  4. XPay بيحوّل العميل تاني للـ URL اللي ظبطته في afterCompletion.redirect.url.
  5. XPay بيبعت webhook بـ checkout.session.completed لنقطة النهاية بتاعتك علشان السيرفر يأكّد وينفّذ الطلب.

تجربة العميل عبارة عن تحويل من 5 لـ 15 ثانية لصفحة XPay المستضافة ورجوع. السيرفر بتاعك هو الحاجة الوحيدة اللي بتحتفظ بالحالة عبر العملية كلها.

نفّذه

1. خُد مفتاح API للاختبار

افتح لوحة التحكم، وروح المطورين ← مفاتيح API، وانسخ مفتاح سرّي بيبدأ بـ sk_test_. خلّيه على السيرفر بتاعك. عمرك ما تحطّ مفتاح سرّي في كود الواجهة ولا تعمله commit في repo.

هتستخدم نفس مسار الكود بـ sk_live_* بمجرد ما حسابك يتعتمد للدفعات الفعلية.

2. أنشئ جلسة دفع على السيرفر بتاعك

الحقل الوحيد اللي لازم تبعته هو afterCompletion، اللي بيقول لـ XPay يحوّل العميل لفين بعد الدفع. عمليًا هتبعت كمان lineItems علشان XPay يعرف بيتم خصم إيه.

curl -X POST https://api.xpay.app/checkout/sessions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "afterCompletion": {
      "type": "redirect",
      "redirect": { "url": "https://yourshop.example/order/{CHECKOUT_SESSION_ID}" }
    },
    "lineItems": [
      {
        "priceData": {
          "currency": "EGP",
          "unitAmount": 149900,
          "productData": { "name": "Test product" }
        },
        "quantity": 1
      }
    ]
  }'
const res = await fetch("https://api.xpay.app/checkout/sessions", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    afterCompletion: {
      type: "redirect",
      redirect: { url: "https://yourshop.example/order/{CHECKOUT_SESSION_ID}" },
    },
    lineItems: [
      {
        priceData: {
          currency: "EGP",
          unitAmount: 149900, // 1,499.00 EGP, in minor units
          productData: { name: "Test product" },
        },
        quantity: 1,
      },
    ],
  }),
});

const session = await res.json();
import os, requests

res = requests.post(
    "https://api.xpay.app/checkout/sessions",
    headers={
        "Authorization": f"Bearer {os.environ['XPAY_SECRET_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "afterCompletion": {
            "type": "redirect",
            "redirect": {"url": "https://yourshop.example/order/{CHECKOUT_SESSION_ID}"},
        },
        "lineItems": [
            {
                "priceData": {
                    "currency": "EGP",
                    "unitAmount": 149900,  # 1,499.00 EGP, in minor units
                    "productData": {"name": "Test product"},
                },
                "quantity": 1,
            }
        ],
    },
)
session = res.json()

الرد فيه الحقول اللي هتحتاجها بعد كده:

{
  "id": "cs_test_AbC123...",
  "object": "checkout.session",
  "status": "open",
  "url": "https://checkout.xpay.app/c/cs_test_AbC123...",
  "amountTotal": 149900,
  "currency": "EGP",
  "afterCompletion": { "type": "redirect", "redirect": { "url": "..." } }
}

كام ملاحظة:

  • unitAmount بالوحدات الصغرى. 149900 معناها 1,499.00 EGP. ابعت العدد الصحيح الكامل دايمًا، مش رقم عشري.
  • productData.name مطلوب لما تستخدم priceData علشان تنشئ PriceAPI فوري. لو سبق وأنشأت منتجات في لوحة التحكم، تقدر تبعت price: "price_..." بدالها وتتخطى priceData. المرجع الكامل: البنود والتسعير.
  • {CHECKOUT_SESSION_ID} في redirect.url بيتستبدل من ناحية السيرفر بـ id بتاع الجلسة، فصفحة الرجوع بتاعتك تقدر تقرا معرّف الجلسة من المسار من غير ما تمرّره أنت بالحالة.
  • الجلسة بتنتهي بعد 24 ساعة لو العميل ما دفعش أبدًا. تقدر كمان تنهيها يدويًا بـ POST /checkout/sessions/:id/expire.
  • وضع الاختبار بيتحدّد بمفتاح الـ API، مش بالـ URL. sk_test_* بينتج جلسات cs_test_*؛ وsk_live_* بينتج جلسات cs_live_*. اسم مضيف الـ URL المستضاف واحد في الحالتين.

3. حوّل العميل

ابعت HTTP 302 لحقل url بتاع الرد.

app.post("/start-checkout", async (req, res) => {
  const session = await createCheckoutSession(req.body); // your code from step 2
  res.redirect(303, session.url);
});
from flask import redirect

@app.post("/start-checkout")
def start_checkout():
    session = create_checkout_session(request.json)  # your code from step 2
    return redirect(session["url"], code=303)

استخدم 303 See Other لو بتحوّل من طلب POST، وغير كده 302 Found تمام.

4. تعامل مع رجوع العميل

بعد ما العميل يدفع، XPay بيحوّله لـ afterCompletion.redirect.url من غير أي بارامترات استعلام إضافية. صفحتك على الـ URL ده موجّهة للعميل بحتة: رسالة شكر، أو ملخص طلب، أو أي حاجة عايزها.

ما تعتمدش على التحويل وحده لتأكيد الدفع. العميل ممكن يقفل التاب قبل التحويل أو يضرب الـ URL بالغلط. مصدر الحقيقة الوحيد لنجاح الدفع هو webhook checkout.session.completed اللي السيرفر بتاعك بيستلمه.

لو عايز كمان تعرض الطلب الفعلي على صفحة الرجوع، تقدر تقراه تاني بـ GET /checkout/sessions/:id باستخدام معرّف الجلسة اللي خزّنته قبل التحويل.

أكّد بـ webhook

XPay بيبعت checkout.session.completed بمجرد ما الدفعة تنجح. الـ data.object جوّه الحدث هو نفس جلسة الدفع اللي هتجيبها من POST /checkout/sessions أو GET /checkout/sessions/:id، فكود التنفيذ بتاعك ممكن يبقى دالة واحدة بتقرا جلسة دفع.

لكل حقل في الجلسة، شوف Checkout SessionAPI. الحد الأدنى اللي معالِجك محتاج يعمله:

  1. تحقّق من ترويسة XPay-Signature.
  2. فُكّ محتوى الـ JSON.
  3. لو نوع الحدث checkout.session.completed وحالة الجلسة status هي complete، نفّذ الطلب.

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

XPay بيوقّع كل webhook بـ HMAC-SHA256 باستخدام السرّ بتاع التوقيع اللي خدته لما أنشأت نقطة نهاية الـ webhook (بيبدأ بـ whsec_). الترويسة شكلها كده:

XPay-Signature: t=1730000000,v1=a1b2c3d4...

الحمولة الموقَّعة هي الـ timestamp، وبعده . حرفية، والمحتوى الخام للـ JSON. علشان تتحقّق:

import crypto from "node:crypto";

function verifyWebhook(rawBody: string, header: string, secret: string) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("="))); // { t: "1730000000", v1: "a1b2c3..." }

  const timestamp = parts.t;
  const signature = parts.v1;

  // Reject events older than 5 minutes (replay protection)
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    throw new Error("Webhook timestamp out of tolerance");
  }

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

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
    throw new Error("Bad webhook signature");
  }
}
import hmac, hashlib, time

def verify_webhook(raw_body: bytes, header: str, secret: str):
    parts = dict(kv.split("=") for kv in header.split(","))
    timestamp = parts["t"]
    signature = parts["v1"]

    # Reject events older than 5 minutes (replay protection)
    if abs(time.time() - int(timestamp)) > 300:
        raise ValueError("Webhook timestamp out of tolerance")

    signed = f"{timestamp}.".encode() + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(signature, expected):
        raise ValueError("Bad webhook signature")

اقرا محتوى الطلب كنص خام (أو bytes)، مش كـ JSON مفكوك. إعادة تسلسل الـ JSON مش هتنتج نفس الـ bytes اللي XPay وقّعها.

اقرا الحمولة

جوّه checkout.session.completed، الـ data.object هو Checkout SessionAPI كاملة. الحقول اللي بتهمّك عادةً:

الحقليعني إيه
data.object.idمعرّف جلسة الدفع (cs_test_... / cs_live_...). استخدمه كمفتاح عدم تكرار للتنفيذ.
data.object.statuscomplete عند نجاح الدفعة.
data.object.amountTotalالإجمالي المخصوم، بالوحدات الصغرى.
data.object.currencyكود ISO من تلات حروف، مثلًا EGP.
data.object.customerسجل CustomerAPI (لو بعتّ customerId أو XPay أنشأ واحد من الفورم).
data.object.paymentIntent.idمعرّف Payment IntentAPI (pi_*). خزّنه لو هتعمل مبالغ مستردة من الكود.
data.object.lineItemsاللي اتشري، بالأسعار وبيانات المنتج.
data.object.metadataأي حاجة أرفقتها وقت إنشاء الجلسة.

للصورة الأوسع عن إزاي جلسة الدفع، وPayment Intent، وCharge، والعميل بيتركّبوا مع بعض، وأنهي معرّفات تحتفظ بيها في سجل الطلب بتاعك، شوف نموذج الكائنات.

معالِج قصير بـ Node.js / Express:

app.post("/webhooks/xpay", express.raw({ type: "application/json" }), (req, res) => {
  try {
    verifyWebhook(
      req.body.toString("utf8"),
      req.header("XPay-Signature")!,
      process.env.XPAY_WEBHOOK_SECRET!,
    );
  } catch {
    return res.status(400).send("invalid signature");
  }

  const event = JSON.parse(req.body.toString("utf8"));

  if (event.type === "checkout.session.completed" && event.data.object.status === "complete") {
    fulfillOrder(event.data.object); // idempotent on session.id
  }

  res.status(200).send();
});

تسليمات الـ webhook بتتبع جدول إعادة المحاولة ده عند الردود المش 2xx أو انتهاء المهلة (30 ثانية): فورًا، وبعد دقيقة، و5 دقايق، و30 دقيقة، وساعتين. XPay بيستسلم بعد 5 محاولات.

اختبره

في وضع الاختبار، كل جلسة دفع بتنشئها بترجّع معرّف cs_test_* وURL دفع مستضاف بيستخدم معالِج XPay التجريبي (sandbox). استخدم بطاقة النجاح 5123 4500 0000 0008 بتاريخ انتهاء 01/39 علشان تشغّل المسار الناجح. لقائمة بطاقات الاختبار الكاملة ومصفوفة تاريخ الانتهاء للنتيجة، شوف وضع الاختبار وبطاقات الاختبار.

علشان تجرّب معالِج الـ webhook بتاعك محليًا قبل النشر، شوف تطوير الـ webhook محليًا.

قائمة التحقق قبل الإنتاج

قبل ما تحوّل للحساب الفعلي:

  • بدّل المفاتيح. استبدل sk_test_* بـ sk_live_* في بيئة السيرفر بتاعك. رابط الـ API مش بيتغيّر.
  • اظبط نقطة نهاية webhook فعلية. وضع الاختبار والحساب الفعلي ليهم نقاط نهاية webhook منفصلة. كل واحدة بياخد السرّ بتاع التوقيع whsec_* الخاص بيها.
  • استخدم URL رجوع حقيقي. لازم afterCompletion.redirect.url يكون URL بـ https:// على نطاق أنت متحكّم فيه. ما تستخدمش localhost.
  • امنع تكرار تسليمات الـ webhook على event.id. XPay بيعيد محاولة تسليمات الـ webhook عند الردود المش 2xx أو انتهاء المهلة، فـ checkout.session.completed لنفس الجلسة ممكن يوصل أكتر من مرة. تتبّع معرّفات الأحداث اللي عالجتها بالفعل وتخطّى المكرّر.
  • ابعت Idempotency-Key على POST /checkout/sessions. لو الطلب وقعت مهلته، إعادة المحاولة بنفس المفتاح بترجّع الجلسة الأصلية بدل ما تفتح تانية لنفس السلة. شوف عدم التكرار.
  • فكّر في cancelUrl للدفعات الفاشلة. ظبطه على الجلسة لو عايز تتعامل مع حالات الفشل (الرفض / رفض 3DS / انتهاء مهلة وسيلة الدفع المحلية) على صفحتك بدل صفحة الإعادة المستضافة بتاعة XPay. السلوك الكامل في After completion → cancelUrl.
  • تحقّق من التواقيع في الإنتاج كمان. ما تعطّلش التحقق من التوقيع "علشان تعمل debug". استخدم أداة tunneling للـ webhook مع وضع الاختبار بدالها.

رايح فين بعد كده

في الصفحة دي