Le transfert d'argent et la fin de l'accès sont deux événements distincts sur les deux plateformes, et un seul des deux se produit automatiquement. Google relègue la suppression du droit d'accès derrière une option désactivée par défaut. Apple gère les licences, mais ne peut pas toucher à un consommable que quelqu'un a déjà dépensé. Tout ce que votre serveur ne nettoie pas continue de fonctionner sur un revenu que vous n'avez plus.
Points clés
Rembourser une commande Play laisse l'accès du client intact, sauf indication contraire de votre part. Soit la case « Remove entitlement » est cochée dans Play Console, soit votre appel API comporte revoke=true. Aucun des deux ne se produit automatiquement.
Les remboursements effectués sans cette option revoke sont totalement absents de la Voided Purchases API. Toute tâche de révocation basée uniquement sur ce flux les ignorera tous, silencieusement.
Apple peut révoquer pour vous les licences des non-consommables et des abonnements. Les consommables sont considérés comme livrés dès qu'ils sont dépensés, ce qui ne laisse rien à révoquer pour StoreKit : déduire le solde revient donc à votre propre serveur.
Apple annule parfois ses propres remboursements. Une notification REFUND_REVERSED signifie qu'un remboursement accordé a été annulé, revocationDate repasse à null, et tout ce que vous avez retiré doit être rétabli.
Neuf valeurs distinctes peuplent le champ voidedReason de Google, allant de remorse et accidental_purchase jusqu'à fraud, friendly_fraud et unacknowledged_purchase : un seul flux vous indique donc à la fois quand couper l'accès et quels cas ont une odeur d'abus.
Tant qu'un achat remboursé conserve son accès, vous financez son calcul, son stockage et ses appels API avec un revenu déjà reversé à l'acheteur.
Pourquoi l'accès survit à un remboursement par défaut
Les deux entreprises traitent le paiement et le droit d'accès comme des opérations distinctes. Le paiement suit la politique du store, et une fois approuvé, le remboursement est réglé et le registre le reflète. Le droit d'accès est une autre affaire, parfois optionnelle, parfois à votre charge, souvent les deux selon la plateforme et le type de produit. Aucun des deux stores ne présume que rendre l'argent signifie mettre fin à l'accès, car de nombreuses situations légitimes, un remboursement partiel, un crédit de bonne volonté, un geste du support, sont censées laisser le client en possession de ce qu'il a acheté.
Google Play : la case à cocher qui passe inaperçue
Play Console scinde le remboursement en deux décisions. D'abord le montant, puis, tout à fait séparément, le fait de cocher ou non « Remove entitlement ». La plupart des agents du support n'y touchent pas, ce qui est raisonnable puisque la plupart des demandes de remboursement relèvent réellement de la bonne volonté et que le client est censé garder le produit. Ce que l'écran de confirmation ne mentionne jamais, c'est que cette même case non cochée est la raison pour laquelle quelqu'un repart en conservant un abonnement ou un article intégré après un remboursement que vous vouliez définitif.
Le même piège existe dans l'API
Le code présente la même bifurcation. L'endpoint orders.refund possède un paramètre de requête optionnel, revoke, et le définir sur true met fin immédiatement à l'accès à l'article, annulant au passage tout paiement futur lorsqu'il s'agit d'un abonnement récurrent. Omettez ce paramètre et c'est false que vous obtenez par défaut : le remboursement est enregistré, l'enregistrement de l'achat change, et le client continue exactement comme avant.
Remboursement dans Play Console avec « Remove entitlement » laissé décoché : l'accès se poursuit. La commande n'atteint jamais non plus la Voided Purchases API.
Remboursement dans Play Console avec « Remove entitlement » cochée : l'accès se termine immédiatement. La commande apparaît bien dans la Voided Purchases API.
Appel à orders.refund en omettant le paramètre revoke, qui vaut donc false par défaut : l'accès se poursuit. Là encore, rien n'atteint la Voided Purchases API.
Appel à orders.refund avec revoke=true : l'accès se termine immédiatement, et la commande apparaît dans la Voided Purchases API.
Une tâche de révocation qui ne lit que la Voided Purchases API ne rencontrera jamais un remboursement émis sans le flag revoke. L'enregistrement n'est pas en retard. Il n'arrivera simplement jamais, et la commande reste en dehors de ce flux, comme si le remboursement n'avait jamais eu lieu.
C'est cette dernière ligne qui casse les intégrations. Construire un pipeline de révocation sans surveiller manuellement Play Console est précisément ce à quoi sert la Voided Purchases API, mais son périmètre se limite aux commandes réellement annulées avec révocation. Les remboursements traités sans la case cochée, ou sans revoke=true, y sont invisibles par conception, et non à cause d'un défaut.
Comprendre voidedReason
Les achats annulés arrivent avec un voidedReason expliquant la cause, parmi neuf valeurs possibles : other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback et unacknowledged_purchase. Le traiter comme une simple donnée d'archivage, c'est le gaspiller. Remorse et accidental_purchase décrivent des remboursements sans particularité. Fraud, friendly_fraud et chargeback méritent une file de revue anti-fraude plutôt qu'un revoke-and-close automatique, car les comptes produisant ces valeurs récidivent à un taux statistiquement plus élevé.
Quiconque développe directement contre ce flux a besoin d'un chiffre opérationnel : purchases.voidedpurchases.list autorise 6,000 requêtes par jour et 30 sur une fenêtre de 30 secondes. Des boucles de polling trop serrées épuisent rapidement ce quota. Des appels planifiés utilisant des jetons de pagination correspondent à la conception prévue.
Apple : les licences peuvent être retirées, pas les consommables
App Store Server Notifications V2 délivre une notification REFUND dès qu'un remboursement est réglé, couvrant sans distinction les consommables, les non-consommables, les abonnements à renouvellement automatique et les abonnements sans renouvellement. Ce qui doit se passer ensuite diverge complètement selon le type de produit, car seuls deux de ces quatre types relèvent du mécanisme de révocation propre à StoreKit.
Pourquoi les consommables en sont exclus
Les droits d'accès permanents appartiennent aux non-consommables et aux abonnements, et les systèmes d'Apple peuvent les désactiver. Les consommables n'ont rien de tel. Dépenser un consommable le marque comme livré, ce qui ne laisse rien à récupérer pour StoreKit. Un client qui a acheté 500 pièces, les a dépensées, puis obtenu un remboursement a, du point de vue d'Apple, déjà tout reçu. Votre registre est le seul où ces pièces existent encore. Cette notification REFUND est votre seul signal pour retrouver le transaction ID original correspondant et soustraire vous-même le solde, car rien du côté d'Apple ne le fera.
REFUND signifie qu'un achat consommable, non-consommable, à renouvellement automatique ou sans renouvellement a été remboursé. Retirez immédiatement l'accès aux non-consommables et abonnements, et soustrayez vous-même tout solde de consommable.
REFUND_DECLINED signifie qu'Apple a rejeté la demande de remboursement du client. Il n'y a rien à faire, puisque l'accès n'a jamais été menacé.
REFUND_REVERSED signifie qu'un remboursement déjà accordé par Apple a été annulé. Rétablissez tout ce que la notification REFUND initiale vous avait conduit à supprimer.
CONSUMPTION_REQUEST signifie qu'Apple veut vos preuves avant de se décider. Répondez dans un délai de 12 heures, et notez que celle-ci arrive avant un REFUND plutôt qu'après.
La notification que presque tout le monde ignore
REFUND_REVERSED est celle que les intégrations omettent le plus souvent, car elle représente Apple annulant sa propre décision, un cas que presque personne ne prévoit au départ. Son arrivée fait repasser revocationDate à null, ce qui signifie qu'Apple considère désormais l'achat comme à nouveau parfaitement valide. Tout ce que votre serveur a fait lors du REFUND initial, supprimer un droit d'accès, annuler un enregistrement d'abonnement, remettre à zéro un solde de consommable, doit être annulé.
Ne vous attendez pas à ce qu'un remboursement annulé ressemble à un nouvel achat, car ce n'est pas le cas. Bannissements appliqués, accès retiré, soldes marqués comme dépensés : REFUND_REVERSED est l'instruction d'annuler précisément ces actions, et aucune notification de suivi n'arrivera plus tard pour vous le rappeler.
Combler l'écart, étape par étape
Faites de revoke=true la valeur par défaut sur toutes les voies de remboursement Play, aussi bien Console qu'API, et réservez l'alternative aux cas de support justifiant spécifiquement que le client garde l'accès.
Interrogez purchases.voidedpurchases.list selon une planification utilisant des jetons de pagination plutôt qu'en bouclant de manière serrée, en restant sous les 6,000 requêtes par jour et 30 par 30 secondes.
Envoyez les valeurs de voidedReason fraud, friendly_fraud et chargeback vers une file de revue plutôt que vers un traitement automatique de type revoke-and-close.
Abonnez-vous à App Store Server Notifications V2 chez Apple, et retirez l'accès aux non-consommables et abonnements dès qu'une notification REFUND les identifie.
Conservez le transaction ID original associé à chaque achat consommable, afin qu'une notification REFUND puisse localiser le solde exact à soustraire sans que quiconque ait à le rechercher manuellement.
Implémentez REFUND_REVERSED délibérément, et enregistrez chaque révocation en l'associant à son transaction ID ou order ID, afin qu'une annulation restaure exactement ce qui a été retiré.
Ce que cet écart coûte réellement
L'équité envers le client n'est pas le sujet ici. Le sujet, c'est ce qui continue de fonctionner une fois le revenu disparu. Un abonnement remboursé qui continue de solliciter votre API, de conserver des données, de consommer du calcul ne pose aucune difficulté morale ; il pose une facture. L'infrastructure a été payée sur un achat dont vous avez déjà rendu le revenu. Aucun des deux stores ne suit cette dépense pour vous, car de leur point de vue, le remboursement s'est conclu à l'instant où l'argent a bougé. Contrôler le volet accès est entièrement à votre charge.
Refund Sensor traite donc la révocation comme faisant partie du pipeline de remboursement plutôt que comme quelque chose de rajouté après coup. Un événement REFUND et le changement d'accès correspondant sont écrits comme une seule action, et un événement REFUND_REVERSED restaure l'état antérieur de lui-même, au lieu d'attendre que quelqu'un remarque qu'un client est bloqué hors de quelque chose qu'Apple lui a déjà rendu des jours plus tôt.
Où ces règles sont documentées
Questions fréquentes
Non. L'accès ne se termine que si la case « Remove entitlement » a été cochée dans Play Console, ou si l'appel orders.refund comportait revoke=true. Comme aucun des deux n'est l'option par défaut, les remboursements sont régulièrement enregistrés alors que les clients continuent d'utiliser le produit.
Une notification REFUND arrive, mais StoreKit ne peut pas révoquer un consommable car le dépenser équivaut à une livraison. Retrouver la transaction d'origine et soustraire le solde est un travail que votre propre serveur doit effectuer.
Cela signifie qu'un remboursement déjà accordé par Apple a été annulé. revocationDate repasse à null et l'achat est à nouveau considéré comme valide. Tout accès ou solde que le REFUND initial vous a amené à supprimer doit être rétabli, et un très grand nombre d'intégrations ne s'abonnent jamais à cette notification.
Parce que cet endpoint ne renvoie que les commandes annulées avec révocation. Les remboursements traités sans l'option revoke n'y figurent pas, si bien que tout pipeline dépendant uniquement de ce flux les ignorera complètement.
Six mille requêtes par jour, et pas plus de 30 sur une fenêtre de 30 secondes. Cet endpoint est conçu pour des appels planifiés utilisant des jetons de pagination, plutôt que pour un polling continu.



