Tanto a Apple quanto o Google podem entregar o mesmo reembolso ao seu servidor mais de uma vez, e as notificações de reembolso duplicadas custam caro se você agir em cada uma
A Apple reenvia uma notificação de reembolso até cinco vezes e o Google Play viaja sobre o Pub/Sub com entrega ao menos uma vez, então o mesmo reembolso pode chegar ao seu servidor mais de uma vez. Veja como lidar com notificações de reembolso duplicadas sem descontar um saldo nem gastar cota de API duas vezes.

Principais conclusões
- A Apple reenvia uma App Store Server Notification V2 cinco vezes, às 1, 12, 24, 48 e 72 horas após a última tentativa, sempre que seu servidor não responde com um status HTTP entre 200 e 206. Contando a primeira tentativa, um reembolso pode chegar até seis vezes.
- Cada reenvio real da Apple carrega o mesmo notificationUUID, então esse campo, e não o id de transação, é sua chave de deduplicação.
- As Real-time Developer Notifications do Google Play viajam sobre o Cloud Pub/Sub, que garante entrega ao menos uma vez e nenhuma ordem, então a mesma mensagem pode chegar duas vezes ou fora de ordem. O Google diz para você verificar a unicidade do messageId antes de processar qualquer coisa.
- As notificações periódicas CONSUMPTION_REQUEST da Apple não são reenvios. A Apple continua enviando novas ao longo da janela de reembolso aberta, cada uma com um notificationUUID diferente, então deduplicar por notificationUUID mantém corretamente todas elas.
- O mesmo transactionId da Apple pode carregar mais de uma decisão, por exemplo um REFUND_DECLINED seguido mais tarde por um REFUND, então deduplicar apenas pelo id de transação descarta um evento distinto de que você precisava.
- Rejeitar um duplicado devolvendo um 4xx ou 5xx só faz a loja reenviá-lo. Deduplique dentro do seu próprio banco de dados e sempre retorne um status de sucesso.
- Um manipulador de reembolsos que não é idempotente age duas vezes na segunda entrega. Ele desconta um saldo duas vezes, reverte um pagamento duas vezes, ou gasta cota faturável da Play Developer API e da App Store Server API reverificando um reembolso que já fechou.
Seu servidor receberá o mesmo evento de reembolso mais de uma vez, e ambas as lojas projetaram isso assim de propósito. A Apple reenvia uma App Store Server Notification até cinco vezes quando seu endpoint não responde de forma limpa. O Google Play entrega suas Real-time Developer Notifications através do Cloud Pub/Sub, que promete entrega ao menos uma vez e nada sobre ordem. Então a pergunta nunca é se um duplicado chega. É o que seu código faz na segunda vez que vê o mesmo reembolso. Erre nisso e você desconta um saldo duas vezes, reverte um pagamento duas vezes, ou gasta cota faturável de API reverificando um reembolso que já fechou. Veja como as notificações de reembolso duplicadas realmente chegam até você, quais repetições são duplicados reais e quais apenas parecem, e como lidar com elas para que a segunda entrega saia de graça.
Uma notificação de reembolso é entregue ao menos uma vez, o que não é o mesmo que exatamente uma vez
Ambas as lojas tratam uma notificação entregue como uma promessa que continuam tentando cumprir, não como um único disparo que lançam e esquecem. Isso é bom para a confiabilidade, porque uma notificação que você perde durante um deploy chega até você mais tarde mesmo assim. É uma armadilha para a corretude, porque o mecanismo que garante que você acabe recebendo o evento também garante que às vezes você o receba duas vezes. Seu manipulador precisa ser idempotente, o que significa que a segunda e a terceira entrega de um mesmo reembolso não mudam nada que a primeira já não tenha mudado.
A Apple reenvia cinco vezes ao longo de três dias
Quando a Apple envia uma App Store Server Notification V2, ela espera que seu servidor responda com um status HTTP na faixa de 200 a 206. Qualquer outra coisa, um 4xx ou um 5xx, diz à Apple que a entrega falhou, e a Apple reenvia. O cronograma é fixo: cinco reenvios, às 1, 12, 24, 48 e 72 horas após a tentativa anterior. Contando a primeira tentativa, um evento de reembolso pode chegar até seis vezes, espalhado ao longo de aproximadamente uma semana. Cada um desses reenvios carrega o mesmo notificationUUID. Esse campo é sua chave de deduplicação. Se você já registrou um notificationUUID, a entrega que está segurando é uma repetição, e a resposta correta é não armazenar nada novo e ainda assim retornar 200.
O Google Play viaja sobre o Pub/Sub, que promete ao menos uma vez e não diz nada sobre ordem
As Real-time Developer Notifications do Google Play são publicadas em um tópico do Cloud Pub/Sub. A garantia de entrega do Pub/Sub é ao menos uma vez, e ele não oferece nenhuma garantia de ordem. Isso significa que a mesma mensagem pode ser entregue ao seu endpoint mais de uma vez, e duas mensagens para a mesma compra podem chegar fora de ordem. A própria orientação do Google é explícita: desempacote o campo base64 data, leia o messageId, e verifique que você não o viu antes de processar qualquer coisa. Um messageId duplicado é uma repetição que você pula. Duas notificações diferentes sobre uma mesma compra devem aterrissar no mesmo registro mesmo assim, então indexe seu estado armazenado também pelo purchaseToken, e deixe um evento posterior atualizar a linha que um anterior criou.
| Plataforma | Modelo de entrega | Deduplicar por | Sinal de sucesso | Se você não confirmar o recebimento |
|---|---|---|---|---|
| App Store Server Notifications V2 | Até 6 tentativas: a primeira, mais 5 reenvios às 1, 12, 24, 48, 72 horas | notificationUUID | HTTP 200 a 206 | A Apple reenvia conforme o cronograma fixo, depois para |
| Google Play RTDN sobre Pub/Sub | Ao menos uma vez, sem garantia de ordem | Pub/Sub messageId, indexado por entidade em purchaseToken | HTTP 200 ao push, ou um ack explícito | O Pub/Sub reenvia quando o prazo do ack expira |
As repetições que não são duplicados
Nem toda notificação que se parece com uma que você já viu é um reenvio. Dois comportamentos da Apple enviam eventos genuinamente novos que compartilham uma compra mas que precisam ser processados cada um, e agrupá-los com uma deduplicação ingênua descarta informação de que você precisava.
A Apple envia novos CONSUMPTION_REQUESTs, não reenvios
Durante uma solicitação de reembolso aberta sobre um consumível, a Apple não envia um único CONSUMPTION_REQUEST e espera. Ela envia novos periodicamente ao longo de toda a janela de reembolso aberta até o reembolso ser fechado. A equipe da Apple confirmou que esses não são reenvios, e o sinal revelador é o campo pelo qual você deduplica: cada novo CONSUMPTION_REQUEST carrega um notificationUUID diferente. Então uma deduplicação indexada por notificationUUID faz a coisa certa automaticamente. Ela agrupa os reenvios reais e mantém cada aviso distinto. O que você não deve fazer é deduplicar por id de transação e tipo de notificação, porque isso silenciaria todo CONSUMPTION_REQUEST depois do primeiro e custaria a você a janela de evidência de 12 horas nos que você descartou.
Uma transação pode carregar mais de uma decisão
Um único transactionId pode produzir mais de um resultado de reembolso ao longo de sua vida. A Apple pode enviar um REFUND_DECLINED e depois, mais tarde, um REFUND para a mesma transação, e desenvolvedores relatam receber três ou mais notificações relacionadas a reembolso para um mesmo id de transação. Cada uma é um evento distinto com seu próprio notificationUUID. Se sua chave de deduplicação é o id de transação, a segunda decisão parece um duplicado da primeira e você nunca fica sabendo que o reembolso acabou sendo concedido. O id de transação agrupa eventos. Ele não os identifica.

Quanto um duplicado realmente custa a você
Uma notificação de reembolso não é uma luz de status. Ela dispara ações reais: você revoga o acesso, desconta um saldo de consumível, reverte o pagamento a um criador, chama a App Store Server API ou a Play Developer API para confirmar o estado. Execute qualquer uma dessas uma segunda vez sobre um duplicado e o custo é real.
Siga o dinheiro. Revogar o acesso duas vezes é inofensivo, porque o acesso já se foi. Descontar um saldo duas vezes não é: um usuário que comprou um pacote de moedas e o reembolsou pode acabar com um saldo negativo que sua equipe de suporte terá então que desfazer à mão. Reverter um pagamento duas vezes recupera dinheiro que você já devolveu uma vez, e agora você deve a um criador um pedido de desculpas e uma correção. E cada duplicado que você reprocessa contra uma API da loja gasta cota que o Google avisa explicitamente para você proteger, então uma rajada de reentrega do Pub/Sub durante uma queda pode empurrá-lo para o limite de taxa justamente no dia em que você menos pode se dar a esse luxo.
As janelas de evidência de reembolso aumentam o que está em jogo do lado da Apple. Se uma deduplicação ingênua silencia os CONSUMPTION_REQUESTs repetidos que a Apple envia ao longo da janela de reembolso aberta, você pode perder aquele que precisava responder, e um CONSUMPTION_REQUEST que você não responde dentro de 12 horas é um reembolso que a Apple muitas vezes concede por padrão. Isso não é uma cobrança dupla. É uma venda perdida mais o processamento, as chamadas de API, o armazenamento e os pagamentos que você já gastou entregando a compra, nada do que o reembolso devolve.
| Modo de falha | O que dá errado | Quanto custa |
|---|---|---|
| Redescontar um saldo em um REFUND duplicado | O saldo de consumível do usuário fica negativo | Tempo de suporte manual para reconciliar, e uma experiência ruim para o cliente |
| Reverter um pagamento duas vezes | Você recupera dinheiro que já devolveu uma vez | Uma correção ao criador e uma limpeza contábil |
| Reprocessar contra uma API da loja | Chamadas duplicadas gastam cota da Play Developer API ou da App Store Server API | Limite de taxa durante a queda que causou a reentrega |
| Deduplicar demais os CONSUMPTION_REQUESTs | Você descarta um aviso de reembolso distinto como falso duplicado | Uma janela de 12 horas perdida, então a Apple concede o reembolso por padrão |
Como lidar com notificações de reembolso duplicadas sem agir duas vezes
O padrão é o mesmo em ambas as lojas, com uma chave diferente. Registre a entrega, verifique a chave antes de agir, aja uma vez, e sempre diga à loja que você a recebeu.
- Deduplique pelo id de entrega da loja, não pela transação. Use
notificationUUIDpara a Apple e omessageIddo Pub/Sub para o Google Play. Armazene-o com uma restrição de unicidade para que um duplicado concorrente perca a corrida em vez de agir duas vezes. - Faça a ação seguinte ser idempotente por si mesma. Indexar pelo id de entrega interrompe o reprocessamento, mas escreva também o efeito de modo que revogar, descontar ou reverter verifique primeiro o estado atual e seja seguro de executar duas vezes.
- Persista primeiro, depois confirme o recebimento. Escreva o evento no seu banco de dados antes de retornar 200 ou confirmar a mensagem do Pub/Sub. Se você confirmar primeiro e a escrita falhar, a loja considera a mensagem entregue e nunca mais a envia, e agora você a perdeu de vez.
- Sempre retorne um status de sucesso, mesmo para um duplicado. Um 200 a 206 para a Apple, um 200 ao push do Pub/Sub para o Google. Rejeitar uma repetição com um erro só faz a loja reenviá-la.
- Agrupe pela entidade, identifique pelo evento. Indexe seu estado de compra armazenado pelo
purchaseTokenou pelooriginalTransactionIdpara que entregas fora de ordem atualizem uma única linha, mas trate cadanotificationUUIDoumessageIdcomo seu próprio evento, porque uma compra legitimamente produz vários.
Uma breve lista de verificação antes de confiar no seu webhook de reembolsos
- As entregas da Apple são deduplicadas por
notificationUUID, e uma repetição não escreve nada novo mas ainda assim retorna 200. - As entregas do Google Play são deduplicadas pelo
messageIddo Pub/Sub, verificado antes de qualquer processamento. - O estado de compra é indexado por
purchaseTokenouoriginalTransactionId, para que eventos fora de ordem aterrissem em um único registro. - Cada efeito colateral de reembolso, revogar, descontar ou reverter, é seguro de executar mais de uma vez.
- Seu manipulador escreve o evento antes de confirmar o recebimento, nunca depois.
- Os CONSUMPTION_REQUESTs repetidos são tratados como avisos distintos, não como duplicados, então nenhuma janela de reembolso aberta é descartada.
Passe um duplicado pelo seu próprio webhook de propósito e observe que ele não muda nada na segunda vez. Esse é todo o teste. Um manipulador de reembolsos que é seguro de acionar duas vezes é um com que você pode parar de se preocupar no momento em que uma loja decide acioná-lo seis vezes.
Perguntas frequentes
- Por que meu servidor recebe a mesma notificação de reembolso da App Store mais de uma vez?
- Porque a Apple reenvia uma App Store Server Notification V2 até cinco vezes, às 1, 12, 24, 48 e 72 horas após a última tentativa, sempre que seu servidor não responde com um status HTTP entre 200 e 206. Cada reenvio carrega o mesmo notificationUUID, então você pode reconhecê-lo e pulá-lo.
- Qual campo devo usar para deduplicar as App Store Server Notifications?
- Use o notificationUUID. Um reenvio real sempre repete o mesmo notificationUUID, enquanto cada evento genuinamente novo, incluindo cada novo CONSUMPTION_REQUEST, recebe um diferente, então deduplicar por notificationUUID pula as repetições sem descartar eventos distintos.
- As notificações CONSUMPTION_REQUEST repetidas são duplicados que devo ignorar?
- Não. A Apple envia novas notificações CONSUMPTION_REQUEST periodicamente ao longo da janela de reembolso aberta, e a equipe da Apple confirma que esses não são reenvios. Cada uma tem seu próprio notificationUUID, então processe todas. Descartá-las corre o risco de perder a janela de 12 horas que a Apple dá para você responder.
- Como eu deduplico as Real-time Developer Notifications do Google Play?
- Leia o messageId do Pub/Sub de cada notificação e compare-o com os que você já processou antes de agir, porque o Pub/Sub entrega ao menos uma vez e pode enviar a mesma mensagem mais de uma vez. O Google recomenda isso explicitamente para evitar o processamento duplicado e o desperdício de cota de API.
- Devo retornar um erro para rejeitar uma notificação de reembolso duplicada?
- Não. Retornar um 4xx ou 5xx diz à loja que a entrega falhou, então ela reenvia mesmo assim. Deduplique dentro do seu próprio banco de dados e sempre retorne um status de sucesso, HTTP 200 a 206 para a Apple ou uma confirmação 200 para o push do Google Play.
Fontes e leituras adicionais
- 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
O piloto automático de reembolsos para App Store e Google Play
Continue lendo
Um reembolso de Family Sharing reverte um pagamento, mas pode deixar outras cinco pessoas ainda usando seu app, e só o seu servidor pode cortar o acesso delas
Um reembolso de Family Sharing reverte um pagamento, mas pode deixar até cinco familiares ainda com seus recursos pagos. A Apple envia um REVOKE e espera que o seu servidor encerre o acesso. Veja como funcionam os reembolsos compartilhados com a família e quanto um deles custa.
O tratamento de reembolsos falha de formas silenciosas, então teste os reembolsos de compras no aplicativo no sandbox antes que um cliente real faça isso
Seu tratamento de reembolsos só é executado depois que o cliente já foi embora, então um erro nele permanece invisível até custar dinheiro de verdade. As duas lojas permitem disparar um reembolso primeiro em um ambiente de teste. Veja como testar os reembolsos de compras no aplicativo na App Store e no Google Play antes que um seja real.