الدفع المستضاف
دمج من ناحية السيرفر بس. أنشئ جلسة دفع، حوّل العميل لصفحة XPay المستضافة، وأكّد بـ webhook.
الدفع المستضاف هو أبسط دمج فيه أي كود. السيرفر بتاعك بينشئ جلسة دفع، ويحوّل العميل لصفحة الدفع المستضافة بتاعة XPay، والـ webhook بتاعك بيستلم checkout.session.completed لما الدفعة تنزل. ومش بتكتب أي كود واجهة.
اختار النمط ده لما تكون عايز دمج من ناحية الـ backend بس، ومش هتمانع إن العميل يتحوّل بعيد عن موقعك للتلت ثواني اللي بياخدها الدفع.
الدليل ده بيعرض الحد الأدنى من المحتوى المطلوب علشان تبدأ جلسة بنمط التحويل. لمجموعة الحقول
الكاملة اللي تقدر تظبطها على POST /checkout/sessions (البنود، وتحصيل بيانات العميل، والعلامة
التجارية، وقيود وسائل الدفع، والرسوم، والبيانات الوصفية، إلخ)، شوف مرجع جلسة
الدفع.
إزاي بيشتغل
- السيرفر بتاعك بيستدعي
POST /checkout/sessionsبالبنود ومكان توديه العميل بعد الدفع. XPay بيرجّع كائن جلسة فيهurl. - السيرفر بتاعك بيحوّل متصفّح العميل للـ
urlده. - العميل بيوصل
https://checkout.xpay.app/c/cs_test_...، ويدخّل بطاقته، ويكمّل الـ 3D Secure لو لازم، والدفعة بتشتغل. - XPay بيحوّل العميل تاني للـ URL اللي ظبطته في
afterCompletion.redirect.url. - 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. الحد الأدنى اللي معالِجك محتاج يعمله:
- تحقّق من ترويسة
XPay-Signature. - فُكّ محتوى الـ JSON.
- لو نوع الحدث
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.status | complete عند نجاح الدفعة. |
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 مع وضع الاختبار بدالها.
رايح فين بعد كده
نموذج الكائنات
إزاي جلسة الدفع، وPayment Intent، وCharge، والعميل بيرتبطوا، وأنهي معرّفات تحتفظ بيها في سجل الطلب بتاعك.
المبالغ المستردة
اعكس دفعة ناجحة باستخدام الـ pi_* اللي خزّنته من الـ webhook.
إعداد الـ Webhooks
أضِف نقطة نهاية webhook وخُد السرّ بتاع التوقيع whsec_*.
التحقق من التواقيع
طريقة التحقق من التوقيع بتفصيل أكتر، زائد الحماية من إعادة التشغيل.
وضع الاختبار وبطاقات الاختبار
قائمة بطاقات الاختبار الكاملة والنتائج اللي تقدر تحاكيها بتغيير تاريخ الانتهاء.
عايز نمط مختلف؟
قارن الدفع المستضاف بـ Drop-in، وElements، وروابط الدفع.
مثال HTML عادي
hosted-redirect.html: إنشاء الجلسة من السيرفر وإعادة التوجيه، من غير SDK على الصفحة. ملف واحد
جاهز للتشغيل زائد سيرفر Express صغيّر.
متجر مثال Next.js
مسار /checkout?ui=hosted: إنشاء الجلسة، وإعادة التوجيه، ومستقبِل webhooks متحقّق من التوقيع.