التوثيق
الدمججلسة الدفع

دورة حياة العميل

قرّر مين العميل، وإيه اللي الفورم بيجمعه، وإزاي سجل العميل في 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_* بتاع ضيف بيفشل بخطأ واضح. الضيوف للدفع بس؛ مفيش مسار لربطهم بجلسة مستقبلية.

قواعد التعبئة المسبقة

لما الصفحة تتعرض، الفورم بيتعبّى مسبقًا بترتيب أولوية:

  1. customerDetails.{name, email, phone} لو متقدّم.
  2. name، وemail، وphone، وaddress، وshipping بتوع العميل الموجود لو customerId اتظبّط.
  3. فاضي غير كده. العميل بيملاه.

حقول الإيميل والتليفون بتبقى مقفولة (للقراءة بس) لما 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 مسجَّل واحد لكل مستخدم مسجَّل، والإنفاق على مدار الوقت بيتجمّع صح، والضيوف بيتتبّعوا لوحدهم من غير ما تضطر تديرهم.

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

في الصفحة دي