Todos os artigos
Deep dive8 min de leitura

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.

Muitos envelopes de papel idênticos empilhados sobre uma mesa escura com um separado ao lado, representando as notificações de reembolso duplicadas que chegam ao seu servidor

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.

PlataformaModelo de entregaDeduplicar porSinal de sucessoSe você não confirmar o recebimento
App Store Server Notifications V2Até 6 tentativas: a primeira, mais 5 reenvios às 1, 12, 24, 48, 72 horasnotificationUUIDHTTP 200 a 206A Apple reenvia conforme o cronograma fixo, depois para
Google Play RTDN sobre Pub/SubAo menos uma vez, sem garantia de ordemPub/Sub messageId, indexado por entidade em purchaseTokenHTTP 200 ao push, ou um ack explícitoO 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.

Uma garra robótica levantando um pacote duplicado de uma esteira transportadora para um compartimento lateral, uma imagem que representa a deduplicação de notificações de reembolso repetidas

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 falhaO que dá erradoQuanto custa
Redescontar um saldo em um REFUND duplicadoO saldo de consumível do usuário fica negativoTempo de suporte manual para reconciliar, e uma experiência ruim para o cliente
Reverter um pagamento duas vezesVocê recupera dinheiro que já devolveu uma vezUma correção ao criador e uma limpeza contábil
Reprocessar contra uma API da lojaChamadas duplicadas gastam cota da Play Developer API ou da App Store Server APILimite de taxa durante a queda que causou a reentrega
Deduplicar demais os CONSUMPTION_REQUESTsVocê descarta um aviso de reembolso distinto como falso duplicadoUma 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 notificationUUID para a Apple e o messageId do 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 purchaseToken ou pelo originalTransactionId para que entregas fora de ordem atualizem uma única linha, mas trate cada notificationUUID ou messageId como 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 messageId do Pub/Sub, verificado antes de qualquer processamento.
  • O estado de compra é indexado por purchaseToken ou originalTransactionId, 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

RefundHalt

O piloto automático de reembolsos para App Store e Google Play

Continue lendo

A próxima solicitação de reembolso já está a caminho.

Configure a RefundHalt no tempo que você levaria para ler mais um e-mail de suporte sobre um reembolso que não conseguiu contestar.