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

Drop-in

سيرفر زائد كام سطر واجهة. افتح دفع XPay كنافذة منبثقة أو iframe داخلي على نطاقك. العميل عمره ما بيغادر موقعك.

Drop-in بيفتح دفع XPay الكامل جوّه iframe على نطاقك. السيرفر بتاعك بينشئ جلسة دفع، والواجهة بتاعتك بتستدعي xpay.checkout({ ... })، والعميل بيدفع من غير ما ينتقل بعيد أبدًا. بتكتب حوالي عشر سطور كود واجهة؛ والـ iframe بيتولّى وسائل الدفع، وفورم البطاقة، والـ 3D Secure، والوسائل المحلية زي ڤاليو وفوري.

اختار النمط ده لما تكون عايز العميل يفضل على نطاقك بس مش عايز تبني فورم الدفع بنفسك. لو محتاج واجهة مخصّصة، استخدم Elements. لو مش عايز أي كود واجهة، استخدم الدفع المستضاف أو رابط دفع.

أمثلة الكود تحت بتغطي الاتنين Vanilla JavaScript (أي إطار، أو HTML عادي) وReact. الـ SDK الاتنين بيشتركوا في نفس الـ API؛ اختار التاب اللي يناسب الستاك بتاعك. أطر العمل غير React (Vue، Svelte، Solid، Angular، Lit) بتستخدم مسار الـ vanilla.

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

إزاي بيشتغل

  1. السيرفر بتاعك بيستدعي POST /checkout/sessions بـ uiMode: "embedded" والبنود. XPay بيرجّع جلسة فيها clientSecret وpaymentMethodTypes.
  2. السيرفر بتاعك بيشحن الـ clientSecret للواجهة (عادةً جوّه عرض صفحة HTML أو رد fetch).
  3. الواجهة بتاعتك بتحمّل الـ SDK بمفتاحك القابل للنشر وبتستدعي xpay.checkout({ clientSecret, mode: "modal" }). استدعاء checkout.open() بيعرض الـ iframe.
  4. العميل بيختار وسيلة، ويملا الفورم، ويكمّل الـ 3D Secure لو لازم، ويدفع. كل ده من غير ما ينتقل بعيد.
  5. الـ SDK بيطلق الـ onComplete callback بتاعك بمعرّف الـ Payment Intent. والسيرفر بتاعك بيستلم checkout.session.completed بشكل مستقل علشان ينفّذ الطلب.

التفاعل كله بيحصل جوّه iframe بيشاور على https://checkout.xpay.app. بيانات البطاقة عمرها ما بتدخل الـ DOM بتاعك.

نفّذه

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

محتاج المفتاحين لـ Drop-in:

  • sk_test_* للسيرفر بتاعك. بيُستخدم في إنشاء جلسة الدفع.
  • pk_test_* للواجهة بتاعتك. بيُستخدم في تحميل الـ SDK. آمن إنك تشحنه للمتصفّح.

في لوحة التحكم، روح المطورين ← مفاتيح API وانسخ الاتنين. المفاتيح الفعلية (sk_live_* / pk_live_*) بتشتغل بنفس الطريقة بمجرد ما حسابك يتعتمد.

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

شرطين خاصين بـ Drop-in:

  • uiMode: "embedded" بيقول لـ XPay إن الجلسة دي لـ iframe.
  • رد الجلسة فيه clientSecret بيحصر الـ SDK على الجلسة دي بالظبط. هتشحنه للواجهة.
curl -X POST https://api.xpay.app/checkout/sessions \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "uiMode": "embedded",
    "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({
    uiMode: "embedded",
    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 session = await res.json();
// Send `session.clientSecret` to your frontend
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={
        "uiMode": "embedded",
        "afterCompletion": {
            "type": "redirect",
            "redirect": {"url": "https://yourshop.example/order/{CHECKOUT_SESSION_ID}"},
        },
        "lineItems": [
            {
                "priceData": {
                    "currency": "EGP",
                    "unitAmount": 149900,
                    "productData": {"name": "Test product"},
                },
                "quantity": 1,
            }
        ],
    },
    timeout=10,
)
session = res.json()
# Send `session["clientSecret"]` to your frontend

الرد فيه نفس شكل جلسة الدفع اللي بيرجّعه الدفع المستضاف. الحقول اللي بتشحنها للواجهة:

{
  "id": "cs_test_AbC123...",
  "clientSecret": "cs_test_AbC123..._secret_xyz789",
  "amountTotal": 149900,
  "currency": "EGP"
}

كام قاعدة:

  • cancelUrl بيترفض مع uiMode: "embedded". Drop-in بيطلّع حالات الفشل جوّه الـ iframe مع إعادة محاولة. مفيش مسار "وَدّي العميل لـ URL فشل".
  • clientSecret لجلسة واحدة. بيحصر الـ SDK على الجلسة دي بالظبط ومينفعش يتعاد استخدامه. ما تخزّنوش بعد عمر الجلسة.
  • afterCompletion.redirect.url لسه ليه معنى. لما الدفعة تنجح، الـ SDK بيمرّر الـ URL ده لـ onComplete callback بتاعك كـ redirectUrl. كودك هو اللي بيقرّر يتنقّل ولا لأ. لو استخدمت hosted_confirmation، الـ redirectUrl بيبقى undefined: الـ iframe بيعرض صفحة النجاح لحظة وبيقفل النافذة المنبثقة تلقائيًا.

3. حمّل الـ SDK على الواجهة بتاعتك

لو بتستخدم bundler (Vite، أو webpack، إلخ)، ثبّت المحمّل من npm، وبعدين استورد loadXPay واعمله await:

npm install @xpayeg/sdk
import { loadXPay } from "@xpayeg/sdk";

const xpay = await loadXPay("pk_test_..."); // your publishable key

const checkout = xpay.checkout({
  clientSecret: "cs_test_..._secret_xyz789", // from your server
  mode: "modal",
  onComplete: (result) => {
    // result.paymentIntentId is the pi_*
    // result.redirectUrl is your afterCompletion.redirect.url (if set)
    if (result.redirectUrl) {
      window.location.href = result.redirectUrl;
    }
  },
  onClose: () => {
    console.log("Customer closed the modal");
  },
});

document.getElementById("pay-button")?.addEventListener("click", () => {
  checkout.open();
});

على HTML عادي من غير build step، ما تستوردش. حطّ الـ CDN script tag واستدعي الـ factory العام XPay اللي الـ runtime بيعرضه. نفس الـ API، من غير await:

<script src="https://checkout.xpay.app/v1/sdk.js"></script>
<script>
  // الـ factory العام، مش loadXPay. `loadXPay` موجود بس في الـ package بتاع npm.
  const xpay = XPay("pk_test_...");

  const checkout = xpay.checkout({
    clientSecret: "cs_test_..._secret_xyz789", // from your server
    mode: "modal",
    onComplete: (result) => {
      if (result.redirectUrl) window.location.href = result.redirectUrl;
    },
  });

  document.getElementById("pay-button")?.addEventListener("click", () => {
    checkout.open();
  });
</script>

ثبّت الـ SDK packages الاتنين:

npm install @xpayeg/sdk @xpayeg/react
import { CheckoutButton, XPayProvider } from "@xpayeg/react";
import { loadXPay } from "@xpayeg/sdk";

// Load once at module level (not inside a component)
const xpayPromise = loadXPay("pk_test_...");

export function PayPage({ clientSecret }: { clientSecret: string }) {
  return (
    <XPayProvider xpay={xpayPromise}>
      <CheckoutButton
        clientSecret={clientSecret}
        checkoutOptions={{
          onComplete: (result) => {
            if (result.redirectUrl) window.location.href = result.redirectUrl;
          },
        }}
      >
        Pay now
      </CheckoutButton>
    </XPayProvider>
  );
}

<XPayProvider> بيقبل يا إما XPayInstance أو Promise<XPayInstance>، فتمرير xpayPromise مباشرة هو النمط الأصح. ما تستدعيش loadXPay() جوّه مكوّن. حمّله مرة واحدة لكل صفحة على مستوى الموديول.

الـ SDK بيتحمّل من https://checkout.xpay.app/v1/sdk.js مرة واحدة لكل صفحة؛ واستدعاءات loadXPay() اللي بعدها بترجّع نفس النسخة المخزّنة.

4. تعامل مع النتيجة

xpay.checkout(...) بيرجّع CheckoutInstance تقدر تشترك فيه عن طريق الـ callbacks في الـ constructor أو .on(event, handler). خمس أحداث بتطلق على مدى عمر عملية دفع واحدة.

الحدثإمتىالحمولة
readyالجلسة اتحمّلت والـ iframe جاهز للعرض.الـ Checkout SessionAPI الكاملة.
confirmedالعميل ضغط ادفع. مفيد لو عايز تعطّل زرار الإغلاق أو تعرض حالة "بنعالج".لا شيء.
completeالدفعة نجحت. الـ iframe بيقفل النافذة المنبثقة تلقائيًا بعد 300 مللي ثانية.{ status: "succeeded", paymentIntentId, chargeId?, redirectUrl? }.
closeالنافذة المنبثقة اتقفلت. بتطلق للإغلاق اليدوي، أو بعد complete، أو لو checkout.close() اتنادى.لا شيء.
errorالجلسة فشلت في التحميل أو الـ SDK واجه خطأ قاتل. ما بتطلقش للأخطاء اللي العميل ممكن يتعافى منها زي رفض البطاقة.{ message, code? }.

حدث الـ complete مجاملة لتجربة المستخدم. لازم السيرفر بتاعك يفضل يستنى webhook checkout.session.completed علشان يعلّم الطلب مدفوع. الـ iframe ممكن يتقفل بدري، والشبكة ممكن تفصل، والـ onComplete handler بتاعك ممكن يرمي خطأ. الـ webhook هو مصدر الحقيقة.

في React، نفس الـ callbacks بتركب على الـ checkoutOptions prop بتاع <CheckoutButton />: onComplete، onClose، onReady، onConfirmed، onError. بتتطابق واحد لواحد مع الأحداث اللي فوق.

mode بيتحكم في إن الـ iframe يغطّي الصفحة ولا يقعد جوّه حاوية.

mode: modal

تغطية بملء الشاشة بـ iframe في النص. بتستدعي checkout.open() علشان تعرضه. اضغط بره، أو دوس Escape، أو استدعي checkout.close() علشان تخفيه. الأنسب لزراير "ادفع دلوقتي"، وصفحات ما بعد السلة، وأي حاجة مدفوعة بالأحداث.

mode: inline

iframe متركّب جوّه حاوية بتوفّرها أنت. مفيش فتح/قفل: بيبقى موجود بمجرد ما تنشئ النسخة. مرّر container كـ CSS selector أو HTMLElement. الأنسب لصفحات "الدفع" المضمّنة اللي الفورم فيها هو الصفحة.

// Inline mode
const checkout = xpay.checkout({
  clientSecret,
  mode: "inline",
  container: "#checkout-container", // or document.getElementById(...)
  onComplete: (result) => {
    /* ... */
  },
});

// On unmount or before re-creating, release the iframe
checkout.destroy();

الـ iframe المضمّن بيعيد ضبط حجمه تلقائيًا على حسب ارتفاع محتواه. مش محتاج تظبط ارتفاع ثابت في الـ CSS بتاعك.

استدعي checkout.destroy() دايمًا لما تفكّك المكوّن اللي بيملك الدفع المضمّن. من غيرها، الـ iframe بيفضل في الـ DOM، واستدعاء xpay.checkout({ mode: "inline" }) جديد هيركّب واحد تاني. وضع الـ modal بيتولّى تنظيفه بنفسه عند الإغلاق، لكن لسه تقدر تستدعي destroy() علشان تحرّر المستمعين بدري.

<CheckoutButton /> بيشتغل modal بس. للنمط المضمّن في React، انزل للـ JS SDK عن طريق useXPay() واستدعي xpay.checkout({ mode: "inline" }) بنفسك. وافتكر تستدعي destroy() في useEffect للتنظيف.

تخصيص المظهر واللغة

appearance بيقبل نفس شكل brandingSettings بتاع الجلسة. قيم الـ SDK في وقت التشغيل بتكسب؛ وbrandingSettings بتاع الجلسة والقيم الافتراضية للتاجر بتوفّر البدائل الاحتياطية. لقائمة الحقول الكاملة شوف الإعدادات المتقدمة → العلامة التجارية.

const checkout = xpay.checkout({
  clientSecret,
  mode: "modal",
  locale: "ar",
  appearance: {
    colorMode: "dark",
    borderStyle: "rounded",
    colors: { primary: "#635bff" },
  },
});
<CheckoutButton
  clientSecret={clientSecret}
  checkoutOptions={{
    locale: "ar",
    appearance: {
      colorMode: "dark",
      borderStyle: "rounded",
      colors: { primary: "#635bff" },
    },
    onComplete: (result) => {
      /* ... */
    },
  }}
>
  Pay now
</CheckoutButton>

مفيد لما نفس الجلسة محتاجة تبان مختلفة عبر سطوح مختلفة (مثلًا تطابق مفتاح الوضع الداكن في صفحتك من غير ما تحفظه على الجلسة).

أكّد بـ webhook

الـ onComplete callback بتاع Drop-in سريع ومريح، لكنه مش موثوق. جلسة الدفع اللي السيرفر بتاعك بينشئها هي نفس الكائن اللي بينتجه كل نمط تاني، فمعالِج webhook checkout.session.completed اللي كنت هتكتبه للدفع المستضاف بيشتغل من غير تغيير مع Drop-in.

الحد الأدنى اللي معالِجك محتاج يعمله:

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

لشرح المعالِج الكامل (طريقة التحقق من التوقيع، وجدول حقول الحمولة، وسلوك إعادة المحاولة)، شوف الدفع المستضاف → أكّد بـ webhook. نفس الكود بينطبق هنا.

اختبره

في وضع الاختبار، استخدم بطاقة النجاح 5123 4500 0000 0008 بتاريخ انتهاء 01/39 علشان تشغّل المسار الناجح جوّه الـ iframe. فورم البطاقة، وتحدّي الـ 3D Secure، وحالة النجاح بعد الدفع، كلهم بيشتغلوا جوّه الـ iframe؛ مش محتاج تبدّل تابات ولا صفحات. لقائمة بطاقات الاختبار الكاملة ومصفوفة تاريخ الانتهاء للنتيجة، شوف وضع الاختبار وبطاقات الاختبار.

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

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

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

  • بدّل المفاتيح. استبدل sk_test_* بـ sk_live_* على السيرفر وpk_test_* بـ pk_live_* على الواجهة. رابط الـ API مش بيتغيّر.
  • اظبط نقطة نهاية webhook فعلية. وضع الاختبار والحساب الفعلي ليهم نقاط نهاية webhook منفصلة. كل واحدة بياخد السرّ بتاع التوقيع whsec_* الخاص بيها.
  • تحقّق من التواقيع على الـ webhook، مش على onComplete. عامل حدث الـ complete كتلميح لتجربة المستخدم، مش كتصريح.
  • ما تكشفش المفتاح السرّي للمتصفّح. مفاتيح sk_* للسيرفر بس. الـ SDK بياخد مفتاح قابل للنشر (pk_*) بالتصميم.
  • امنع تكرار تسليمات الـ webhook على event.id. XPay بيعيد محاولة التسليمات عند الردود المش 2xx أو انتهاء المهلة، فنفس checkout.session.completed لنفس الجلسة ممكن يوصل أكتر من مرة. تتبّع معرّفات الأحداث اللي عالجتها بالفعل.
  • ابعت Idempotency-Key على POST /checkout/sessions. لو الطلب وقعت مهلته، إعادة المحاولة بنفس المفتاح بترجّع الجلسة الأصلية بدل ما تفتح تانية. شوف عدم التكرار.
  • اظبط content-security-policy بيسمح بـ https://checkout.xpay.app. لو موقعك بيستخدم توجيه frame-src أو script-src صارم، لازم تسمح بالاتنين سكربت الـ SDK والـ iframe المضمّن.

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

في الصفحة دي