Zowel Apple als Google kunnen dezelfde terugbetaling meer dan eens aan uw server bezorgen, en dubbele terugbetalingsmeldingen kosten u geld als u op elke afzonderlijke reageert
Apple probeert een terugbetalingsmelding tot vijf keer opnieuw en Google Play leunt op Pub/Sub met at-least-once-bezorging, dus dezelfde terugbetaling kan uw server meer dan eens bereiken. Zo behandelt u dubbele terugbetalingsmeldingen zonder een saldo dubbel af te trekken of API-quota tweemaal te verbranden.

Belangrijkste inzichten
- Apple probeert een App Store Server Notification V2 vijf keer opnieuw, na 1, 12, 24, 48 en 72 uur na de laatste poging, telkens wanneer uw server niet antwoordt met een HTTP-status tussen 200 en 206. De eerste poging meegeteld kan één terugbetaling tot zes keer binnenkomen.
- Elke echte Apple-retry draagt dezelfde notificationUUID, dus dat veld, niet de transactie-id, is uw deduplicatiesleutel.
- De Real-time Developer Notifications van Google Play leunen op Cloud Pub/Sub, dat at-least-once-bezorging en geen volgorde garandeert, dus hetzelfde bericht kan twee keer of in de verkeerde volgorde binnenkomen. Google zegt u de messageId op uniekheid te controleren voordat u iets verwerkt.
- Apple's periodieke CONSUMPTION_REQUEST-meldingen zijn geen retries. Apple blijft over het open terugbetalingsvenster steeds nieuwe sturen, elk met een andere notificationUUID, dus dedupliceren op notificationUUID behoudt ze correct allemaal.
- Dezelfde Apple-transactionId kan meer dan één beslissing dragen, bijvoorbeeld een REFUND_DECLINED gevolgd later door een REFUND, dus dedupliceren op transactie-id alleen gooit een afzonderlijke gebeurtenis weg die u nodig had.
- Een duplicaat afwijzen door een 4xx of 5xx terug te geven zorgt er alleen voor dat de store het opnieuw probeert. Dedupliceer in uw eigen database en geef altijd een successtatus terug.
- Een terugbetalingshandler die niet idempotent is, handelt bij de tweede bezorging dubbel. Hij trekt een saldo tweemaal af, draait een uitbetaling tweemaal terug, of verbrandt factureerbare Play Developer API- en App Store Server API-quota door een terugbetaling opnieuw te controleren die hij al had afgesloten.
Uw server zal dezelfde terugbetalingsgebeurtenis meer dan eens ontvangen, en beide stores hebben dat met opzet zo ontworpen. Apple probeert een App Store Server Notification tot vijf keer opnieuw wanneer uw endpoint niet netjes antwoordt. Google Play bezorgt zijn Real-time Developer Notifications over Cloud Pub/Sub, dat at-least-once-bezorging belooft en niets over volgorde. De vraag is dus nooit of een duplicaat binnenkomt. Ze luidt wat uw code doet de tweede keer dat hij dezelfde terugbetaling ziet. Doe dat verkeerd en u trekt een saldo tweemaal af, draait een uitbetaling tweemaal terug, of verbrandt factureerbare API-quota door een terugbetaling opnieuw te controleren die u al had afgesloten. Zo bereiken dubbele terugbetalingsmeldingen u werkelijk, welke herhalingen echte duplicaten zijn en welke er alleen zo uitzien, en hoe u ze behandelt zodat de tweede bezorging gratis is.
Een terugbetalingsmelding wordt minstens één keer bezorgd, wat niet hetzelfde is als precies één keer
Beide stores behandelen een bezorgde melding als een belofte die ze steeds opnieuw proberen na te komen, niet als een enkel schot dat ze afvuren en vergeten. Dat is goed voor de betrouwbaarheid, want een melding die u tijdens een deploy mist, bereikt u later alsnog. Het is een valkuil voor de juistheid, want het mechanisme dat garandeert dat u de gebeurtenis uiteindelijk krijgt, garandeert ook dat u ze soms twee keer krijgt. Uw handler moet idempotent zijn, wat betekent dat de tweede en derde bezorging van één terugbetaling niets veranderen wat de eerste niet al veranderde.
Apple probeert vijf keer opnieuw over drie dagen
Wanneer Apple een App Store Server Notification V2 stuurt, verwacht het dat uw server antwoordt met een HTTP-status in het bereik 200 tot 206. Al het andere, een 4xx of een 5xx, vertelt Apple dat de bezorging is mislukt, en Apple probeert opnieuw. Het schema ligt vast: vijf retries, na 1, 12, 24, 48 en 72 uur na de vorige poging. De eerste poging meegeteld kan één terugbetalingsgebeurtenis tot zes keer binnenkomen, verspreid over ongeveer een week. Elk van die retries draagt dezelfde notificationUUID. Dat veld is uw deduplicatiesleutel. Als u een notificationUUID al hebt vastgelegd, is de bezorging die u vasthoudt een herhaling, en het juiste antwoord is niets nieuws op te slaan en toch 200 terug te geven.
Google Play leunt op Pub/Sub, dat at-least-once belooft en niets zegt over volgorde
De Real-time Developer Notifications van Google Play worden gepubliceerd naar een Cloud Pub/Sub-topic. De bezorggarantie van Pub/Sub is at-least-once, en er is helemaal geen volgordegarantie. Dat betekent dat hetzelfde bericht meer dan eens aan uw endpoint kan worden bezorgd, en dat twee berichten voor dezelfde aankoop in verkeerde volgorde kunnen binnenkomen. Googles eigen richtlijn is expliciet: pak het base64-data-veld uit, lees de messageId en controleer dat u die nog niet hebt gezien voordat u iets verwerkt. Een dubbele messageId is een herhaling die u overslaat. Twee verschillende meldingen over één aankoop moeten toch op hetzelfde record belanden, dus veranker uw opgeslagen toestand ook aan de purchaseToken en laat een latere gebeurtenis de rij bijwerken die een eerdere aanmaakte.
| Platform | Bezorgmodel | Dedupliceren op | Successignaal | Als u niet bevestigt |
|---|---|---|---|---|
| App Store Server Notifications V2 | Tot 6 pogingen: de eerste, plus 5 retries na 1, 12, 24, 48, 72 uur | notificationUUID | HTTP 200 tot 206 | Apple probeert opnieuw volgens het vaste schema, dan stopt het |
| Google Play RTDN over Pub/Sub | At-least-once, geen volgordegarantie | Pub/Sub messageId, entiteit-gekoppeld aan purchaseToken | HTTP 200 op de push, of een expliciete ack | Pub/Sub stuurt opnieuw nadat de ack-deadline verloopt |
De herhalingen die geen duplicaten zijn
Niet elke melding die eruitziet als een die u al hebt gezien, is een retry. Twee Apple-gedragingen sturen echt nieuwe gebeurtenissen die een aankoop delen maar elk verwerkt moeten worden, en ze samenvoegen met een naïeve deduplicatie gooit informatie weg die u nodig had.
Apple stuurt verse CONSUMPTION_REQUESTs, geen retries
Tijdens een open terugbetalingsverzoek voor een verbruiksartikel stuurt Apple niet één CONSUMPTION_REQUEST en wacht af. Het stuurt over het hele open terugbetalingsvenster periodiek nieuwe, tot de terugbetaling is afgesloten. Apple-medewerkers hebben bevestigd dat dit geen retries zijn, en het verraderlijke teken is het veld waarop u dedupliceert: elke verse CONSUMPTION_REQUEST draagt een andere notificationUUID. Een deduplicatie verankerd aan de notificationUUID doet dus automatisch het juiste. Ze voegt echte retries samen en behoudt elke afzonderlijke prompt. Wat u niet mag doen, is dedupliceren op transactie-id en meldingstype, want dat zou elke CONSUMPTION_REQUEST na de eerste het zwijgen opleggen en u het 12-uur-bewijsvenster kosten op de weggegooide.
Eén transactie kan meer dan één beslissing dragen
Een enkele transactionId kan over haar leven meer dan één terugbetalingsuitkomst voortbrengen. Apple kan een REFUND_DECLINED sturen en dan, later, een REFUND voor dezelfde transactie, en ontwikkelaars melden drie of meer terugbetalingsgerelateerde meldingen voor één transactie-id te ontvangen. Elk is een afzonderlijke gebeurtenis met haar eigen notificationUUID. Als uw deduplicatiesleutel de transactie-id is, ziet de tweede beslissing eruit als een duplicaat van de eerste en komt u nooit te weten dat de terugbetaling uiteindelijk is toegekend. De transactie-id groepeert gebeurtenissen. Ze identificeert ze niet.

Wat een duplicaat u werkelijk kost
Een terugbetalingsmelding is geen statuslampje. Ze zet echte acties in gang: u trekt toegang in, u trekt een verbruikssaldo af, u draait een uitbetaling aan een maker terug, u roept de App Store Server API of de Play Developer API aan om de toestand te bevestigen. Voer een van die een tweede keer uit op een duplicaat en de kosten zijn reëel.
Volg het geld. Toegang tweemaal intrekken is onschadelijk, want de toegang is al weg. Een saldo tweemaal aftrekken is dat niet: een gebruiker die een pakket munten kocht en terugbetaalde, kan naar een negatief saldo worden gedreven dat uw supportteam dan met de hand moet ontwarren. Een uitbetaling tweemaal terugdraaien haalt geld terug dat u al één keer had teruggegeven, en nu bent u een maker een verontschuldiging en een correctie schuldig. En elk duplicaat dat u opnieuw tegen een store-API verwerkt, verbruikt quota die Google u uitdrukkelijk aanraadt te beschermen, dus een uitbarsting van Pub/Sub-herbezorging tijdens een storing kan u naar rate limiting duwen op precies de dag waarop u het zich het minst kunt veroorloven.
De terugbetalings-bewijsvensters verhogen de inzet aan de Apple-kant. Als een naïeve deduplicatie de herhaalde CONSUMPTION_REQUESTs het zwijgen oplegt die Apple over het open terugbetalingsvenster stuurt, kunt u degene missen die u moest beantwoorden, en een CONSUMPTION_REQUEST die u niet binnen 12 uur beantwoordt, is een terugbetaling die Apple vaak standaard toekent. Dat is geen dubbele afschrijving. Het is een verloren verkoop plus de rekenkracht, API-aanroepen, opslag en uitbetalingen die u al hebt uitgegeven om de aankoop te leveren, waarvan de terugbetaling er geen terugbrengt.
| Faalmodus | Wat er misgaat | Wat het kost |
|---|---|---|
| Een saldo opnieuw aftrekken bij een dubbele REFUND | Het verbruikssaldo van de gebruiker wordt negatief | Handmatige supporttijd om te verzoenen, en een slechte klantervaring |
| Een uitbetaling tweemaal terugdraaien | U haalt geld terug dat u al één keer had teruggegeven | Een makercorrectie en een boekhoudopschoning |
| Opnieuw verwerken tegen een store-API | Dubbele aanroepen verbranden Play Developer- of App Store Server API-quota | Rate limiting tijdens de storing die de herbezorging veroorzaakte |
| CONSUMPTION_REQUESTs over-dedupliceren | U gooit een afzonderlijke terugbetalingsprompt weg als vals duplicaat | Een gemist 12-uur-venster, dus Apple kent de terugbetaling standaard toe |
Hoe u dubbele terugbetalingsmeldingen behandelt zonder dubbel te handelen
Het patroon is op beide stores hetzelfde, met een andere sleutel. Leg de bezorging vast, controleer de sleutel voordat u handelt, handel één keer, en vertel de store altijd dat u ze hebt ontvangen.
- Dedupliceer op de bezorg-id van de store, niet op de transactie. Gebruik
notificationUUIDvoor Apple en de Pub/Sub-messageIdvoor Google Play. Sla die op met een unieke constraint zodat een gelijktijdig duplicaat de race verliest in plaats van dubbel te handelen. - Maak de downstream-actie idempotent op haar eigen voorwaarden. Verankeren aan de bezorg-id stopt de herverwerking, maar schrijf ook het effect zo dat intrekken, aftrekken of terugdraaien eerst de huidige toestand controleert en veilig twee keer kan draaien.
- Eerst persisteren, dan bevestigen. Schrijf de gebeurtenis naar uw database voordat u 200 teruggeeft of het Pub/Sub-bericht bevestigt. Als u eerst bevestigt en het schrijven mislukt, beschouwt de store het bericht als bezorgd en stuurt het nooit meer, en nu bent u het definitief kwijt.
- Geef altijd een successtatus terug, ook voor een duplicaat. Een 200 tot 206 voor Apple, een 200 op de Pub/Sub-push voor Google. Een herhaling afwijzen met een fout zorgt er alleen voor dat de store ze opnieuw stuurt.
- Groepeer op de entiteit, identificeer op de gebeurtenis. Veranker uw opgeslagen aankooptoestand aan de
purchaseTokenoforiginalTransactionIdzodat bezorgingen in verkeerde volgorde één rij bijwerken, maar behandel elkenotificationUUIDofmessageIdals een eigen gebeurtenis, want één aankoop brengt terecht meerdere voort.
Een korte checklist voordat u uw terugbetalings-webhook vertrouwt
- Apple-bezorgingen worden gededupliceerd op
notificationUUID, en een herhaling schrijft niets nieuws maar geeft toch 200 terug. - Google Play-bezorgingen worden gededupliceerd op de Pub/Sub-
messageId, gecontroleerd vóór enige verwerking. - De aankooptoestand is verankerd aan
purchaseTokenoforiginalTransactionId, zodat gebeurtenissen in verkeerde volgorde op één record belanden. - Elk terugbetalingsneveneffect, intrekken, aftrekken of terugdraaien, is veilig meer dan eens uit te voeren.
- Uw handler schrijft de gebeurtenis voordat hij bevestigt, nooit erna.
- Herhaalde CONSUMPTION_REQUESTs worden behandeld als afzonderlijke prompts, niet als duplicaten, zodat geen open terugbetalingsvenster wordt weggegooid.
Stuur met opzet een duplicaat door uw eigen webhook en kijk hoe het de tweede keer niets verandert. Dat is de hele test. Een terugbetalingshandler die veilig twee keer geraakt kan worden, is er een waarover u zich niet meer druk hoeft te maken zodra een store besluit hem zes keer te raken.
Veelgestelde vragen
- Waarom ontvangt mijn server dezelfde App Store-terugbetalingsmelding meer dan eens?
- Omdat Apple een App Store Server Notification V2 tot vijf keer opnieuw probeert, na 1, 12, 24, 48 en 72 uur na de laatste poging, telkens wanneer uw server niet antwoordt met een HTTP-status tussen 200 en 206. Elke retry draagt dezelfde notificationUUID, zodat u die kunt herkennen en overslaan.
- Welk veld moet ik gebruiken om App Store Server Notifications te dedupliceren?
- Gebruik de notificationUUID. Een echte retry herhaalt altijd dezelfde notificationUUID, terwijl elke echt nieuwe gebeurtenis, inclusief elke verse CONSUMPTION_REQUEST, een andere krijgt, zodat dedupliceren op notificationUUID herhalingen overslaat zonder afzonderlijke gebeurtenissen weg te gooien.
- Zijn herhaalde CONSUMPTION_REQUEST-meldingen duplicaten die ik moet negeren?
- Nee. Apple stuurt periodiek nieuwe CONSUMPTION_REQUEST-meldingen over het open terugbetalingsvenster, en Apple-medewerkers bevestigen dat dit geen retries zijn. Elk heeft een eigen notificationUUID, dus verwerk ze allemaal. Ze weggooien riskeert het missen van het 12-uur-venster dat Apple u geeft om te reageren.
- Hoe dedupliceer ik Google Play Real-time Developer Notifications?
- Lees de Pub/Sub-messageId uit elke melding en controleer die tegen de reeds verwerkte voordat u handelt, want Pub/Sub bezorgt at-least-once en kan hetzelfde bericht meer dan eens sturen. Google raadt dit uitdrukkelijk aan om dubbele verwerking en verspilde API-quota te vermijden.
- Moet ik een fout teruggeven om een dubbele terugbetalingsmelding af te wijzen?
- Nee. Een 4xx of 5xx teruggeven vertelt de store dat de bezorging is mislukt, dus probeert hij toch opnieuw. Dedupliceer in uw eigen database en geef altijd een successtatus terug, HTTP 200 tot 206 voor Apple of een 200-bevestiging voor de Google Play-push.
Bronnen en verder lezen
- Apple Developer: App Store Server Notifications V2
- Apple Developer: Responding to App Store Server Notifications
- Apple Developer: notificationUUID
- Apple Developer Forums: App Store Server Notifications V2 retry schedule and CONSUMPTION_REQUEST clarification
- Android Developers: Real-time developer notifications reference
- Android Developers: Purchase lifecycle and RTDNs
- Google Cloud: Pub/Sub subscriber and at-least-once delivery
RefundHalt
De terugbetalingsautopiloot voor de App Store en Google Play
Lees verder
Een Family Sharing-terugbetaling draait één betaling terug maar kan vijf andere mensen achterlaten die je app nog gebruiken, en alleen je server kan hun toegang afsluiten
Een Family Sharing-terugbetaling draait één betaling terug maar kan tot vijf gezinsleden achterlaten die je betaalde functies nog gebruiken. Apple stuurt een REVOKE en verwacht dat je server die toegang beëindigt. Zo werken gezinsgedeelde terugbetalingen en wat kost er een.
Terugbetalingsafhandeling gaat stil kapot, dus test in-app-aankoop-terugbetalingen in de sandbox voordat een echte klant dat doet
Je terugbetalingsafhandeling draait pas nadat een klant al weg is, dus een bug erin blijft onzichtbaar totdat hij echt geld kost. Beide stores laten je eerst een terugbetaling in een testomgeving activeren. Zo test je in-app-aankoop-terugbetalingen in de App Store en Google Play voordat er een echt is.