Zarówno Apple, jak i Google mogą dostarczyć ten sam zwrot na Twój serwer więcej niż raz, a zduplikowane powiadomienia o zwrotach kosztują Cię, jeśli reagujesz na każde z nich
Apple ponawia powiadomienie o zwrocie do pięciu razy, a Google Play opiera się na Pub/Sub z dostarczaniem at-least-once, więc ten sam zwrot może dotrzeć na Twój serwer więcej niż raz. Oto jak obsługiwać zduplikowane powiadomienia o zwrotach bez podwójnego odejmowania salda i bez dwukrotnego spalania limitu API.

Najważniejsze wnioski
- Apple ponawia App Store Server Notification V2 pięć razy, po 1, 12, 24, 48 i 72 godzinach od ostatniej próby, za każdym razem, gdy Twój serwer nie odpowie statusem HTTP między 200 a 206. Licząc pierwszą próbę, jeden zwrot może dotrzeć nawet sześć razy.
- Każde prawdziwe ponowienie Apple niesie tę samą notificationUUID, więc to pole, a nie identyfikator transakcji, jest Twoim kluczem deduplikacji.
- Real-time Developer Notifications od Google Play opierają się na Cloud Pub/Sub, które gwarantuje dostarczanie at-least-once i brak kolejności, więc ta sama wiadomość może dotrzeć dwa razy lub w złej kolejności. Google każe Ci sprawdzić unikalność messageId, zanim cokolwiek przetworzysz.
- Okresowe powiadomienia CONSUMPTION_REQUEST od Apple nie są ponowieniami. Apple wysyła coraz to nowe przez cały otwarty okres zwrotu, każde z inną notificationUUID, więc deduplikacja po notificationUUID poprawnie zachowuje każde z nich.
- Ta sama transactionId Apple może nieść więcej niż jedną decyzję, na przykład REFUND_DECLINED, po którym później następuje REFUND, więc deduplikacja wyłącznie po identyfikatorze transakcji odrzuca odrębne zdarzenie, którego potrzebowałeś.
- Odrzucenie duplikatu przez zwrócenie 4xx lub 5xx sprawia tylko, że sklep ponawia je. Deduplikuj we własnej bazie danych i zawsze zwracaj status sukcesu.
- Program obsługi zwrotów, który nie jest idempotentny, działa podwójnie przy drugim dostarczeniu. Odejmuje saldo dwa razy, cofa wypłatę dwa razy lub spala rozliczany limit Play Developer API i App Store Server API, ponownie sprawdzając zwrot, który już zamknął.
Twój serwer otrzyma to samo zdarzenie zwrotu więcej niż raz, i oba sklepy zaprojektowały to celowo. Apple ponawia App Store Server Notification do pięciu razy, gdy Twój punkt końcowy nie odpowie czysto. Google Play dostarcza swoje Real-time Developer Notifications przez Cloud Pub/Sub, które obiecuje dostarczanie at-least-once i nic o kolejności. Pytanie nigdy nie brzmi więc, czy duplikat dotrze. Brzmi ono, co robi Twój kod za drugim razem, gdy widzi ten sam zwrot. Zrób to źle, a odejmiesz saldo dwa razy, cofniesz wypłatę dwa razy lub spalisz rozliczany limit API, ponownie sprawdzając zwrot, który już zamknąłeś. Oto jak zduplikowane powiadomienia o zwrotach faktycznie do Ciebie docierają, które powtórzenia są prawdziwymi duplikatami, a które tylko tak wyglądają, i jak je obsługiwać, aby drugie dostarczenie było darmowe.
Powiadomienie o zwrocie jest dostarczane co najmniej raz, co nie jest tym samym co dokładnie raz
Oba sklepy traktują dostarczone powiadomienie jako obietnicę, którą wciąż próbują spełnić, a nie pojedynczy strzał, który oddają i zapominają. To dobre dla niezawodności, bo powiadomienie, które przegapisz podczas wdrożenia, i tak dotrze do Ciebie później. To pułapka dla poprawności, bo mechanizm gwarantujący, że w końcu otrzymasz zdarzenie, gwarantuje też, że czasem otrzymasz je dwa razy. Twój program obsługi musi być idempotentny, co oznacza, że drugie i trzecie dostarczenie jednego zwrotu nie zmieniają niczego, czego pierwsze już nie zmieniło.
Apple ponawia pięć razy w ciągu trzech dni
Gdy Apple wysyła App Store Server Notification V2, oczekuje, że Twój serwer odpowie statusem HTTP w zakresie od 200 do 206. Wszystko inne, 4xx lub 5xx, mówi Apple, że dostarczenie się nie powiodło, i Apple ponawia. Harmonogram jest stały: pięć ponowień, po 1, 12, 24, 48 i 72 godzinach od poprzedniej próby. Licząc pierwszą próbę, jedno zdarzenie zwrotu może dotrzeć nawet sześć razy, rozłożone na mniej więcej tydzień. Każde z tych ponowień niesie tę samą notificationUUID. To pole jest Twoim kluczem deduplikacji. Jeśli już zapisałeś notificationUUID, dostarczenie, które trzymasz, jest powtórzeniem, a poprawną reakcją jest nie zapisywać nic nowego i mimo to zwrócić 200.
Google Play opiera się na Pub/Sub, które obiecuje at-least-once i nic nie mówi o kolejności
Real-time Developer Notifications od Google Play są publikowane do tematu Cloud Pub/Sub. Gwarancja dostarczania Pub/Sub to at-least-once i nie ma żadnej gwarancji kolejności. To oznacza, że ta sama wiadomość może zostać dostarczona do Twojego punktu końcowego więcej niż raz, a dwie wiadomości dla tego samego zakupu mogą dotrzeć w złej kolejności. Własne wytyczne Google są jednoznaczne: rozpakuj pole data w base64, odczytaj messageId i sprawdź, że jeszcze go nie widziałeś, zanim cokolwiek przetworzysz. Zduplikowana messageId to powtórzenie, które pomijasz. Dwa różne powiadomienia o jednym zakupie powinny mimo to trafić do tego samego rekordu, więc oprzyj swój zapisany stan także na purchaseToken i pozwól, by późniejsze zdarzenie zaktualizowało wiersz utworzony przez wcześniejsze.
| Platforma | Model dostarczania | Deduplikacja po | Sygnał sukcesu | Jeśli nie potwierdzisz |
|---|---|---|---|---|
| App Store Server Notifications V2 | Do 6 prób: pierwsza plus 5 ponowień po 1, 12, 24, 48, 72 godzinach | notificationUUID | HTTP 200 do 206 | Apple ponawia według stałego harmonogramu, potem przestaje |
| Google Play RTDN przez Pub/Sub | At-least-once, brak gwarancji kolejności | Pub/Sub messageId, powiązane z encją po purchaseToken | HTTP 200 na push lub jawne ack | Pub/Sub ponawia po wygaśnięciu terminu ack |
Powtórzenia, które nie są duplikatami
Nie każde powiadomienie, które wygląda jak już widziane, jest ponowieniem. Dwa zachowania Apple wysyłają naprawdę nowe zdarzenia, które dzielą jeden zakup, ale każde musi zostać przetworzone, a scalenie ich naiwną deduplikacją odrzuca informacje, których potrzebowałeś.
Apple wysyła świeże CONSUMPTION_REQUEST, a nie ponowienia
Podczas otwartego żądania zwrotu za produkt konsumpcyjny Apple nie wysyła jednego CONSUMPTION_REQUEST i nie czeka. Wysyła nowe okresowo przez cały otwarty okres zwrotu, aż zwrot zostanie zamknięty. Pracownicy Apple potwierdzili, że nie są to ponowienia, a znakiem rozpoznawczym jest pole, po którym deduplikujesz: każdy świeży CONSUMPTION_REQUEST niesie inną notificationUUID. Deduplikacja oparta na notificationUUID robi więc automatycznie właściwą rzecz. Scala prawdziwe ponowienia i zachowuje każdy odrębny monit. Czego nie wolno Ci robić, to deduplikować po identyfikatorze transakcji i typie powiadomienia, bo to uciszyłoby każdy CONSUMPTION_REQUEST po pierwszym i kosztowałoby Cię 12-godzinne okno dowodowe na tych porzuconych.
Jedna transakcja może nieść więcej niż jedną decyzję
Pojedyncza transactionId może w ciągu swojego życia wygenerować więcej niż jeden wynik zwrotu. Apple może wysłać REFUND_DECLINED, a potem, później, REFUND dla tej samej transakcji, a deweloperzy zgłaszają otrzymywanie trzech lub więcej powiadomień związanych ze zwrotem dla jednego identyfikatora transakcji. Każde jest odrębnym zdarzeniem z własną notificationUUID. Jeśli Twoim kluczem deduplikacji jest identyfikator transakcji, druga decyzja wygląda jak duplikat pierwszej i nigdy nie dowiesz się, że zwrot ostatecznie przyznano. Identyfikator transakcji grupuje zdarzenia. Nie identyfikuje ich.

Ile naprawdę kosztuje Cię duplikat
Powiadomienie o zwrocie to nie lampka statusu. Wyzwala prawdziwe działania: odbierasz dostęp, odejmujesz saldo produktu konsumpcyjnego, cofasz wypłatę dla twórcy, wywołujesz App Store Server API lub Play Developer API, aby potwierdzić stan. Uruchom którekolwiek z nich po raz drugi na duplikacie, a koszt jest realny.
Prześledź pieniądze. Dwukrotne odebranie dostępu jest nieszkodliwe, bo dostęp już zniknął. Dwukrotne odjęcie salda nie jest: użytkownik, który kupił pakiet monet i zwrócił go, może zostać zepchnięty na ujemne saldo, które Twój zespół wsparcia musi potem rozplątywać ręcznie. Dwukrotne cofnięcie wypłaty odbiera pieniądze, które już raz zwróciłeś, i teraz jesteś twórcy winien przeprosiny i korektę. A każdy duplikat, który ponownie przetwarzasz względem API sklepu, zużywa limit, którego ochrony Google wyraźnie Ci zaleca, więc lawina ponownych dostarczeń Pub/Sub podczas awarii może wepchnąć Cię w ograniczanie przepustowości dokładnie w tym dniu, w którym najmniej możesz sobie na to pozwolić.
Okna dowodowe zwrotu podnoszą stawkę po stronie Apple. Jeśli naiwna deduplikacja ucisza powtarzane CONSUMPTION_REQUEST, które Apple wysyła przez otwarty okres zwrotu, możesz przegapić to, na które musiałeś odpowiedzieć, a CONSUMPTION_REQUEST, na które nie odpowiesz w ciągu 12 godzin, to zwrot, który Apple często przyznaje domyślnie. To nie jest podwójne obciążenie. To utracona sprzedaż plus moc obliczeniowa, wywołania API, pamięć i wypłaty, które już wydałeś, dostarczając zakup, z których żadnego zwrot nie odzyskuje.
| Tryb awarii | Co idzie nie tak | Ile kosztuje |
|---|---|---|
| Ponowne odjęcie salda przy zduplikowanym REFUND | Saldo produktu konsumpcyjnego użytkownika staje się ujemne | Ręczny czas wsparcia na uzgodnienie i złe doświadczenie klienta |
| Dwukrotne cofnięcie wypłaty | Odbierasz pieniądze, które już raz zwróciłeś | Korekta u twórcy i porządkowanie księgowości |
| Ponowne przetwarzanie względem API sklepu | Zduplikowane wywołania spalają limit Play Developer lub App Store Server API | Ograniczanie przepustowości podczas awarii, która spowodowała ponowne dostarczenie |
| Nadmierna deduplikacja CONSUMPTION_REQUEST | Porzucasz odrębny monit o zwrot jako fałszywy duplikat | Przegapione 12-godzinne okno, więc Apple przyznaje zwrot domyślnie |
Jak obsługiwać zduplikowane powiadomienia o zwrotach bez podwójnego działania
Wzorzec jest taki sam w obu sklepach, z innym kluczem. Zapisz dostarczenie, sprawdź klucz, zanim zadziałasz, zadziałaj raz i zawsze powiedz sklepowi, że je otrzymałeś.
- Deduplikuj po identyfikatorze dostarczenia sklepu, a nie po transakcji. Użyj
notificationUUIDdla Apple i Pub/SubmessageIddla Google Play. Zapisz go z ograniczeniem unikalności, aby równoczesny duplikat przegrał wyścig zamiast działać podwójnie. - Uczyń działanie downstream idempotentnym na własnych warunkach. Oparcie na identyfikatorze dostarczenia zatrzymuje ponowne przetwarzanie, ale napisz też efekt tak, aby odbieranie, odejmowanie lub cofanie najpierw sprawdzało bieżący stan i było bezpieczne do dwukrotnego uruchomienia.
- Najpierw zapisz, potem potwierdź. Zapisz zdarzenie do bazy danych, zanim zwrócisz 200 lub potwierdzisz wiadomość Pub/Sub. Jeśli potwierdzisz najpierw, a zapis się nie powiedzie, sklep uzna wiadomość za dostarczoną i nigdy jej nie wyśle ponownie, a teraz straciłeś ją na dobre.
- Zawsze zwracaj status sukcesu, nawet dla duplikatu. 200 do 206 dla Apple, 200 na push Pub/Sub dla Google. Odrzucenie powtórzenia błędem sprawia tylko, że sklep wysyła je ponownie.
- Grupuj po encji, identyfikuj po zdarzeniu. Oprzyj swój zapisany stan zakupu na
purchaseTokenluboriginalTransactionId, aby dostarczenia w złej kolejności aktualizowały jeden wiersz, ale traktuj każdąnotificationUUIDlubmessageIdjako własne zdarzenie, bo jeden zakup zasadnie generuje kilka.
Krótka lista kontrolna, zanim zaufasz swojemu webhookowi zwrotów
- Dostarczenia Apple są deduplikowane po
notificationUUID, a powtórzenie nie zapisuje nic nowego, ale mimo to zwraca 200. - Dostarczenia Google Play są deduplikowane po Pub/Sub
messageId, sprawdzanej przed jakimkolwiek przetwarzaniem. - Stan zakupu jest oparty na
purchaseTokenluboriginalTransactionId, więc zdarzenia w złej kolejności trafiają do jednego rekordu. - Każdy efekt uboczny zwrotu, odebranie, odjęcie lub cofnięcie, jest bezpieczny do uruchomienia więcej niż raz.
- Twój program obsługi zapisuje zdarzenie, zanim je potwierdzi, nigdy po.
- Powtarzane CONSUMPTION_REQUEST są traktowane jako odrębne monity, a nie duplikaty, więc żadne otwarte okno zwrotu nie zostaje porzucone.
Przepuść celowo duplikat przez własny webhook i patrz, jak za drugim razem nic nie zmienia. To cały test. Program obsługi zwrotów, który można bezpiecznie trafić dwa razy, to taki, o który przestajesz się martwić w chwili, gdy sklep postanawia trafić go sześć razy.
Często zadawane pytania
- Dlaczego mój serwer otrzymuje to samo powiadomienie o zwrocie z App Store więcej niż raz?
- Ponieważ Apple ponawia App Store Server Notification V2 do pięciu razy, po 1, 12, 24, 48 i 72 godzinach od ostatniej próby, za każdym razem, gdy Twój serwer nie odpowie statusem HTTP między 200 a 206. Każde ponowienie niesie tę samą notificationUUID, więc możesz je rozpoznać i pominąć.
- Którego pola powinienem użyć do deduplikacji App Store Server Notifications?
- Użyj notificationUUID. Prawdziwe ponowienie zawsze powtarza tę samą notificationUUID, podczas gdy każde naprawdę nowe zdarzenie, w tym każdy świeży CONSUMPTION_REQUEST, dostaje inną, więc deduplikacja po notificationUUID pomija powtórzenia bez odrzucania odrębnych zdarzeń.
- Czy powtarzane powiadomienia CONSUMPTION_REQUEST to duplikaty, które powinienem ignorować?
- Nie. Apple wysyła nowe powiadomienia CONSUMPTION_REQUEST okresowo przez otwarty okres zwrotu, a pracownicy Apple potwierdzają, że nie są to ponowienia. Każde ma własną notificationUUID, więc przetwórz każde z nich. Ich porzucenie grozi przegapieniem 12-godzinnego okna, które Apple daje Ci na odpowiedź.
- Jak deduplikować Google Play Real-time Developer Notifications?
- Odczytaj Pub/Sub messageId z każdego powiadomienia i sprawdź je względem już przetworzonych, zanim zadziałasz, bo Pub/Sub dostarcza at-least-once i może wysłać tę samą wiadomość więcej niż raz. Google wyraźnie to zaleca, aby uniknąć podwójnego przetwarzania i zmarnowanego limitu API.
- Czy powinienem zwrócić błąd, aby odrzucić zduplikowane powiadomienie o zwrocie?
- Nie. Zwrócenie 4xx lub 5xx mówi sklepowi, że dostarczenie się nie powiodło, więc i tak ponawia. Deduplikuj we własnej bazie danych i zawsze zwracaj status sukcesu, HTTP 200 do 206 dla Apple lub potwierdzenie 200 dla push Google Play.
Źródła i materiały dodatkowe
- 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
Autopilot zwrotów dla App Store i Google Play
Czytaj dalej
Zwrot w ramach Family Sharing cofa jedną płatność, ale może zostawić pięć innych osób nadal korzystających z Twojej aplikacji, a tylko Twój serwer może odciąć im dostęp
Zwrot w ramach Family Sharing cofa jedną płatność, ale może zostawić nawet pięciu członków rodziny nadal korzystających z Twoich płatnych funkcji. Apple wysyła REVOKE i oczekuje, że Twój serwer zakończy ten dostęp. Oto jak działają zwroty w ramach udostępniania rodzinnego i ile kosztuje jeden z nich.
Obsługa zwrotów psuje się po cichu, więc przetestuj zwroty za zakupy w aplikacji w środowisku sandbox, zanim zrobi to prawdziwy klient
Twoja obsługa zwrotów uruchamia się dopiero po tym, jak klient już odszedł, więc błąd w niej pozostaje niewidoczny, dopóki nie zacznie kosztować prawdziwych pieniędzy. Oba sklepy pozwalają najpierw uruchomić zwrot w środowisku testowym. Oto jak przetestować zwroty za zakupy w aplikacji w App Store i Google Play, zanim jeden z nich stanie się prawdziwy.