Tous les articles
Deep dive8 min de lecture

Apple comme Google peuvent livrer le même remboursement à votre serveur plus d'une fois, et les notifications de remboursement en double vous coûtent cher si vous agissez sur chacune

Apple réessaie une notification de remboursement jusqu'à cinq fois et Google Play s'appuie sur Pub/Sub avec une livraison au moins une fois, donc le même remboursement peut atteindre votre serveur plus d'une fois. Voici comment gérer les notifications de remboursement en double sans déduire un solde ni consommer un quota d'API deux fois.

De nombreuses enveloppes en papier identiques empilées sur un bureau sombre avec une mise à l'écart, représentant les notifications de remboursement en double qui arrivent sur votre serveur

Points clés

  • Apple réessaie une App Store Server Notification V2 cinq fois, à 1, 12, 24, 48 et 72 heures après la dernière tentative, chaque fois que votre serveur ne répond pas avec un statut HTTP entre 200 et 206. En comptant la première tentative, un remboursement peut arriver jusqu'à six fois.
  • Chaque véritable nouvelle tentative d'Apple porte le même notificationUUID, donc ce champ, et non l'id de transaction, est votre clé de déduplication.
  • Les Real-time Developer Notifications de Google Play s'appuient sur Cloud Pub/Sub, qui garantit une livraison au moins une fois et aucun ordre, donc le même message peut arriver deux fois ou dans le désordre. Google vous demande de vérifier l'unicité du messageId avant de traiter quoi que ce soit.
  • Les notifications périodiques CONSUMPTION_REQUEST d'Apple ne sont pas des nouvelles tentatives. Apple continue d'en envoyer de nouvelles tout au long de la fenêtre de remboursement ouverte, chacune avec un notificationUUID différent, donc dédupliquer sur notificationUUID les conserve correctement toutes.
  • Le même transactionId d'Apple peut porter plus d'une décision, par exemple un REFUND_DECLINED suivi plus tard d'un REFUND, donc dédupliquer sur le seul id de transaction jette un événement distinct dont vous aviez besoin.
  • Rejeter un doublon en renvoyant un 4xx ou un 5xx ne fait que pousser la boutique à le réessayer. Dédupliquez dans votre propre base de données et renvoyez toujours un statut de succès.
  • Un gestionnaire de remboursement qui n'est pas idempotent agit deux fois à la seconde livraison. Il déduit un solde deux fois, annule un versement deux fois, ou consomme un quota facturable de la Play Developer API et de l'App Store Server API en revérifiant un remboursement qu'il a déjà clôturé.

Votre serveur recevra le même événement de remboursement plus d'une fois, et les deux boutiques l'ont conçu ainsi à dessein. Apple réessaie une App Store Server Notification jusqu'à cinq fois lorsque votre endpoint ne répond pas proprement. Google Play livre ses Real-time Developer Notifications via Cloud Pub/Sub, qui promet une livraison au moins une fois et rien sur l'ordre. La question n'est donc jamais de savoir si un doublon arrive. C'est ce que fait votre code la deuxième fois qu'il voit le même remboursement. Trompez-vous là-dessus et vous déduisez un solde deux fois, annulez un versement deux fois, ou consommez un quota d'API facturable en revérifiant un remboursement que vous avez déjà clôturé. Voici comment les notifications de remboursement en double vous parviennent réellement, quelles répétitions sont de vrais doublons et lesquelles ne font qu'en avoir l'air, et comment les gérer pour que la seconde livraison soit gratuite.

Une notification de remboursement est livrée au moins une fois, ce qui n'est pas la même chose qu'exactement une fois

Les deux boutiques traitent une notification livrée comme une promesse qu'elles continuent d'essayer de tenir, non comme un tir unique qu'elles lancent et oublient. C'est bon pour la fiabilité, car une notification que vous manquez pendant un déploiement vous parvient tout de même plus tard. C'est un piège pour l'exactitude, car le mécanisme qui garantit que vous finissez par recevoir l'événement garantit aussi que vous le recevez parfois deux fois. Votre gestionnaire doit être idempotent, c'est-à-dire que la deuxième et la troisième livraison d'un même remboursement ne changent rien que la première n'ait déjà changé.

Apple réessaie cinq fois sur trois jours

Lorsque Apple envoie une App Store Server Notification V2, elle attend que votre serveur réponde avec un statut HTTP dans la plage 200 à 206. Toute autre chose, un 4xx ou un 5xx, indique à Apple que la livraison a échoué, et Apple réessaie. Le calendrier est fixe : cinq nouvelles tentatives, à 1, 12, 24, 48 et 72 heures après la tentative précédente. En comptant la première tentative, un événement de remboursement peut arriver jusqu'à six fois, réparti sur environ une semaine. Chacune de ces nouvelles tentatives porte le même notificationUUID. Ce champ est votre clé de déduplication. Si vous avez déjà enregistré un notificationUUID, la livraison que vous tenez est une répétition, et la réponse correcte est de ne rien stocker de nouveau et de renvoyer tout de même 200.

Google Play s'appuie sur Pub/Sub, qui promet au moins une fois et ne dit rien sur l'ordre

Les Real-time Developer Notifications de Google Play sont publiées sur un sujet Cloud Pub/Sub. La garantie de livraison de Pub/Sub est au moins une fois, et il n'offre aucune garantie d'ordre. Cela signifie que le même message peut être livré à votre endpoint plus d'une fois, et que deux messages pour le même achat peuvent arriver dans le désordre. Les propres recommandations de Google sont explicites : décompressez le champ base64 data, lisez le messageId, et vérifiez que vous ne l'avez pas déjà vu avant de traiter quoi que ce soit. Un messageId en double est une répétition que vous ignorez. Deux notifications différentes au sujet d'un même achat doivent tout de même atterrir sur le même enregistrement, alors indexez aussi votre état stocké sur le purchaseToken, et laissez un événement ultérieur mettre à jour la ligne qu'un événement antérieur a créée.

PlateformeModèle de livraisonDédupliquer surSignal de succèsSi vous n'accusez pas réception
App Store Server Notifications V2Jusqu'à 6 tentatives : la première, plus 5 nouvelles tentatives à 1, 12, 24, 48, 72 heuresnotificationUUIDHTTP 200 à 206Apple réessaie selon le calendrier fixe, puis s'arrête
Google Play RTDN sur Pub/SubAu moins une fois, aucune garantie d'ordrePub/Sub messageId, indexé par entité sur purchaseTokenHTTP 200 au push, ou un ack explicitePub/Sub renvoie à l'expiration du délai d'ack

Les répétitions qui ne sont pas des doublons

Toute notification qui ressemble à une que vous avez déjà vue n'est pas une nouvelle tentative. Deux comportements d'Apple envoient des événements véritablement nouveaux qui partagent un achat mais qui doivent chacun être traités, et les regrouper avec une déduplication naïve jette une information dont vous aviez besoin.

Apple envoie de nouveaux CONSUMPTION_REQUESTs, pas des nouvelles tentatives

Pendant une demande de remboursement ouverte sur un consommable, Apple n'envoie pas un seul CONSUMPTION_REQUEST et attend. Elle en envoie de nouveaux périodiquement tout au long de la fenêtre de remboursement ouverte jusqu'à la clôture du remboursement. Le personnel d'Apple a confirmé qu'il ne s'agit pas de nouvelles tentatives, et l'indice est le champ sur lequel vous dédupliquez : chaque nouveau CONSUMPTION_REQUEST porte un notificationUUID différent. Une déduplication indexée sur notificationUUID fait donc automatiquement ce qu'il faut. Elle regroupe les vraies nouvelles tentatives et conserve chaque invite distincte. Ce que vous ne devez pas faire, c'est dédupliquer sur l'id de transaction et le type de notification, car cela ferait taire chaque CONSUMPTION_REQUEST après le premier et vous coûterait la fenêtre de preuve de 12 heures sur ceux que vous avez laissés tomber.

Une transaction peut porter plus d'une décision

Un seul transactionId peut produire plus d'un résultat de remboursement au cours de sa vie. Apple peut envoyer un REFUND_DECLINED puis, plus tard, un REFUND pour la même transaction, et des développeurs rapportent recevoir trois notifications liées au remboursement ou plus pour un même id de transaction. Chacune est un événement distinct avec son propre notificationUUID. Si votre clé de déduplication est l'id de transaction, la deuxième décision ressemble à un doublon de la première et vous n'apprenez jamais que le remboursement a finalement été accordé. L'id de transaction regroupe les événements. Il ne les identifie pas.

Une pince robotisée soulevant un colis en double d'une ligne de convoyage vers un bac latéral, une image représentant la déduplication de notifications de remboursement répétées

Ce qu'un doublon vous coûte réellement

Une notification de remboursement n'est pas un voyant d'état. Elle déclenche des actions réelles : vous révoquez un accès, vous déduisez un solde de consommable, vous annulez un versement à un créateur, vous appelez l'App Store Server API ou la Play Developer API pour confirmer l'état. Exécutez l'une de ces actions une seconde fois sur un doublon et le coût est réel.

Suivez l'argent. Révoquer un accès deux fois est sans conséquence, car l'accès est déjà parti. Déduire un solde deux fois ne l'est pas : un utilisateur qui a acheté un lot de pièces et s'est fait rembourser peut se retrouver avec un solde négatif que votre équipe de support doit ensuite démêler à la main. Annuler un versement deux fois reprend de l'argent que vous avez déjà rendu une fois, et vous devez maintenant à un créateur des excuses et une correction. Et chaque doublon que vous retraitez contre une API de boutique dépense un quota que Google vous avertit explicitement de protéger, donc une rafale de redistribution Pub/Sub pendant une panne peut vous pousser dans la limitation de débit le jour précis où vous pouvez le moins vous le permettre.

Les fenêtres de preuve de remboursement augmentent les enjeux du côté d'Apple. Si une déduplication naïve fait taire les CONSUMPTION_REQUESTs répétés qu'Apple envoie tout au long de la fenêtre de remboursement ouverte, vous pouvez manquer celui auquel vous deviez répondre, et un CONSUMPTION_REQUEST auquel vous ne répondez pas dans les 12 heures est un remboursement qu'Apple accorde souvent par défaut. Ce n'est pas un double prélèvement. C'est une vente perdue plus le calcul, les appels d'API, le stockage et les versements que vous avez déjà dépensés pour livrer l'achat, dont rien n'est rendu par le remboursement.

Mode de défaillanceCe qui tourne malCe que cela coûte
Redéduire un solde sur un REFUND en doubleLe solde de consommable de l'utilisateur devient négatifDu temps de support manuel pour réconcilier, et une mauvaise expérience client
Annuler un versement deux foisVous reprenez de l'argent que vous avez déjà rendu une foisUne correction pour le créateur et un nettoyage comptable
Retraiter contre une API de boutiqueLes appels en double consomment le quota de la Play Developer API ou de l'App Store Server APIUne limitation de débit pendant la panne qui a causé la redistribution
Sur-dédupliquer les CONSUMPTION_REQUESTsVous laissez tomber une invite de remboursement distincte comme un faux doublonUne fenêtre de 12 heures manquée, donc Apple accorde le remboursement par défaut

Comment gérer les notifications de remboursement en double sans agir deux fois

Le schéma est le même sur les deux boutiques, avec une clé différente. Enregistrez la livraison, vérifiez la clé avant d'agir, agissez une seule fois, et dites toujours à la boutique que vous l'avez reçue.

  • Dédupliquez sur l'id de livraison de la boutique, pas sur la transaction. Utilisez notificationUUID pour Apple et le messageId de Pub/Sub pour Google Play. Stockez-le avec une contrainte d'unicité pour qu'un doublon concurrent perde la course au lieu d'agir deux fois.
  • Rendez l'action en aval idempotente par elle-même. Indexer sur l'id de livraison arrête le retraitement, mais écrivez aussi l'effet de sorte que révoquer, déduire ou annuler vérifie d'abord l'état actuel et soit sûr à exécuter deux fois.
  • Persistez d'abord, puis accusez réception. Écrivez l'événement dans votre base de données avant de renvoyer 200 ou d'accuser réception du message Pub/Sub. Si vous accusez réception d'abord et que l'écriture échoue, la boutique considère le message comme livré et ne le renvoie plus jamais, et vous l'avez maintenant perdu pour de bon.
  • Renvoyez toujours un statut de succès, même pour un doublon. Un 200 à 206 pour Apple, un 200 au push Pub/Sub pour Google. Rejeter une répétition avec une erreur ne fait que pousser la boutique à la réessayer.
  • Regroupez sur l'entité, identifiez sur l'événement. Indexez votre état d'achat stocké sur le purchaseToken ou l'originalTransactionId pour que les livraisons dans le désordre mettent à jour une seule ligne, mais traitez chaque notificationUUID ou messageId comme son propre événement, car un achat en produit légitimement plusieurs.

Une courte liste de contrôle avant de faire confiance à votre webhook de remboursement

  • Les livraisons d'Apple sont dédupliquées sur notificationUUID, et une répétition n'écrit rien de nouveau mais renvoie tout de même 200.
  • Les livraisons de Google Play sont dédupliquées sur le messageId de Pub/Sub, vérifié avant tout traitement.
  • L'état d'achat est indexé sur purchaseToken ou originalTransactionId, pour que les événements dans le désordre atterrissent sur un seul enregistrement.
  • Chaque effet de bord de remboursement, révoquer, déduire ou annuler, est sûr à exécuter plus d'une fois.
  • Votre gestionnaire écrit l'événement avant d'accuser réception, jamais après.
  • Les CONSUMPTION_REQUESTs répétés sont traités comme des invites distinctes, pas comme des doublons, donc aucune fenêtre de remboursement ouverte n'est laissée tomber.

Passez un doublon dans votre propre webhook exprès et regardez-le ne rien changer la seconde fois. C'est tout le test. Un gestionnaire de remboursement qui est sûr à déclencher deux fois est un gestionnaire dont vous pouvez cesser de vous inquiéter dès l'instant où une boutique décide de le déclencher six fois.

Questions fréquentes

Pourquoi mon serveur reçoit-il la même notification de remboursement de l'App Store plus d'une fois ?
Parce qu'Apple réessaie une App Store Server Notification V2 jusqu'à cinq fois, à 1, 12, 24, 48 et 72 heures après la dernière tentative, chaque fois que votre serveur ne répond pas avec un statut HTTP entre 200 et 206. Chaque nouvelle tentative porte le même notificationUUID, vous pouvez donc le reconnaître et l'ignorer.
Quel champ dois-je utiliser pour dédupliquer les App Store Server Notifications ?
Utilisez le notificationUUID. Une véritable nouvelle tentative répète toujours le même notificationUUID, tandis que chaque événement véritablement nouveau, y compris chaque nouveau CONSUMPTION_REQUEST, en reçoit un différent, donc dédupliquer sur notificationUUID ignore les répétitions sans laisser tomber d'événements distincts.
Les notifications CONSUMPTION_REQUEST répétées sont-elles des doublons que je devrais ignorer ?
Non. Apple envoie de nouvelles notifications CONSUMPTION_REQUEST périodiquement tout au long de la fenêtre de remboursement ouverte, et le personnel d'Apple confirme qu'il ne s'agit pas de nouvelles tentatives. Chacune a son propre notificationUUID, alors traitez-les toutes. Les laisser tomber risque de manquer la fenêtre de 12 heures qu'Apple vous donne pour répondre.
Comment dédupliquer les Real-time Developer Notifications de Google Play ?
Lisez le messageId de Pub/Sub de chaque notification et comparez-le à ceux que vous avez déjà traités avant d'agir, car Pub/Sub livre au moins une fois et peut envoyer le même message plus d'une fois. Google le recommande explicitement pour éviter le traitement en double et le gaspillage de quota d'API.
Dois-je renvoyer une erreur pour rejeter une notification de remboursement en double ?
Non. Renvoyer un 4xx ou un 5xx indique à la boutique que la livraison a échoué, elle réessaie donc malgré tout. Dédupliquez dans votre propre base de données et renvoyez toujours un statut de succès, HTTP 200 à 206 pour Apple ou un accusé de réception 200 pour le push Google Play.

Sources et lectures complémentaires

RefundHalt

Le pilote automatique des remboursements pour l'App Store et Google Play

Poursuivre la lecture

La prochaine demande de remboursement est déjà en route.

Configurez RefundHalt dans le temps qu'il vous faudrait pour lire un nouvel e-mail d'assistance au sujet d'un remboursement que vous n'avez pas pu contester.