التوثيق
الدمج

المبالغ المستردة

اعكس دفعة ناجحة بالكامل أو جزئيًا. POST واحد مقابل الـ Payment Intent أو الـ Charge، وGET اختياري علشان تقراه.

الـ Refund بيعكس فلوس من دفعة ناجحة ويرجّعها للعميل. تقدر ترد المبلغ كامل أو أي جزء منه، أكتر من مرة، لحد ما الدفعة ما يبقاش فيها حاجة تتردّ. سطح الـ API صغير: POST /refunds واحد للإصدار، وGET /refunds/:id واحد للقراءة، وGET /refunds واحد للسرد.

الصفحة دي بتغطّي مسار المطوّر: إمتى تستخدم الـ API، وتبعت إيه، وبيرجع إيه، وأنهي أحداث تستمع لها. علشان مسار الإصدار من لوحة التحكم اللي التجار بيستخدموه يوم بيوم، شوف المبالغ المستردة تحت المميزات.

POST /refunds بياخد paymentIntentId (مقبض المعاملة اللي خزّنته على الطلب) أو chargeId (charge معيّن). قدّم واحد بالظبط. مسار paymentIntentId هو الأساسي واللي معظم عمليات الدمج المفروض تستخدمه؛ بيشتغل مباشرة مع الـ pi_* اللي بتمسكه أصلًا من checkout.session.completed.

إزاي بيشتغل

  1. السيرفر بتاعك معاه الـ pi_* بتاع الدفعة، اللي مسكته من webhook checkout.session.completed (أو قريته عند الطلب من GET /checkout/sessions/:id).
  2. السيرفر بتاعك بينادي POST /refunds بالـ paymentIntentId. واختياريًا ضيف amount (للمبالغ المستردة الجزئية)، أو reason، أو description، أو metadata.
  3. XPay بيعالج الاسترداد عن طريق وسيلة الدفع الأصلية.
  4. XPay بيبعت webhook لـ refund.created وواحد لـ charge.refunded علشان السيرفر بتاعك يعلّم الطلب على إنه مسترد ويبلّغ العميل.

الـ Balance Transaction الخاص بالـ Refund هو اللي بيأثّر على رصيدك في XPay. والقيمة بإشارتها (سالبة للمبالغ المستردة) هي اللي بتتسوّى مقابل تسوياتك.

ابنيها

1. أنشئ الاسترداد

الحقل الوحيد المطلوب على POST /refunds هو واحد من paymentIntentId أو chargeId. سيب amount علشان ترد كامل المبلغ المتبقّي القابل للاسترداد.

curl -X POST https://api.xpay.app/refunds \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "paymentIntentId": "pi_test_xyz789",
    "amount": 50000,
    "reason": "requested_by_customer",
    "metadata": { "order_id": "ord_42" }
  }'
const res = await fetch("https://api.xpay.app/refunds", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.XPAY_SECRET_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    paymentIntentId: "pi_test_xyz789",
    amount: 50000, // 500.00 EGP, in minor units
    reason: "requested_by_customer",
    metadata: { order_id: "ord_42" },
  }),
});

const refund = await res.json();
import os, requests

res = requests.post(
    "https://api.xpay.app/refunds",
    headers={
        "Authorization": f"Bearer {os.environ['XPAY_SECRET_KEY']}",
        "Content-Type": "application/json",
    },
    json={
        "paymentIntentId": "pi_test_xyz789",
        "amount": 50000,
        "reason": "requested_by_customer",
        "metadata": {"order_id": "ord_42"},
    },
    timeout=10,
)

refund = res.json()

2. استرداد charge معيّن (بديل)

لو بتشتغل أصلًا على مستوى الـ Charge (مثلًا، بتبني تنفيذ الطلب على webhooks charge.succeeded)، مرّر chargeId بدالها. نفس المحتوى في الباقي؛ وchargeId وpaymentIntentId بيستبعدوا بعض.

curl -X POST https://api.xpay.app/refunds \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "chargeId": "ch_test_AbC123",
    "amount": 50000
  }'

لما paymentIntentId يترفض بخطأ "صفر أو أكتر من charge ناجح"، ارجع لـ chargeId علشان تستهدف الـ charge المعيّن اللي عايز ترده.

3. اقرا الرد

{
  "id": "re_test_AbC123",
  "object": "refund",
  "amount": 50000,
  "currency": "EGP",
  "chargeId": "ch_test_AbC123",
  "paymentIntentId": "pi_test_xyz789",
  "status": "succeeded",
  "reason": "requested_by_customer",
  "balanceTransactionId": "txn_test_def456",
  "createdAt": "2026-05-01T12:00:00.000Z"
}

كل استرداد بتعمله عن طريق API التاجر النهارده بيحسم لـ status: "succeeded" بشكل متزامن. مخطط كائن الـ RefundAPI بيحجز قيم إضافية (pending، requires_action، failed) للاستخدام مستقبلًا؛ مش هتشوفها في الردود النهارده.

الرد بيحمل chargeId وpaymentIntentId الاتنين علشان تقدر توصّل الاسترداد لأي جنب من نموذج بياناتك. والـ balanceTransactionId بيشاور على صف السجل اللي خصم من رصيدك المتاح. وللمبالغ المستردة الجزئية، كذا كائن Refund بيتراكموا مقابل Charge واحد؛ كل واحد ليه txn_* خاص بيه.

الاختيار بين paymentIntentId وchargeId

الحقلين بيستبعدوا بعض: قدّم واحد بالظبط.

  • استخدم paymentIntentId افتراضيًا. الـ pi_* هو مقبض المعاملة لدفعة ناجحة واحدة؛ والسيرفر بيحسمه للـ charge الوحيد الناجح والمتحصَّل على الـ intent ده. ده المسار الصح لكل عملية دمج بتعوز ببساطة ترد "الدفعة".
  • استخدم chargeId لما يبقى فيه التباس. الـ paymentIntentId بيترفض لما الـ Payment Intent يبقى فيه صفر أو أكتر من charge ناجح. ده ممكن يحصل في تدفقات حدّية ممكن تبنيها بعدين (تحصيل متعدد، أو تحصيلات جزئية عبر charges منفصلة). الـ chargeId بيستهدف دايمًا charge معيّن.

اعمل حسابك لـ chargeId بس لما تحتاج مستوى التحديد ده. في الحالة المعتادة "العميل دفع مرة، وعايز تردّله"، الـ paymentIntentId أقصر، وأوضح، وبيستخدم المعرّف اللي بتخزّنه أصلًا.

الاسترداد الجزئي مقابل الكامل

الـ amount بالوحدات الصغرى، زي كل حاجة تانية (50000 يعني 500.00 EGP).

  • سيب amount علشان ترد أي حاجة لسه قابلة للاسترداد على الـ Charge. في أول استرداد ده بيبقى إجمالي الـ charge. وفي الاستردادات اللي بعديها بيبقى اللي فاضل بعد المبالغ المستردة الجزئية السابقة.
  • ابعت amount علشان ترد جزء معيّن. لازم يكون وحدة صغرى واحدة على الأقل وبحد أقصى المبلغ المتبقّي القابل للاسترداد.
  • تقدر تعمل كذا استرداد جزئي مقابل نفس الـ Charge طول ما الإجمالي ما يتعداش المبلغ الأصلي. كل استدعاء بيعمل re_* خاص بيه وBalance Transaction خاص بيه.
  • بمجرد ما المبلغ المسترد التراكمي يساوي إجمالي الـ Charge، محاولات الاسترداد اللي بعد كده بترجّع خطأ.

السبب والبيانات الوصفية

الـ reason هو كود قصير معدود بيوصف ليه رددت. مفيد للتحليلات والدعم. القيم الشائعة:

القيمةإمتى تستخدمها
requested_by_customerالعميل طلبه. الافتراضي بتاعك للمبالغ المستردة بالخدمة الذاتية.
duplicateالعميل اتخصم منه مرتين لنفس الطلب.
fraudulentالـ charge اتعمل عليه نزاع كاحتيال وأنت بترد بشكل استباقي.
expired_uncaptured_chargecharge للتفويض بس انتهى من غير تحصيل. نادرًا بيتعرض يدويًا.

الـ description بياخد لحد 500 حرف كملاحظة حرة على الـ Refund. بتتخزّن على الكائن وبتظهر في الخط الزمني للاسترداد في لوحة التحكم، مفيدة لسجلاتك أنت ولموظفي الدعم اللي بيراجعوا الاسترداد بعدين.

الـ metadata هي حقيبة مفاتيح وقيم خاصة بيك، XPay مش بيفهمها، مفيدة للربط المرجعي مع نظام الطلبات بتاعك.

استمع لأحداث الاسترداد

للمبالغ المستردة المعمولة من لوحة التحكم، أو علشان تتأكد إن الاستردادات غير المتزامنة اكتملت، استمع لـ webhooks الاسترداد. حدثين بيتطلقوا على كل استرداد ناجح:

الحدثإمتى
refund.createdكائن Refund جديد اتعمل. بيتطلق للمبالغ المستردة من الـ API ومن لوحة التحكم.
charge.refundedالـ Charge اتطبّق عليه استرداد. مفيد لما تبني تنفيذ الطلب على الـ Charges.

الـ data.object على refund.created بيبقى RefundAPI كامل، مطابق لـ GET /refunds/:id. وعلى charge.refunded بيبقى ChargeAPI كامل مع الـ Refund الجديد متضمَّن جوّه refunds[].

علشان وصفة التحقق من التوقيع والحماية من إعادة الإرسال، شوف Webhooks ← التحقق من التوقيعات.

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

  • ابعت Idempotency-Key على POST /refunds. الإعادة بعد مهلة شبكة منتهية بترجّع وقتها الاسترداد الأصلي بدل ما تعمل واحد تاني. شوف عدم التكرار.
  • تنفيذ بلا تكرار. تتبّع أنهي معرّفات Refund اتعاملت معاها قبل كده في قاعدة بياناتك. الـ refund.created ممكن يتسلّم تاني.
  • ما تعملش الإصدار من التطبيق اللي بيواجه العميل. المبالغ المستردة بتستخدم مفتاح سري (sk_test_* / sk_live_*). أصدرها من السيرفر بتاعك، مش من كود الواجهة.
  • قرّر مين يقدر يعمل المبالغ المستردة. حدّد صلاحيات الاسترداد على مفتاح الـ API اللي بتستخدمه في مسار الكود ده. وللمبالغ المستردة من لوحة التحكم، قيّد صلاحية Refunds على صلاحيات الفريق.
  • اضبط التوقعات بخصوص فترة البنك. الاسترداد الناجح بيأثّر على سجل XPay على طول، بس المبالغ المستردة على البطاقة بتاخد من 7 لـ 14 يوم علشان تظهر في كشف حساب العميل. قول للعميل في إيميل الاسترداد بتاعك علشان ما يفتكرش إنه فشل.
  • سوّي مقابل الـ Balance Transactions. الـ balanceTransactionId على كل استرداد هو اللي خصم من رصيدك. استخدمه للمحاسبة، مش الـ amount بتاع الـ Refund لوحده.

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

في الصفحة دي