دورة حياة العميل
قرّر مين العميل، وإيه اللي الفورم بيجمعه، وإزاي سجل العميل في XPay بيتطوّر بين الدافعين لمرة واحدة والعملاء العائدين.
الـ العميلAPI هو سجل XPay لحد دفعلك. بيحمل اسمه، وإيميله، وتليفونه، وعنوانه، وإجمالي الإنفاق المربوط بالمعرّفات دي. الصفحة دي عن إزاي السجل ده بيتعمل، ويتعبّى مسبقًا، ويتعاد استخدامه عبر سيناريوهات الدمج الثلاثة اللي على الأرجح هتبنيها: العملاء العائدين المسجَّلين، والعملاء المسجَّلين لأول مرة، وعمليات دفع الضيوف.
القاعدة الوحيدة اللي بتحرّك كل قرار تحت: خزّن الـ cus_* مقابل المستخدم المسجَّل بتاعك، مش مقابل ضيف أبدًا. XPay بيمنع تكرار عمليات دفع الضيوف في السيرفر؛ ولو ثبّتّ cus_* لضيف في قاعدة بياناتك هتلاقي نفسك بتحارب منع التكرار ده.
نوعَي العميل
XPay بيتتبّع نوعين من سجلات العميل.
| النوع | إمتى بيتعمل | الاستخدام |
|---|---|---|
| مسجَّل | يا إما تظبّط customerCreation: "always" على جلسة دفع والعميل يدفع، يا إما التاجر يربط customerId كان معاك بالفعل صراحةً. لوحة التحكم بتعدّهم عملاء معروفين؛ وتقدر تربط بيهم طرق دفع محفوظة بعدين (اشتراكات، إلخ). | المستخدم المسجَّل بتاعك دفع. احفظ الـ cus_* على صف حسابه. |
| ضيف | بيتعمل تلقائيًا أثناء الدفع لما مفيش customerId متقدّم وcustomerCreation يكون if_required (الافتراضي). XPay بيجمّع الضيوف حسب الإيميل، والتليفون، وبصمة البطاقة علشان يتعرّف على نفس الشخص عبر الزيارات. | دافع لمرة واحدة أو حد ما سجّلش دخوله. ما تخزّنش الـ cus_*: XPay بيتولّى منع التكرار، وإنت هتحاربه بس. |
النوعين الاتنين بيظهروا في لوحة التحكم. حقول تجميع الضيف (كل إيميل وتليفون استخدمهم، وكل بطاقة دفع بيها) بتتراكم تلقائيًا. لوحة التحكم بتعرض كمان "العملاء المرتبطين" علشان المشغّل يشوف لما سجل ضيف يتقاطع مع سجل مسجَّل.
ثلاثة أنماط حسب سيناريو التاجر
شكل customerCreation، وcustomerId، وcustomerDetails اللي بتبعتهم بيعتمد على اللي إنت عارفه عن الشخص اللي بيدفع.
مستخدم مسجَّل عائد
عندك cus_* متخزّن على صف المستخدم بتاعك. مرّره كـ customerId. الفورم بيتعبّى مسبقًا من السجل الموجود. ما تبعتش customerDetails (متعارض مع customerId). اختياريًا ظبّط customerUpdate علشان تكتب تغييرات الفورم رجوعًا للسجل.
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}" } },
"customerId": "cus_test_AbC123",
"customerUpdate": { "address": "auto", "shipping": "auto" },
"lineItems": [ { "price": "price_test_xyz", "quantity": 1 } ]
}'const session = 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}" },
},
customerId: user.xpay_customer_id, // stored on your user row
customerUpdate: { address: "auto", shipping: "auto" },
lineItems: [{ price: "price_test_xyz", quantity: 1 }],
}),
}).then((r) => r.json());import os, requests
session = 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}"},
},
"customerId": user.xpay_customer_id, # stored on your user row
"customerUpdate": {"address": "auto", "shipping": "auto"},
"lineItems": [{"price": "price_test_xyz", "quantity": 1}],
},
timeout=10,
).json()مستخدم مسجَّل لأول مرة
لسه مفيش cus_*. ابعت اللي إنت عارفه كـ customerDetails للتعبئة المسبقة، وظبّط customerCreation: "always" علشان يتعمل عميل مسجَّل وقت الدفع. خُد data.object.customer.id من webhook الـ checkout.session.completed وخزّنه على صف المستخدم بتاعك.
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}" } },
"customerCreation": "always",
"customerDetails": {
"name": "Aya Hassan",
"email": "aya@example.com",
"phone": "+201234567890"
},
"metadata": { "user_id": "u_42" },
"lineItems": [ { "price": "price_test_xyz", "quantity": 1 } ]
}'const session = 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}" },
},
customerCreation: "always",
customerDetails: {
name: user.name,
email: user.email,
phone: user.phone,
},
metadata: { user_id: user.id },
lineItems: [{ price: "price_test_xyz", quantity: 1 }],
}),
}).then((r) => r.json());import os, requests
session = 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}"},
},
"customerCreation": "always",
"customerDetails": {
"name": user.name,
"email": user.email,
"phone": user.phone,
},
"metadata": {"user_id": user.id},
"lineItems": [{"price": "price_test_xyz", "quantity": 1}],
},
timeout=10,
).json()دفع ضيف
مفيش مستخدم مسجَّل. سيب customerId واترك customerCreation على الافتراضي (if_required). XPay بينشئ أو بيطابق سجل ضيف بالإيميل، والتليفون، وبصمة البطاقة. مش محتاج تتبّع الـ cus_* على ناحيتك. لوحة التحكم بتجمّع الضيوف تلقائيًا.
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": [ { "price": "price_test_xyz", "quantity": 1 } ]
}'const session = 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: [{ price: "price_test_xyz", quantity: 1 }],
}),
}).then((r) => r.json());import os, requests
session = 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": [{"price": "price_test_xyz", "quantity": 1}],
},
timeout=10,
).json()الـ webhook اللي بيتطلق عند النجاح (checkout.session.completed) بيحمل العميل المستقرّ في data.object.customer. لحالة المسجَّل لأول مرة، ده المكان اللي بتاخد منه الـ cus_* الجديد علشان تحفظه على صف المستخدم بتاعك.
customerId مقابل customerDetails
دول الطريقتين المتعارضتين لتحديد العميل على الجلسة.
| الحقل | بيعمل إيه | النوع المطلوب |
|---|---|---|
customerId | بيشير لعميل موجود (cus_*). الفورم بيتعبّى مسبقًا بالاسم، والإيميل، والتليفون، وعنوان الفوترة، والشحن من السجل ده. | مسجَّل |
customerDetails | بيانات تواصل وعنوان مضمَّنة (name، email، phone، billingDetails.address، shipping). بتعبّي الفورم مسبقًا. بتُستخدم لإنشاء عميل جديد وقت الدفع. | لا ينطبق |
إرسال الاتنين بيتم رفضه. إرسال ولا واحدة كويس: الفورم بيبقى فاضي والعميل بيملاه.
customerId لازم يشير لعميل مسجَّل. تمرير cus_* بتاع ضيف بيفشل بخطأ واضح. الضيوف للدفع بس؛ مفيش مسار لربطهم بجلسة مستقبلية.
قواعد التعبئة المسبقة
لما الصفحة تتعرض، الفورم بيتعبّى مسبقًا بترتيب أولوية:
customerDetails.{name, email, phone}لو متقدّم.name، وemail، وphone، وaddress، وshippingبتوع العميل الموجود لوcustomerIdاتظبّط.- فاضي غير كده. العميل بيملاه.
حقول الإيميل والتليفون بتبقى مقفولة (للقراءة بس) لما customerId يكون متظبّط والسجل الموجود يحملها. العميل مينفعش يغيّر إيميل أو تليفون حد تاني في وسط الدفع.
customerCreation
بيتحكم في إمتى XPay بينشئ سجل عميل وقت الدفع. قيمتين؛ الافتراضي if_required.
| القيمة | بيعمل إيه |
|---|---|
if_required | الافتراضي. لو العميل ملا إيميل أو تليفون، بيتعمل أو بيتطابق عميل ضيف. ولو ما ملاش حاجة والتاجر ما ظبّطش customerId، مفيش سجل عميل بيتعمل خالص. |
always | بيتعمل عميل مسجَّل من customerDetails وقت الدفع، حتى لو كتب اسم بس. استخدمه لما تكون عايز حساب معروف محفوظ (زيارات عائدة، إيميلات بعد الشراء، اشتراكات مستقبلية). |
customerCreation متعارض مع customerId: "cus_..." لقيمة always. لو عندك عميل بالفعل، فإنت مش بتطلب من XPay إنه يعمل واحد تاني.
customerUpdate
لما customerId يكون متظبّط، البيانات اللي اتجمعت من الفورم مش بتتكتب رجوعًا للعميل الموجود افتراضيًا. ظبّط customerUpdate علشان تختار التحديث لكل حقل. كل علم فرعي بيبقى auto (يكتب) أو never (الافتراضي، ما يكتبش).
{
"customerId": "cus_test_AbC123",
"customerUpdate": {
"name": "auto", // overwrite customer.name with the form value
"address": "auto", // overwrite customer.address with the billing address
"shipping": "auto", // overwrite customer.shipping with the shipping address
},
}customerUpdate ليه معنى بس لما customerId يكون متقدّم. إرساله من غير customerId بيبقى خطأ تحقّق.
email وphone مش موجودين في customerUpdate لأنهم مقفولين على مستوى الفورم لما العميل الموجود يحملهم. ينفع بس يتـملّوا لما يكونوا ناقصين (شوف إزاي XPay بيتعامل مع العملاء الضيوف).
إيه اللي الفورم بيجمعه
أربعة مفاتيح منطقية بتتحكم في أنهي حقول الفورم المستضاف بيطلبها من العميل. كلها افتراضيًا false.
| المفتاح | بيطلب إيه | مطلوب عند الإرسال؟ |
|---|---|---|
nameCollection | اسم العميل الكامل. | نعم لما يكون مفعّل. |
phoneNumberCollection | تليفون العميل مع رمز الدولة. | نعم لما يكون مفعّل. الإرسال بيفشل بـ parameter_missing على customerDetails.phone لو فاضي. |
billingAddressCollection | اسم حامل البطاقة وعنوان الفوترة الكامل (الشارع، المدينة، المحافظة). | نعم لما يكون مفعّل. الإرسال بيفشل لو customerDetails.billingDetails.address.line1 ناقص. |
shippingAddressCollection | اسم المستلِم وعنوان الشحن. | نعم لما يكون مفعّل. الإرسال بيفشل لو customerDetails.shipping.address.line1 ناقص. |
الإيميل بيتطلب دايمًا ومطلوب دايمًا. هو وجهة الإيصال والمعرّف الأساسي لمنع تكرار الضيوف.
تسمية زر الإرسال بتتظبّط لوحدها عن طريق submitType (pay / subscribe / book / donate).
الحقول المخصّصة
محتاج تطلب رقم ضريبي، أو ملاحظة على الطلب، أو تفضيل توصيل؟ customFields مصفوفة لحد 3 إدخالات. كل واحد بيعرض حقل إدخال على الفورم وإجابة العميل بترجع في رد الجلسة وحمولة الـ webhook.
| نوع الحقل | الإدخال المعروض | التحقّق |
|---|---|---|
TEXT | حقل نص سطر واحد. | minCharacters / maxCharacters اختياري عن طريق hasLimits + limitType. |
NUMBER | حقل أرقام بس. | نفس حدود الأحرف بتنطبق. |
DROPDOWN | قائمة بـ dropdownOptions بيحدّدها التاجر. | خيارين على الأقل مطلوبين. |
CHECKBOX | مربع اختيار واحد العميل بيعلّمه. | مفيش حدود أحرف. |
{
"customFields": [
{
"label": "Tax ID",
"type": "TEXT",
"isOptional": false,
"hasLimits": true,
"limitType": "BETWEEN",
"minCharacters": 9,
"maxCharacters": 15,
},
{
"label": "Delivery preference",
"type": "DROPDOWN",
"dropdownOptions": [{ "label": "Leave at door" }, { "label": "Hand to recipient" }],
},
],
}كل حقل بياخد key ثابت في السيرفر (مشتق من label) علشان الإجابة ترجع على نفس المفتاح في حمولة الرد. القيمة المجمَّعة بتعيش في الخانة المطابقة text / numeric / dropdown على كل إدخال.
إزاي XPay بيتعامل مع العملاء الضيوف
مسار الدفع الافتراضي (من غير customerId، وcustomerCreation: "if_required") بينشئ عميل ضيف أول مرة حد يدفع. نفس الضيف بيتطابق في الزيارات اللي بعدها لما أي معرّف يتقاطع. مش بتكتب أي كود لده.
XPay بيطابق بـ:
- الإيميل، من غير حساسية للأحرف الكبيرة/الصغيرة ومتشال منه الفراغات.
- التليفون، متطبّع (رمز الدولة، وبادئة
00بتتحوّل لـ+). - بصمة البطاقة، بتتلتقط بعد عملية دفع ناجحة. لو ضيف دفع ببطاقة اتشافت على سجل ضيف تاني، XPay بيدمج الاتنين وبيوحّد الإنفاق، وسجل الدفع، والأجهزة على السجل الباقي.
ده بيديك إيه في لوحة التحكم:
- ملف الضيف بيتراكم فيه كل إيميل، وتليفون، وبطاقة استخدمهم على مدار الوقت.
- إنفاق الضيف، والمبالغ المستردة، وعدد عمليات الدفع بتبقى دقيقة حتى لو دفع تلات مرات بثلاث فورمات مختلفة شوية.
- قسم "العملاء المرتبطين" في ملف العميل المسجَّل بيعرض سجلات الضيوف اللي بتشاركه بيانات التواصل، علشان المشغّل يربط بينهم.
اللي العملاء الضيوف مينفعش يعملوه:
- مينفعش يبقى ليهم طريقة دفع محفوظة مربوطة لعمليات خصم مستقبلية. كل دفع ضيف هو إدخال بطاقة جديد. لو منتجك محتاج بطاقات محفوظة (اشتراكات، إعادة طلب بضغطة واحدة)، لازم تظبّط
customerCreation: "always"علشان تعمل عميل مسجَّل بدلًا منه.
تجنّب تكرار سجلات العميل
أكبر طريقة بيقع فيها التجار: إعادة إنشاء عميل XPay مع كل تسجيل دخول أو كل طلب، بدل إعادة استخدام اللي عندك بالفعل. النمط اللي تحت بيتجنّب ده.
خزّن الـ cus_* مقابل المستخدم المسجَّل بتاعك
لما مستخدم مسجَّل دخوله يعمل أول عملية دفع، ظبّط customerCreation: "always" وخُد data.object.customer.id من webhook الـ checkout.session.completed. احفظ الـ cus_* ده على صف المستخدم في قاعدة بياناتك.
// In your webhook handler
if (event.type === "checkout.session.completed") {
const session = event.data.object;
const userId = session.metadata?.user_id; // you set this on session create
const customerId = session.customer?.id; // cus_test_...
if (userId && customerId) {
await db.users.update(userId, { xpay_customer_id: customerId });
}
}مرّره كـ customerId على كل جلسة بعد كده
لكل عملية دفع بعد كده من نفس المستخدم، ظبّط customerId: "cus_test_...". الفورم بيتعبّى مسبقًا، مفيش عميل جديد بيتعمل، والإنفاق بيتجمّع تحت نفس السجل.
const session = 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: returnUrl } },
customerId: user.xpay_customer_id, // re-use, don't recreate
lineItems: [...],
}),
});ما تخزّنش cus_* بتاع ضيف على صفوف المستخدمين
لو مستخدم دفع كضيف الأول (قبل ما يسجّل) وبعدين عمل حساب، الصح إنك تبدأ قصة الـ cus_* من أول عملية دفع مسجَّلة ليه. ما تحاولش تنقل cus_* بتاع الضيف على حسابه. الضيوف بيتمنع تكرارهم في السيرفر؛ والمشغّل يقدر يشوف الربط في قسم "العملاء المرتبطين" في لوحة التحكم.
استخدم metadata علشان تمرّر معرّف المستخدم بتاعك إنت
ظبّط metadata: { user_id: "..." } على كل جلسة بتنشئها. بترجعلك في حمولة الـ webhook، وفي رد استرجاع الجلسة، وعلى الـ Payment Intent. بيخلّي معالِج الـ webhook بتاعك بسيط جدًا: حمولة الجلسة بتقولك بالظبط أنهي مستخدم دفع.
النتيجة: عميل XPay مسجَّل واحد لكل مستخدم مسجَّل، والإنفاق على مدار الوقت بيتجمّع صح، والضيوف بيتتبّعوا لوحدهم من غير ما تضطر تديرهم.
رايح فين بعد كده
بنود الفاتورة والتسعير
إزاي بتشتغل `lineItems` على جلسة الدفع: الأسعار الموجودة مقابل `priceData` المضمَّن، والكمية الثابتة والقابلة للتعديل، وبنود المبلغ المخصّص، والعملة، وبوابة الإتاحة.
بعد إتمام الدفع
رجّع العميل لموقعك، أو سيب XPay يعرض صفحة شكر مستضافة. بالإضافة لإزاي تتعامل مع عمليات الدفع الفاشلة وليه الـ webhook هو مصدر الحقيقة.