Sia Apple sia Google possono consegnare lo stesso rimborso al tuo server più di una volta, e le notifiche di rimborso duplicate ti costano se agisci su ognuna
Apple riprova una notifica di rimborso fino a cinque volte e Google Play viaggia su Pub/Sub con consegna almeno una volta, quindi lo stesso rimborso può raggiungere il tuo server più di una volta. Ecco come gestire le notifiche di rimborso duplicate senza scalare un saldo né consumare quota API due volte.

Punti chiave
- Apple riprova una App Store Server Notification V2 cinque volte, a 1, 12, 24, 48 e 72 ore dopo l'ultimo tentativo, ogni volta che il tuo server non risponde con uno stato HTTP tra 200 e 206. Contando il primo tentativo, un rimborso può arrivare fino a sei volte.
- Ogni vero nuovo tentativo di Apple porta lo stesso notificationUUID, quindi quel campo, e non l'id della transazione, è la tua chiave di deduplicazione.
- Le Real-time Developer Notifications di Google Play viaggiano su Cloud Pub/Sub, che garantisce la consegna almeno una volta e nessun ordine, quindi lo stesso messaggio può arrivare due volte o fuori ordine. Google ti dice di verificare l'unicità del messageId prima di elaborare qualsiasi cosa.
- Le notifiche periodiche CONSUMPTION_REQUEST di Apple non sono nuovi tentativi. Apple continua a inviarne di nuove per tutta la finestra di rimborso aperta, ciascuna con un notificationUUID diverso, quindi deduplicare su notificationUUID le conserva correttamente tutte.
- Lo stesso transactionId di Apple può portare più di una decisione, per esempio un REFUND_DECLINED seguito più tardi da un REFUND, quindi deduplicare solo sull'id della transazione scarta un evento distinto di cui avevi bisogno.
- Rifiutare un duplicato restituendo un 4xx o un 5xx fa solo sì che lo store lo riprovi. Deduplica dentro il tuo database e restituisci sempre uno stato di successo.
- Un gestore di rimborsi che non è idempotente agisce due volte alla seconda consegna. Scala un saldo due volte, inverte un pagamento due volte, o consuma quota fatturabile della Play Developer API e della App Store Server API ricontrollando un rimborso che ha già chiuso.
Il tuo server riceverà lo stesso evento di rimborso più di una volta, ed entrambi gli store lo hanno progettato così di proposito. Apple riprova una App Store Server Notification fino a cinque volte quando il tuo endpoint non risponde in modo pulito. Google Play consegna le sue Real-time Developer Notifications tramite Cloud Pub/Sub, che promette la consegna almeno una volta e nulla sull'ordine. Quindi la domanda non è mai se arriva un duplicato. È cosa fa il tuo codice la seconda volta che vede lo stesso rimborso. Sbaglia in questo e scali un saldo due volte, inverti un pagamento due volte, o consumi quota API fatturabile ricontrollando un rimborso che hai già chiuso. Ecco come le notifiche di rimborso duplicate ti raggiungono davvero, quali ripetizioni sono duplicati reali e quali solo lo sembrano, e come gestirle perché la seconda consegna sia gratuita.
Una notifica di rimborso viene consegnata almeno una volta, il che non è lo stesso che esattamente una volta
Entrambi gli store trattano una notifica consegnata come una promessa che continuano a cercare di mantenere, non come un colpo singolo che sparano e dimenticano. Questo è positivo per l'affidabilità, perché una notifica che perdi durante un deploy ti raggiunge comunque più tardi. È una trappola per la correttezza, perché il meccanismo che garantisce che alla fine ricevi l'evento garantisce anche che a volte lo ricevi due volte. Il tuo gestore deve essere idempotente, cioè la seconda e la terza consegna di uno stesso rimborso non cambiano nulla che la prima non abbia già cambiato.
Apple riprova cinque volte nell'arco di tre giorni
Quando Apple invia una App Store Server Notification V2, si aspetta che il tuo server risponda con uno stato HTTP nell'intervallo da 200 a 206. Qualsiasi altra cosa, un 4xx o un 5xx, dice ad Apple che la consegna è fallita, e Apple riprova. Il calendario è fisso: cinque nuovi tentativi, a 1, 12, 24, 48 e 72 ore dopo il tentativo precedente. Contando il primo tentativo, un evento di rimborso può arrivare fino a sei volte, distribuito nell'arco di circa una settimana. Ognuno di quei nuovi tentativi porta lo stesso notificationUUID. Quel campo è la tua chiave di deduplicazione. Se hai già registrato un notificationUUID, la consegna che hai in mano è una ripetizione, e la risposta corretta è non memorizzare nulla di nuovo e restituire comunque 200.
Google Play viaggia su Pub/Sub, che promette almeno una volta e non dice nulla sull'ordine
Le Real-time Developer Notifications di Google Play vengono pubblicate su un topic di Cloud Pub/Sub. La garanzia di consegna di Pub/Sub è almeno una volta, e non offre alcuna garanzia di ordine. Ciò significa che lo stesso messaggio può essere consegnato al tuo endpoint più di una volta, e due messaggi per lo stesso acquisto possono arrivare fuori ordine. La guida di Google è esplicita: spacchetta il campo base64 data, leggi il messageId, e verifica di non averlo già visto prima di elaborare qualsiasi cosa. Un messageId duplicato è una ripetizione che salti. Due notifiche diverse su uno stesso acquisto devono comunque atterrare sullo stesso record, quindi indicizza il tuo stato memorizzato anche sul purchaseToken, e lascia che un evento successivo aggiorni la riga che uno precedente ha creato.
| Piattaforma | Modello di consegna | Deduplicare su | Segnale di successo | Se non confermi la ricezione |
|---|---|---|---|---|
| App Store Server Notifications V2 | Fino a 6 tentativi: il primo, più 5 nuovi tentativi a 1, 12, 24, 48, 72 ore | notificationUUID | HTTP da 200 a 206 | Apple riprova secondo il calendario fisso, poi si ferma |
| Google Play RTDN su Pub/Sub | Almeno una volta, nessuna garanzia di ordine | Pub/Sub messageId, indicizzato per entità su purchaseToken | HTTP 200 al push, o un ack esplicito | Pub/Sub rinvia allo scadere del termine dell'ack |
Le ripetizioni che non sono duplicati
Non ogni notifica che assomiglia a una che hai già visto è un nuovo tentativo. Due comportamenti di Apple inviano eventi genuinamente nuovi che condividono un acquisto ma che devono essere elaborati ciascuno, e comprimerli con una deduplicazione ingenua scarta informazioni di cui avevi bisogno.
Apple invia nuovi CONSUMPTION_REQUEST, non nuovi tentativi
Durante una richiesta di rimborso aperta su un consumabile, Apple non invia un solo CONSUMPTION_REQUEST e attende. Ne invia di nuovi periodicamente per tutta la finestra di rimborso aperta finché il rimborso non viene chiuso. Il personale di Apple ha confermato che non sono nuovi tentativi, e l'indizio è il campo su cui deduplichi: ogni nuovo CONSUMPTION_REQUEST porta un notificationUUID diverso. Quindi una deduplicazione indicizzata su notificationUUID fa automaticamente la cosa giusta. Comprime i veri nuovi tentativi e conserva ogni prompt distinto. Ciò che non devi fare è deduplicare sull'id della transazione e sul tipo di notifica, perché questo silenzierebbe ogni CONSUMPTION_REQUEST dopo il primo e ti costerebbe la finestra di prova di 12 ore su quelli che hai scartato.
Una transazione può portare più di una decisione
Un singolo transactionId può produrre più di un esito di rimborso nel corso della sua vita. Apple può inviare un REFUND_DECLINED e poi, più tardi, un REFUND per la stessa transazione, e gli sviluppatori riferiscono di ricevere tre o più notifiche relative al rimborso per uno stesso id di transazione. Ognuna è un evento distinto con il proprio notificationUUID. Se la tua chiave di deduplicazione è l'id della transazione, la seconda decisione sembra un duplicato della prima e non vieni mai a sapere che il rimborso è stato infine concesso. L'id della transazione raggruppa gli eventi. Non li identifica.

Quanto ti costa davvero un duplicato
Una notifica di rimborso non è una spia di stato. Scatena azioni reali: revochi l'accesso, scali un saldo di consumabile, inverti un pagamento a un creatore, chiami la App Store Server API o la Play Developer API per confermare lo stato. Esegui una di queste una seconda volta su un duplicato e il costo è reale.
Segui il denaro. Revocare l'accesso due volte è innocuo, perché l'accesso è già sparito. Scalare un saldo due volte non lo è: un utente che ha comprato un pacchetto di monete e lo ha fatto rimborsare può essere spinto a un saldo negativo che il tuo team di supporto deve poi sbrogliare a mano. Invertire un pagamento due volte recupera denaro che hai già restituito una volta, e ora devi a un creatore delle scuse e una correzione. E ogni duplicato che rielabori contro una API dello store spende quota che Google ti avverte esplicitamente di proteggere, quindi una raffica di riconsegna di Pub/Sub durante un'interruzione può spingerti nel rate limiting proprio nel giorno in cui meno te lo puoi permettere.
Le finestre di prova del rimborso alzano la posta sul lato Apple. Se una deduplicazione ingenua silenzia i CONSUMPTION_REQUEST ripetuti che Apple invia per tutta la finestra di rimborso aperta, puoi perdere quello a cui dovevi rispondere, e un CONSUMPTION_REQUEST a cui non rispondi entro 12 ore è un rimborso che Apple spesso concede per impostazione predefinita. Non è un doppio addebito. È una vendita persa più il calcolo, le chiamate API, l'archiviazione e i pagamenti che hai già speso per consegnare l'acquisto, nulla dei quali il rimborso restituisce.
| Modalità di guasto | Cosa va storto | Quanto costa |
|---|---|---|
| Riscalare un saldo su un REFUND duplicato | Il saldo di consumabile dell'utente diventa negativo | Tempo di supporto manuale per riconciliare, e una cattiva esperienza per il cliente |
| Invertire un pagamento due volte | Recuperi denaro che hai già restituito una volta | Una correzione al creatore e una pulizia contabile |
| Rielaborare contro una API dello store | Le chiamate duplicate consumano quota della Play Developer API o della App Store Server API | Rate limiting durante l'interruzione che ha causato la riconsegna |
| Deduplicare troppo i CONSUMPTION_REQUEST | Scarti un prompt di rimborso distinto come falso duplicato | Una finestra di 12 ore persa, quindi Apple concede il rimborso per impostazione predefinita |
Come gestire le notifiche di rimborso duplicate senza agire due volte
Lo schema è lo stesso su entrambi gli store, con una chiave diversa. Registra la consegna, verifica la chiave prima di agire, agisci una volta, e di' sempre allo store che l'hai ricevuta.
- Deduplica sull'id di consegna dello store, non sulla transazione. Usa
notificationUUIDper Apple e ilmessageIddi Pub/Sub per Google Play. Memorizzalo con un vincolo di unicità in modo che un duplicato concorrente perda la corsa invece di agire due volte. - Rendi l'azione a valle idempotente di per sé. Indicizzare sull'id di consegna ferma la rielaborazione, ma scrivi anche l'effetto in modo che revocare, scalare o invertire verifichi prima lo stato attuale e sia sicuro da eseguire due volte.
- Persisti prima, poi conferma la ricezione. Scrivi l'evento nel tuo database prima di restituire 200 o confermare il messaggio di Pub/Sub. Se confermi prima e la scrittura fallisce, lo store considera il messaggio consegnato e non lo invia mai più, e ora lo hai perso per sempre.
- Restituisci sempre uno stato di successo, anche per un duplicato. Un 200 a 206 per Apple, un 200 al push di Pub/Sub per Google. Rifiutare una ripetizione con un errore fa solo sì che lo store la riprovi.
- Raggruppa sull'entità, identifica sull'evento. Indicizza il tuo stato d'acquisto memorizzato sul
purchaseTokeno sull'originalTransactionIdin modo che le consegne fuori ordine aggiornino una sola riga, ma tratta ogninotificationUUIDomessageIdcome il proprio evento, perché un acquisto ne produce legittimamente diversi.
Una breve lista di controllo prima di fidarti del tuo webhook di rimborso
- Le consegne di Apple sono deduplicate su
notificationUUID, e una ripetizione non scrive nulla di nuovo ma restituisce comunque 200. - Le consegne di Google Play sono deduplicate sul
messageIddi Pub/Sub, verificato prima di qualsiasi elaborazione. - Lo stato d'acquisto è indicizzato su
purchaseTokenooriginalTransactionId, in modo che gli eventi fuori ordine atterrino su un solo record. - Ogni effetto collaterale del rimborso, revocare, scalare o invertire, è sicuro da eseguire più di una volta.
- Il tuo gestore scrive l'evento prima di confermare la ricezione, mai dopo.
- I CONSUMPTION_REQUEST ripetuti sono trattati come prompt distinti, non come duplicati, quindi nessuna finestra di rimborso aperta viene scartata.
Fai passare un duplicato attraverso il tuo webhook di proposito e osserva come non cambia nulla la seconda volta. Questo è tutto il test. Un gestore di rimborsi che è sicuro da colpire due volte è uno di cui puoi smettere di preoccuparti nel momento in cui uno store decide di colpirlo sei volte.
Domande frequenti
- Perché il mio server riceve la stessa notifica di rimborso dell'App Store più di una volta?
- Perché Apple riprova una App Store Server Notification V2 fino a cinque volte, a 1, 12, 24, 48 e 72 ore dopo l'ultimo tentativo, ogni volta che il tuo server non risponde con uno stato HTTP tra 200 e 206. Ogni nuovo tentativo porta lo stesso notificationUUID, così puoi riconoscerlo e saltarlo.
- Quale campo dovrei usare per deduplicare le App Store Server Notifications?
- Usa il notificationUUID. Un vero nuovo tentativo ripete sempre lo stesso notificationUUID, mentre ogni evento genuinamente nuovo, incluso ogni nuovo CONSUMPTION_REQUEST, ne riceve uno diverso, quindi deduplicare su notificationUUID salta le ripetizioni senza scartare eventi distinti.
- Le notifiche CONSUMPTION_REQUEST ripetute sono duplicati che dovrei ignorare?
- No. Apple invia nuove notifiche CONSUMPTION_REQUEST periodicamente per tutta la finestra di rimborso aperta, e il personale di Apple conferma che non sono nuovi tentativi. Ognuna ha il proprio notificationUUID, quindi elaborale tutte. Scartarle rischia di far perdere la finestra di 12 ore che Apple ti dà per rispondere.
- Come deduplico le Real-time Developer Notifications di Google Play?
- Leggi il messageId di Pub/Sub di ogni notifica e confrontalo con quelli che hai già elaborato prima di agire, perché Pub/Sub consegna almeno una volta e può inviare lo stesso messaggio più di una volta. Google lo raccomanda esplicitamente per evitare l'elaborazione duplicata e lo spreco di quota API.
- Dovrei restituire un errore per rifiutare una notifica di rimborso duplicata?
- No. Restituire un 4xx o un 5xx dice allo store che la consegna è fallita, quindi riprova comunque. Deduplica dentro il tuo database e restituisci sempre uno stato di successo, HTTP da 200 a 206 per Apple o una conferma 200 per il push di Google Play.
Fonti e approfondimenti
- 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
Il pilota automatico dei rimborsi per App Store e Google Play
Continua a leggere
Un rimborso di Family Sharing annulla un solo pagamento ma può lasciare altre cinque persone a usare ancora la tua app, e solo il tuo server può escluderle
Un rimborso di Family Sharing annulla un solo pagamento ma può lasciare fino a cinque membri della famiglia sulle tue funzioni a pagamento. Apple invia un REVOKE e si aspetta che sia il tuo server a terminare l'accesso. Ecco come funzionano i rimborsi condivisi in famiglia e quanto ne costa uno.
La gestione dei rimborsi si rompe in modi silenziosi, quindi testa i rimborsi degli acquisti in-app nel sandbox prima che lo faccia un cliente reale
La tua gestione dei rimborsi viene eseguita solo dopo che il cliente se n'è già andato, quindi un bug al suo interno resta invisibile finché non costa denaro reale. Entrambi gli store ti permettono di attivare prima un rimborso in un ambiente di test. Ecco come testare i rimborsi degli acquisti in-app sull'App Store e su Google Play prima che uno sia reale.