نظرة عامة
جلسة الدفع هي الكائن المركزي اللي بينشئه أي دمج. نفس الشكل بيرجعلك من POST، وGET، وعميل الـ SDK، ومن كل webhook خاص بـ checkout.session.
جلسة الدفع بتمثّل محاولة عميل واحد إنه يدفعلك. السيرفر بتاعك بينشئها، وXPay بيرجّعلك كائن الجلسة، ومن هنا كل نمط دمج هو مجرد طريقة مختلفة لعرض نفس الكائن: صفحة مستضافة، أو رابط دفع، أو SDK drop-in، أو Elements مخصّصة بالكامل.
كائن الجلسة اللي بيرجعلك من POST /checkout/sessions، واللي بتعمله GET بعدين، والـ data.object اللي بيتحمل على كل checkout.sessionAPI webhook. كلهم نفس الشكل. فيه DTO واحد وmapper واحد ورا الثلاثة. لو بنيت على رد الإنشاء، معالِج الـ webhook بتاعك هيقرا نفس الحقول بنفس الأسماء.
علشان تعرف علاقة جلسة الدفع بـ Payment Intent، وCharge، وRefund، والعميل، وBalance Transaction، شوف نموذج الكائنات.
إزاي بتتركّب
اختار نمط الدمج اللي يناسب قد إيه عايز تتحكم في الواجهة. كل دليل نمط بيشرح البناء، وطريقة الاختبار، ومعالِج الـ webhook، وقائمة التحقق قبل الإنتاج للسطح ده. كلها بتنشئ جلسة دفع بنفس الطريقة.
روابط الدفع
من غير كود. شارك رابط https://checkout.xpay.app/p/plink_.... وبتتنشئ الجلسة أول ما العميل
يتفاعل.
الدفع المستضاف
من السيرفر بس. أنشئ جلسة، وحوّل العميل لـ session.url، وأكّد الدفع بـ webhook.
Drop-in
سيرفر زائد كام سطر في الواجهة. افتح صفحة الدفع كنافذة منبثقة أو iframe متضمَّن جوّه نطاقك.
Elements
سيرفر زائد واجهة مخصّصة. ابنِ الفورم بنفسك بـ <PaymentElement /> وconfirmPayment().
دورة الحياة
الجلسة بتمر بآلتين حالة قصيّرتين: status للجلسة نفسها وpaymentStatus للفلوس.
status
| الحالة | إمتى بتتحدد | إنت بتعمل إيه |
|---|---|---|
open | بتتحدد عند الإنشاء. الجلسة بتقبل التعديلات وقابلة للعرض في واجهة الدفع. | استنى العميل يدفع، أو الجلسة تنتهي صلاحيتها. |
complete | بتتحدد لما الدفع ينجح. الجلسة بتتقفل: مفيش تعديلات تانية، ومفيش محاولات دفع. | نفّذ الطلب بناءً على checkout.session.completed. |
expired | بتتحدد لما expiresAt يعدّي، أو لما تستدعي POST /checkout/sessions/:id/expire. الجلسة مينفعش تتعاد. | أنشئ جلسة جديدة لو العميل عايز يحاول تاني. |
paymentStatus
| الحالة | إمتى بتتحدد |
|---|---|
unpaid | الافتراضي. ما حصلتش أي عملية دفع ناجحة. |
paid | فيه عملية دفع نجحت بكامل مبلغ الجلسة. |
no_payment_required | الجلسة اتنشئت في وضع setup (جمع بيانات الدفع بس من غير خصم). |
حقل paymentIntent في الرد بيبقى null لحد ما العميل يبعت الفورم لأول مرة. بعد كده بيحمل شكل Payment IntentAPI بالكامل، نفس اللي بيرجعه GET /payment-intents/:id. الـ ChargesAPI والـ RefundsAPI بتبقى متضمَّنة جوّه الـ Payment Intent.
إنشاء جلسة
الحقل الوحيد المطلوب في POST /checkout/sessions هو afterCompletion. وكل حاجة تانية ليها قيمة افتراضية. عمليًا هتبعت كمان lineItems علشان العميل يشوف بيدفع مقابل إيه. والبنود ممكن تكون ثابتة، أو قابلة للتعديل، أو إضافات اختيارية العميل بيختار يضيفها، والجلسة اللي كلها اختيارية صحيحة: بتفتح بإجمالي 0 والعميل بيضيف بنود قبل ما يدفع. شوف بنود الفاتورة والتسعير.
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,
"productData": {"name": "Test product"},
},
"quantity": 1,
},
],
},
timeout=10,
)
session = res.json()الرد بيحمل الجلسة كاملة، وفيها الاعتمادَين اللي الدمج بتاعك هيستخدمهم بعد كده:
{
"id": "cs_test_AbC123...",
"object": "checkout.session",
"status": "open",
"paymentStatus": "unpaid",
"url": "https://checkout.xpay.app/c/cs_test_AbC123...",
"clientSecret": "cs_test_AbC123..._secret_xyz",
"amountTotal": 149900,
"currency": "EGP",
"afterCompletion": { "type": "redirect", "redirect": { "url": "..." } },
"paymentIntent": null,
"livemode": false
}استخدم url علشان تحوّل العميل (الدفع المستضاف، روابط الدفع). واستخدم clientSecret علشان تركّب الـ SDK (Drop-in، Elements). الرمز {CHECKOUT_SESSION_ID} الحرفي في redirect.url بيتستبدل في السيرفر بـ id بتاع الجلسة قبل ما الرابط يتخزّن، فصفحة العودة بتاعتك تقدر تقراه من المسار من غير ما تضطر تمرّره في query parameters.
المجموعة الكاملة من الحقول اللي تقدر تظبطها متجمّعة حسب الغرض. وكل مجموعة ليها صفحتها بالتفصيل حقل بحقل.
بنود الفاتورة والتسعير
الأسعار الموجودة مقابل priceData المضمَّن، والكميات، والكميات القابلة للتعديل، وبنود المبلغ
المخصّص، والعملة.
دورة حياة العميل
customerId مقابل customerDetails، وcustomerCreation، وcustomerUpdate، والاسم، والتليفون،
والعنوان، والشحن، والحقول المخصّصة.
بعد إتمام الدفع
redirect مقابل hosted_confirmation، وcancelUrl، ورابط زر العودة، والرسالة المخصّصة.
الإعدادات المتقدّمة
العلامة التجارية، واللغة، وأنواع طرق الدفع، والرسوم وتمرير ضريبة القيمة المضافة، وانتهاء الصلاحية، والبيانات الوصفية.
الأوضاع
اثنان من الـ enums على الجلسة بيحدّدوا نوع الدفع اللي بتمثّله وإزاي العميل بيتعامل معاها. الاتنين بيتحددوا عند الإنشاء وثابتين طول حياة الجلسة.
mode
mode هو نوع حركة الفلوس. القيمة الافتراضية payment.
| القيمة | المعنى |
|---|---|
payment | عملية دفع لمرة واحدة. الافتراضي لأغلب عمليات الدمج تقريبًا. |
subscription | فوترة متكرّرة. لسه مش مدعومة على الـ checkout API. |
setup | جمع طريقة دفع من غير خصم منها. paymentStatus بيستقر على no_payment_required. |
uiMode
uiMode هو الطريقة اللي العميل بيوصل بيها للفورم. القيمة الافتراضية hosted. الاختيار مرتبط بنمط الدمج اللي اخترته فوق؛ سلوك الجلسة، واستدعاء الـ SDK اللي بتعمله، والحقول اللي تقدر تظبطها، كلها بتتغير بيه.
| القيمة | مكان تشغيل الفورم | تستخدمها مع |
|---|---|---|
hosted | صفحة XPay المستضافة على https://checkout.xpay.app/c/cs_test_.... السيرفر بيرجّع url. | الدفع المستضاف، روابط الدفع. |
embedded | iframe على موقعك، بيفتحه الـ SDK كنافذة منبثقة أو متضمَّن. | Drop-in. |
custom | الفورم بتاعك إنت، باستخدام Elements SDK وconfirmPayment(). | Elements. |
كام قاعدة بتطلع من ده:
cancelUrlصالح بس لماuiMode: "hosted". ده الـ redirect اللي XPay بيبعت العميل ليه لو فشلت محاولة دفع على الصفحة المستضافة (رفض، أو رفض 3DS، أو انتهاء مهلة وسيلة محلية). مش زر إلغاء للعميل: الصفحة المستضافة ما فيهاش واحد.uiMode: "custom"معناه إن الكود بتاعك هو اللي بيتولّى جمع بيانات العميل. الجلسة بترفضnameCollection، وphoneNumberCollection، وbillingAddressCollection، وshippingAddressCollection، وsubmitTypeفي الوضع ده.- بالنسبة لـ
embeddedوcustom، بتوثّق الـ SDK بـclientSecretبتاع الجلسة. المفتاح المنشور لوحده مش كفاية؛ الـ secret بيقصر الـ SDK على جلسة واحدة بعينها.
الحقول المتعارضة
شوية أزواج في جسم الإنشاء متعارضة. الـ API بيرفض الطلبات اللي بتبعت الاتنين مع بعض، ونفس القواعد بتنطبق على PATCH.
| إما | أو | ليه |
|---|---|---|
customerId | customerDetails | سجل العميلAPI الموجود هو مصدر التعبئة المسبقة، أو إنت بتقدّم بيانات خام. مش الاتنين. |
customerId | customerCreation: "always" | "أنشئ عميل جديد دايمًا" بيتعارض مع "استخدم العميل الموجود ده". |
paymentMethodTypes | paymentMethodConfigurationId | اختار قائمة طرق صريحة، أو اشِر لتهيئة محفوظة. |
lineItem.price | lineItem.priceData | كل بند فاتورة بيشير لـ PriceAPI موجود بالـ id، أو بيضمّن بيانات المنتج والسعر داخليًا. |
بند من نوع CUSTOM | allowPromotionCodes، discounts | بنود المبلغ المخصّص (العميل بيدخل المبلغ بنفسه) مش متوافقة مع الخصومات. |
customerUpdate (اللي بيتحكم في إن الدفع يكتب البيانات المجمَّعة على العميل ولا لأ) ليه معنى بس لما customerId يكون متقدّم. إرساله من غير customerId بيبقى خطأ تحقّق.
إيه اللي يتعدّل
PATCH /checkout/sessions/:id بيحدّث جلسة مفتوحة. بيقبل كل حقل في جسم الإنشاء ما عدا اللي تحت، واللي بيتقفل عند الإنشاء:
modeuiModesubmitTypecurrencyexpiresAfterMinutes
lineItems في PATCH هو استبدال كامل: المصفوفة الجديدة بتكتب فوق القديمة. علشان تغيّر كمية بند واحد، ابعت المصفوفة المطلوبة كاملة. الجلسات في حالة complete أو expired بتبقى للقراءة بس وبترفض PATCH.
انتهاء الصلاحية
كل جلسة ليها expiresAt. الافتراضي 24 ساعة بعد الإنشاء؛ والحد الأدنى 30 دقيقة. ظبّط expiresAfterMinutes في جسم الإنشاء علشان تغيّره.
حاجتين بيعلّموا الجلسة كـ expired:
expiresAtبيعدّي. الجلسة بقت مش صالحة للدفع؛ والصفحة المستضافة بتعرض حالة نهائية "خلصت كل حاجة هنا".- إنت بتستدعي
POST /checkout/sessions/:id/expireصراحةً. مفيد لما تكون عايز توقّف قبول الدفع على جلسة مش ناوي تكرّمها، أو تفضّي خانات الاستخدام على كود ترويج لمرة واحدة.
المسارَين الاتنين بيطلّقوا checkout.session.expired webhook.
الـ Webhooks
حدثين بيتطلقوا لدورة حياة الجلسة نفسها. الـ data.object في كل واحد هو جلسة دفعAPI كاملة، نفس شكل اللي بيرجعه GET /checkout/sessions/:id.
| الحدث | إمتى |
|---|---|
checkout.session.completed | الدفع نجح. status بقى complete، وpaymentStatus بقى paid (أو no_payment_required لوضع setup). |
checkout.session.expired | الجلسة انتهت صلاحيتها بالوقت أو باستدعاء /expire صريح. |
عملية دفع ناجحة على الدفع المستضاف بتطلّق أربع أحداث. بتتبعت بالترتيب ده:
payment_intent.createdcharge.succeededpayment_intent.succeededcheckout.session.completed
تسليم الـ webhook غير متزامن وبيتم مع إعادة محاولة، فـ ما تعتمدش على ترتيب الوصول في المعالِج بتاعك. اربط تنفيذ الطلب بـ checkout.session.completed واعتبر الأحداث الأبكر سياق إضافي. الأحداث الأبكر مفيدة لما تحتاج تتفاعل مع الـ Payment IntentAPI أو الـ ChargeAPI الأساسي مباشرةً. شوف مرجع الأحداث للقائمة الكاملة.
رايح فين بعد كده
نموذج الكائنات
إزاي جلسة الدفع بتتعلّق بـ Payment Intent، وCharge، والعميل، وRefund. والمعرّفات اللي تحتفظ بيها في سجل الطلب بتاعك.
بنود الفاتورة والتسعير
ابنِ السلة: الأسعار الموجودة، وpriceData المضمَّن، والكميات، والمبالغ المخصّصة.
دورة حياة العميل
قرّر إزاي XPay بيتعامل مع العميل وأنهي حقول الفورم بيجمعها.
بعد إتمام الدفع
اظبط مكان وصول العميل بعد عملية دفع ناجحة.
الـ Webhooks: إعداد نقطة نهاية
استقبل checkout.session.completed وتحقّق من التوقيع.
نموذج الكائنات
إزاي موارد XPay بتتركّب مع بعض. اللي بتنشئه، اللي بتقراه، والمعرّفين اللي تحتفظ بيهم في سجل الطلب بتاعك.
بنود الفاتورة والتسعير
إزاي بتشتغل `lineItems` على جلسة الدفع: الأسعار الموجودة مقابل `priceData` المضمَّن، والكمية الثابتة والقابلة للتعديل، وبنود المبلغ المخصّص، والعملة، وبوابة الإتاحة.