يصل استرداد المبالغ في Google Play بصمت. تُعكس عملية الدفع، ويستمر العميل في استخدام التطبيق، ولا يتغيّر شيء لديك ما لم تتحقق من ذلك عمداً. والتحقق هو بالضبط ما صُمّمت من أجله Voided Purchases API. فهي تُعيد الطلبات التي أُلغيت أو استُردت أموالها أو جرى الاعتراض عليها مصرفياً، مما يتيح لك سحب الوصول عن كل ما توقف العميل عن الدفع مقابله. اربط مهمة مجدولة بها، واقرأ ما تُعيده، ثم ألغِ الاستحقاق. هذه هي الآلية بأكملها.
هناك أمر واحد يوقع فيه معظم الفرق تقريباً، وهو أمر يتعلق بالسياسة لا بالكود. فهذه الـ API لا تسرد سوى الطلبات التي أُلغيت (revoked) فعلياً. إذا استردت ثمن عملية شراء في Play Console دون تحديد خيار الإلغاء (revoke)، فلن يظهر ذلك الطلب هنا أبداً، فتعمل مهمتك بشكل نظيف تماماً بينما يمضي العميل الذي استُرد له المبلغ محتفظاً بكل ما بعته له. وفيما يلي شرح الـ API حقلاً حقلاً، والحدود التي تقيّده، والمكان الذي تتسرب منه الأموال بصمت حين لا يكون موصولاً.
أهم النقاط
تسرد Voided Purchases API، التي يُصل إليها عبر طريقة purchases.voidedpurchases.list، الطلبات التي ألغتها Google Play أو استردت أموالها أو جرى الاعتراض عليها مصرفياً، وهذا ما يتيح لك بناء نظام يسحب الوصول عن المشتريات التي لم يعد العميل يملكها.
لا تعرض إلا الطلبات المُلغاة (revoked). فأي استرداد يصدره المطوّر دون تفعيل الإلغاء يظل غير مرئي هنا، لذا فإن سحب الوصول يعني استرداد المبلغ مع تفعيل خيار الإلغاء.
نطاقها هو 30 يوماً متجددة. وبما أن startTime لا يمكن أن يعود إلى أبعد من 30 يوماً، فإن أي خادم يظل معطلاً لأكثر من شهر يفقد تلك الإلغاءات بشكل دائم، وهذا هو سبب ضرورة تشغيل الاستقصاء (polling) وفق جدول زمني.
يحدد voidedSource الجهة التي تسببت في الإلغاء: 0 للمستخدم، 1 للمطوّر، 2 لـ Google. ويوضح voidedReason السبب، بدءاً من 0 لـ Other وصولاً إلى 7 لـ Chargeback و8 لـ Unacknowledged_purchase.
تُطلق الإشعارات اللحظية للمطورين (real-time developer notifications) رسالة VoidedPurchaseNotification فور إلغاء عملية الشراء، لكن تعامل معها على أنها تنبيه فقط. تحقق من الأمر عبر Voided Purchases API قبل إلغاء أي شيء.
ميّز بين تجديدات الاشتراك باستخدام orderId، لا purchaseToken أبداً. فرمز purchaseToken واحد يمتد ليشمل كل تجديدات الاشتراك، لذا لا يستطيع الرمز وحده الفصل بين فترة وأخرى.
الحدود القصوى هي 6,000 استعلام في اليوم و30 استعلاماً خلال أي فترة مدتها 30 ثانية، لذا قيّد كل طلب بنافذة زمنية وتصفّح خلالها باستخدام رمز الاستمرار (continuation token)، لا طلباً واحداً لكل طلب شراء.
ما الذي تُعيده الـ API
تجيب نقطة النهاية (endpoint) عن سؤال واحد: أي من طلبات هذا التطبيق أُلغي مؤخراً. ويجمع الإلغاء (void) بين ثلاث نتائج تعيد جميعها أموال العميل، وهي الإلغاء (cancellation)، والاسترداد (refund)، والاعتراض المصرفي (chargeback). وتصل إلى كل من منتجات الشراء داخل التطبيق لمرة واحدة والاشتراكات، ويحدد معامل واحد نطاق ذلك. إذا تركت type عند القيمة الافتراضية 0، فلن تعود إليك سوى مشتريات المنتجات داخل التطبيق الملغاة. وإذا ضبطت type على 1، فستحصل على مشتريات داخل التطبيق واشتراكات ملغاة معاً.
كل عنصر في الاستجابة هو كائن عملية شراء ملغاة (voided purchase object) يحمل مجموعة قصيرة وعالية القيمة من الحقول:
orderId يحدد بشكل فريد عملية شراء لمرة واحدة، أو اشتراكاً، أو تجديداً فردياً واحداً ضمنه. تعامل معه على أنه مفتاح الربط (join key) الخاص بك.
purchaseToken يحدد عملية شراء لمرة واحدة أو اشتراكاً، لكنه لا يفصل بين التجديدات، لذا اعتمد على orderId في تلك الحالات.
purchaseTimeMillis يسجل وقت حدوث عملية الشراء، بالميلي ثانية منذ الحقبة (epoch).
voidedTimeMillis يسجل وقت إلغائها أو استرداد أموالها أو الاعتراض عليها مصرفياً، أيضاً بالميلي ثانية منذ الحقبة.
voidedSource يحدد من بدأ عملية الإلغاء، حيث 0 يعني المستخدم، و1 يعني المطوّر، و2 يعني Google.
voidedReason يوضح السبب كعدد صحيح يتراوح من 0 إلى 8.
voidedQuantity يحمل العدد الملغى من استرداد جزئي قائم على الكمية، ولا يظهر إلا عندما تكون قيمة includeQuantityBasedPartialRefund هي true.
وبما أن رمز purchaseToken واحد يغطي اشتراكاً كاملاً بينما يحصل كل تجديد على orderId جديد تماماً، فإن اعتماد الرمز مفتاحاً لاستحقاقاتك سيجعلك تُلغي الفترة الخاطئة. اعتمد بدلاً من ذلك على orderId كمفتاح.
اقرأ voidedReason قبل أن تلمس أي شيء
إن voidedReason هو ما يحوّل قائمة بسيطة إلى قرار فعلي، لأن الاسترداد الناتج عن تغيير الرأي والاعتراض المصرفي البنكي يتشاركان التغذية (feed) نفسها رغم أنهما لا يتشابهان إطلاقاً. وفيما يلي المجموعة الكاملة:
1. Other لا يحمل فئة مخصصة. ألغِه وتابع عملك.
2. Remorse هو عميل غيّر رأيه، وهو استرداد اعتيادي يحدث يومياً.
3. Not_received هو ادعاء بأن المنتج لم يصل إطلاقاً، وهو أمر يستحق النظر في عملية التسليم لديك.
4. Defective يعني أن المنتج لم يعمل، وهو مؤشر جودة ينبغي أن تسجّله.
5. Accidental_purchase هو شراء غير مقصود، يحدث غالباً على جهاز مشترك.
6. Fraud هو معاملة صنّفتها Google على أنها احتيالية.
7. Friendly_fraud هو اعتراض مصرفي (chargeback) يعترض فيه حامل البطاقة الحقيقي على عملية دفع قام بها فعلاً.
8. Chargeback هو قيام بنك العميل بعكس عملية الدفع، وهو أمر نهائي لدى البنك ويُحمَّل الآن عليك.
9. Unacknowledged_purchase هو قيام Google تلقائياً باسترداد قيمة عملية شراء لم يُقرّها تطبيقك مطلقاً.
السبب 9 هو استرداد تسببت فيه بنفسك. فأي عملية شراء يتركها تطبيقك دون إقرار تُعيد Google أموالها، لذا فإن ظهور unacknowledged_purchase في هذه التغذية يعني إيراداً تخليت عنه بسبب استدعاء واحد فاتك تنفيذه لا بسبب أي اختيار قام به المشتري.
نافذة الثلاثين يوماً التي تُفرغ القائمة بصمت
لا تعود الـ API إلى أبعد من الأيام الثلاثين الماضية. فقيمة startTime الافتراضية هي الوقت الحالي ناقص 30 يوماً، ولا يمكن ضبطها إلى ما قبل ذلك، أما endTime فقيمتها الافتراضية هي الوقت الحاضر، لذا فإن ما تحصل عليه هو نافذة شهرية متجددة لا أرشيف دائم.
والنتيجة صارخة. فإذا تعطلت مهمة الاستقصاء (polling) دون أن يلاحظ ذلك أحد لمدة خمسة أسابيع، تكون إلغاءات الأسبوع الأول قد سقطت بالفعل من الـ API، ولا يستطيع أي استدعاء استرجاعها. تبقى تلك الطلبات دون إلغاء، وقد لا تعلم حتى أنها حدثت ما لم تكن قد التقطتها في مكان آخر. فالفجوة في الشبكة تكون بالضبط بحجم أطول فترة انقطاع لديك. استقصِ يومياً على الأقل وطابق النتائج مع الإشعارات اللحظية، وتعامل مع الثلاثين يوماً على أنها فعلاً ساعة حذف صارمة.
ظهور الطلب من عدمه يعتمد كلياً على الإلغاء (revoke)
هذا هو السبب الأول الذي يجعل الفرق تصف الـ API بأنها معطلة. فهي تُعيد الطلبات المُلغاة فقط ولا شيء غيرها. فالاستردادات التي يبدأها المستخدمون، وعمليات الإلغاء، والاعتراضات المصرفية، والاستردادات التي تبادر بها Google، كلها تُلغى تلقائياً، لذا تظهر دائماً في التغذية. أما الاستردادات التي يبادر بها المطوّر فهي الاستثناء. فإصدار الاسترداد بنفسك، سواء من Play Console أو عبر Orders API، يترك خيار الإلغاء قراراً منفصلاً عليك اتخاذه. إذا لم تختره، تسوى المعاملة مع العميل لكنها لا تظهر أبداً في هذه التغذية.
والخلاصة قصيرة. عندما يكون هدفك قطع الوصول، استرد المبلغ مع تفعيل الإلغاء. تجاوز ذلك وستكون قد أعدت المال بينما تركت الباب مفتوحاً، ولن تجد مهمة الإلغاء لديك، مهما كانت مُصممة بعناية، ما تعمل عليه.
استقصاؤها دون تجاوز الحصة (quota)
نقطة النهاية هذه محدودة المعدل (rate limited)، والحدود القصوى منخفضة بما يكفي لأن تتجاوزها حلقة غير حذرة. تحصل على 6,000 استعلام في اليوم، تُحسب بتوقيت المحيط الهادئ (Pacific Time)، ولا يتجاوز أبداً 30 استعلاماً في أي فترة مدتها 30 ثانية. تناسب هذه الميزانية الاستقصاء القائم على النوافذ الزمنية، وتعاقب أي تصميم يعتمد على استدعاء واحد لكل طلب.
النوافذ الزمنية ورمز الاستمرار (continuation token)
قيمة maxResults الافتراضية هي 1,000، وهذا أيضاً هو الحد الأقصى الذي يمكن بلوغه. وإذا احتوت نافذة ما على إلغاءات أكثر مما يمكن أن تحمله صفحة واحدة، تأتي الاستجابة مصحوبة بكائن tokenPagination يحمل nextPageToken. أعد إرسال ذلك الرمز في استدعائك التالي للتقدّم عبر الصفحات. حدد طرفي النافذة باستخدام startTime وendTime، واستمر في التصفح حتى ينفد الرمز، وعندئذ فقط انقل النافذة إلى الأمام. تبقي هذه الوتيرة بمنأى عن كل من سقف الاندفاع (burst) لمدة 30 ثانية والحصة اليومية.
الإشعارات اللحظية تسدّ الفجوة اليومية
حتى الاستقصاء اليومي يتركك أعمى لمدة تصل إلى 24 ساعة، والفجوات الطويلة هي بالضبط ما تعاقب عليه نافذة الثلاثين يوماً. تسدّ إشعارات المطورين اللحظية (real-time developer notifications) تلك الفجوة الزمنية. ففور إلغاء عملية الشراء، يُنشر VoidedPurchaseNotification إلى موضوع Cloud Pub/Sub تحت سيطرتك، ويستهلكه الخادم الخلفي (backend) لديك خلال ثوانٍ. وتبقى حمولته (payload) مضغوطة:
purchaseToken هو الرمز الخاص بعملية الشراء الأصلية.
orderId هو معرّف المعاملة الملغاة، يُنشأ من جديد مع كل تجديد للاشتراك.
productType يساوي 1 للاشتراك و2 لعملية الشراء لمرة واحدة.
refundType يميّز بين الاسترداد الكامل، ويُشار إليه بـ 1، والاسترداد الجزئي القائم على الكمية، ويُشار إليه بـ 2.
تعامل مع الإشعار على أنه تنبيه لا حقيقة مطلقة. فبمجرد وصول VoidedPurchaseNotification، استعلم من Voided Purchases API للتحقق من الوضع الفعلي للطلب، ولا تُلغِ إلا بعد ذلك. فالتنبيه يخبرك بأن تتحقق؛ والـ API تخبرك بالحقيقة.
ما تكلفه من المال
الـ API هي البنية التحتية (plumbing)، لكن سبب مد هذا الأنبوب هو فاتورة، ورقمان فيها آخذان في التصاعد.
ابتداءً من 3 أغسطس 2026، فاتورة الاعتراض المصرفي (chargeback) عليك أنت
اعتباراً من 3 أغسطس 2026، تنقل Google تكلفة الاعتراض المصرفي إلى المطوّر. فتخسر سعر الشراء وتتحمل رسوم الاعتراض المصرفي التي يفرضها البنك فوق ذلك. ولم يعد voidedReason بقيمة 7 مجرد عملية بيع خاسرة، بل أصبح بنداً في الفاتورة برسوم ملحقة به. لا يمكن عكس الاعتراض المصرفي نفسه، لأنه نهائي لدى البنك، لكن رصد الإلغاء بسرعة يتيح لك إلغاء الاستحقاق، وبالنسبة لأي شيء لا يزال قيد التسليم، وقف الإنفاق على عميل استُرد له المبلغ ثم جرى عكسه.
أنت تستمر في تمويل عميل استُرد له المبلغ بالفعل
يختفي سعر البيع بمجرد حدوث الإلغاء. أما ما لا يزال بيدك فهو تكلفة الاستمرار في التسليم. فمع كل ساعة يبقى فيها استحقاق مسترد نشطاً، تستمر الفواتير التي توقف العميل عن تغطيتها في الوصول إلى جانبك: تكلفة الحوسبة، ومكالمات مزوّد النموذج، والتخزين، وأي دفعة لمُبدع أو شريك يُحفّزها نشاطه. وبناء خط الإلغاء (revocation pipeline) على هذه الـ API هو ما يوقف ذلك الإنفاق. تجاوز ذلك وستكون تموّل المنتج لأناس سبق للمتجر أن عوّضهم بالفعل.
الاحتيال الودّي (friendly fraud) اتجاه لا حادثة معزولة
نادراً ما يظهر voidedReason بقيمة 5 أو 6 بمفرده. فالاحتيال والاحتيال الودّي يتجمعان حول حسابات وأجهزة معينة، وأحياناً حول عروض ترويجية بعينها. وبما أن الـ API تُرفق voidedSource وvoidedReason بكل عملية إلغاء، فلديك ما يكفي لرصد اتجاه إساءة الاستخدام لكل حساب بدلاً من استيعاب كل عملية عكس كخسارة منفردة. فالحساب الذي يجري عليه اعتراض مصرفي للمرة الثانية يخبرك بما لم يخبرك به الاسترداد الأول.
تجميع كل ذلك
النموذج بأكمله صغير بمجرد أن تكون القطع في متناول يدك. أصغِ إلى VoidedPurchaseNotification في الوقت الفعلي حتى لا ينتظر شيء يوماً كاملاً. تعامل مع Voided Purchases API باعتبارها مصدر الحقيقة، معتمداً على orderId كمفتاح حتى لا تختلط التجديدات ببعضها أبداً. اقرأ voidedSource وvoidedReason حتى يُعالَج الاعتراض المصرفي بمعزل عن استرداد ناتج عن تغيير الرأي. استقصِ بوتيرة ضيقة بما يكفي لئلا تؤثر نافذة الثلاثين يوماً أبداً، واسترد المبلغ مع تفعيل الإلغاء في أي وقت تنوي فيه قطع الوصول.
هذه هي الطبقة التي تُشغّلها Refund Sensor نيابة عنك. فهي تستهلك الإشعارات اللحظية، وتطابق كل عملية إلغاء مع الـ API، وتُلغي الطلب المحدد بدقة بدلاً من المنتج بأكمله، وتفصل الاعتراض المصرفي البنكي عن الاسترداد العادي بحيث تظهر الحالات المكلفة بدلاً من أن تختفي. فتحصل على سحب للوصول خلال ثوانٍ وسجل كامل بمن ألغى وماذا وسبب ذلك، دون الحاجة إلى إنشاء خط أنابيب Pub/Sub أو مهمة استقصاء بنفسك.
أين تم توثيق هذه القواعد
Google Play Developer API: طريقة purchases.voidedpurchases.list
Google Play Developer API: مورد purchases.voidedpurchases، ويغطي voidedSource وvoidedReason
Android Developers: مرجع إشعارات المطورين اللحظية الخاص بـ VoidedPurchaseNotification
Android Developers: مكافحة الاحتيال وإساءة الاستخدام باستخدام Play Billing
الأسئلة الشائعة
لأن هذه التغذية لا تحمل سوى الطلبات المُلغاة (revoked). فالاستردادات التي يبدأها المستخدم، إلى جانب عمليات الإلغاء، والاعتراضات المصرفية، والاستردادات التي تبادر بها Google، تُلغى جميعها من تلقاء نفسها وتظهر دائماً. أما الاسترداد الذي تُصدره بنفسك فلا يظهر إلا إذا اخترت أيضاً إلغاءه. استرد المبلغ دون إلغاء وستُسوى المعاملة لكنها ستبقى غير مرئية هنا، لذا فعّل خيار الإلغاء كلما كان هدفك سحب الوصول.
ثلاثون يوماً، ولا أبعد من ذلك. فقيمة startTime الافتراضية هي الوقت الحالي ناقص 30 يوماً، وترفض أي قيمة أقدم من ذلك، لذا تتصرف نقطة النهاية كنافذة شهرية متجددة بدلاً من أرشيف. وبمجرد أن يتجاوز عمر الإلغاء 30 يوماً، يختفي دون أي طريقة لاسترجاعه، وهذا بالضبط سبب ضرورة الاستقصاء وفق جدول زمني ودعمه بالإشعارات اللحظية.
لكل منهما دور. يصلك VoidedPurchaseNotification خلال ثوانٍ ويخبرك بضرورة التحقق، لكن توجيهات Google تنص على التعامل معه كإشارة لا كحقيقة. تحقق من الحالة الراهنة عبر Voided Purchases API، ثم ألغِ الوصول. فالإشعار يقضي على التأخير؛ والـ API توفر voidedSource وvoidedReason الموثوقين اللذين تتصرف بناءً عليهما فعلياً.
انظر إلى voidedReason. القيمة 7 تعني اعتراضاً مصرفياً حيث عكس بنك العميل عملية الدفع، والقيمة 6 تعني احتيالاً ودياً. أما القيمة 1 فهي استرداد عادي ناتج عن تغيير الرأي. وهذا التمييز مهم لأنه، ابتداءً من 3 أغسطس 2026، ستنقل Google سعر الاعتراض المصرفي ورسوم البنك إلى المطوّر، لذا فإن القيمة 7 تكلفك أكثر من استرداد عادي.
نعم. اضبط معامل type على القيمة 1 لتحصل على مشتريات داخل التطبيق ملغاة إلى جانب مشتريات اشتراك ملغاة؛ أما القيمة الافتراضية 0 فتُعيد منتجات داخل التطبيق فقط. وبالنسبة للاشتراكات، حدد الفترة الملغاة بدقة عبر orderId، لأن رمز purchaseToken واحداً يمتد ليشمل كل التجديدات بينما تحصل كل معاملة تجديد على orderId خاص بها.






