المبالغ المستردة
اعكس دفعة ناجحة بالكامل أو جزئيًا. 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.
إزاي بيشتغل
- السيرفر بتاعك معاه الـ
pi_*بتاع الدفعة، اللي مسكته من webhookcheckout.session.completed(أو قريته عند الطلب منGET /checkout/sessions/:id). - السيرفر بتاعك بينادي
POST /refundsبالـpaymentIntentId. واختياريًا ضيفamount(للمبالغ المستردة الجزئية)، أوreason، أوdescription، أوmetadata. - XPay بيعالج الاسترداد عن طريق وسيلة الدفع الأصلية.
- 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_charge | charge للتفويض بس انتهى من غير تحصيل. نادرًا بيتعرض يدويًا. |
الـ 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 لوحده.
رايح فين بعد كده
الإعدادات المتقدّمة
نص زر الإرسال، واللغة، والعلامة التجارية، وقيود طرق الدفع، وأكواد الترويج، والرسوم، وانتهاء الصلاحية، والبيانات الوصفية. كل اللي تقدر تظبطه تاني على جلسة الدفع.
روابط الدفع (من غير كود)
استخدم الرابط اللي التاجر أنشأه في لوحة التحكم. ضمّنه في موقع، حطّه في إيميل، اطبع كود QR، أو استنى أحداث الدفع بـ webhook.