Todos los artículos
Deep dive8 min de lectura

Tanto Apple como Google pueden entregar el mismo reembolso a tu servidor más de una vez, y las notificaciones de reembolso duplicadas te cuestan dinero si actúas sobre cada una

Apple reintenta una notificación de reembolso hasta cinco veces y Google Play viaja sobre Pub/Sub con entrega al menos una vez, así que el mismo reembolso puede llegar a tu servidor más de una vez. Aquí tienes cómo gestionar las notificaciones de reembolso duplicadas sin descontar un saldo ni gastar cuota de API dos veces.

Muchos sobres de papel idénticos apilados sobre un escritorio oscuro con uno apartado a un lado, en representación de las notificaciones de reembolso duplicadas que llegan a tu servidor

Puntos clave

  • Apple reintenta una App Store Server Notification V2 cinco veces, a las 1, 12, 24, 48 y 72 horas después del último intento, siempre que tu servidor no responda con un estado HTTP entre 200 y 206. Contando el primer intento, un reembolso puede llegar hasta seis veces.
  • Cada reintento real de Apple lleva el mismo notificationUUID, así que ese campo, y no el id de transacción, es tu clave de deduplicación.
  • Las Real-time Developer Notifications de Google Play viajan sobre Cloud Pub/Sub, que garantiza la entrega al menos una vez y ningún orden, así que el mismo mensaje puede llegar dos veces o desordenado. Google te indica que compruebes la unicidad del messageId antes de procesar nada.
  • Las notificaciones periódicas CONSUMPTION_REQUEST de Apple no son reintentos. Apple sigue enviando nuevas a lo largo de la ventana de reembolso abierta, cada una con un notificationUUID distinto, así que deduplicar por notificationUUID conserva correctamente todas ellas.
  • El mismo transactionId de Apple puede llevar más de una decisión, por ejemplo un REFUND_DECLINED seguido más tarde de un REFUND, así que deduplicar solo por id de transacción descarta un evento distinto que necesitabas.
  • Rechazar un duplicado devolviendo un 4xx o 5xx solo hace que la tienda lo reintente. Deduplica dentro de tu propia base de datos y devuelve siempre un estado de éxito.
  • Un gestor de reembolsos que no es idempotente actúa dos veces en la segunda entrega. Descuenta un saldo dos veces, revierte un pago dos veces, o gasta cuota facturable de Play Developer API y App Store Server API volviendo a comprobar un reembolso que ya cerró.

Tu servidor recibirá el mismo evento de reembolso más de una vez, y ambas tiendas lo diseñaron así a propósito. Apple reintenta una App Store Server Notification hasta cinco veces cuando tu endpoint no responde limpiamente. Google Play entrega sus Real-time Developer Notifications a través de Cloud Pub/Sub, que promete la entrega al menos una vez y nada sobre el orden. Así que la pregunta nunca es si llega un duplicado. Es qué hace tu código la segunda vez que ve el mismo reembolso. Equivócate en eso y descontarás un saldo dos veces, revertirás un pago dos veces, o gastarás cuota facturable de API volviendo a comprobar un reembolso que ya cerraste. Aquí tienes cómo llegan realmente las notificaciones de reembolso duplicadas, qué repeticiones son duplicados reales y cuáles solo lo parecen, y cómo gestionarlas para que la segunda entrega salga gratis.

Una notificación de reembolso se entrega al menos una vez, lo cual no es lo mismo que exactamente una vez

Ambas tiendas tratan una notificación entregada como una promesa que siguen intentando cumplir, no como un disparo único que lanzan y olvidan. Eso es bueno para la fiabilidad, porque una notificación que pierdes durante un despliegue te llega igualmente más tarde. Es una trampa para la corrección, porque el mecanismo que garantiza que al final recibas el evento también garantiza que a veces lo recibas dos veces. Tu gestor tiene que ser idempotente, lo que significa que la segunda y la tercera entrega de un mismo reembolso no cambian nada que la primera no hubiera cambiado ya.

Apple reintenta cinco veces a lo largo de tres días

Cuando Apple envía una App Store Server Notification V2, espera que tu servidor responda con un estado HTTP en el rango de 200 a 206. Cualquier otra cosa, un 4xx o un 5xx, le dice a Apple que la entrega falló, y Apple reintenta. El calendario es fijo: cinco reintentos, a las 1, 12, 24, 48 y 72 horas después del intento anterior. Contando el primer intento, un evento de reembolso puede llegar hasta seis veces, repartido a lo largo de aproximadamente una semana. Cada uno de esos reintentos lleva el mismo notificationUUID. Ese campo es tu clave de deduplicación. Si ya has registrado un notificationUUID, la entrega que tienes en la mano es una repetición, y la respuesta correcta es no almacenar nada nuevo y aun así devolver 200.

Google Play viaja sobre Pub/Sub, que promete al menos una vez y no dice nada sobre el orden

Las Real-time Developer Notifications de Google Play se publican en un tema de Cloud Pub/Sub. La garantía de entrega de Pub/Sub es al menos una vez, y no ofrece ninguna garantía de orden. Eso significa que el mismo mensaje puede entregarse a tu endpoint más de una vez, y que dos mensajes para la misma compra pueden llegar desordenados. La propia guía de Google es explícita: desempaqueta el campo base64 data, lee el messageId, y comprueba que no lo has visto antes de procesar nada. Un messageId duplicado es una repetición que omites. Dos notificaciones distintas sobre una misma compra deben aterrizar igualmente en el mismo registro, así que indexa también tu estado almacenado por el purchaseToken, y deja que un evento posterior actualice la fila que creó uno anterior.

PlataformaModelo de entregaDeduplicar porSeñal de éxitoSi no confirmas la recepción
App Store Server Notifications V2Hasta 6 intentos: el primero, más 5 reintentos a las 1, 12, 24, 48, 72 horasnotificationUUIDHTTP 200 a 206Apple reintenta según el calendario fijo, luego se detiene
Google Play RTDN sobre Pub/SubAl menos una vez, sin garantía de ordenPub/Sub messageId, indexado por entidad en purchaseTokenHTTP 200 al push, o un ack explícitoPub/Sub reenvía cuando expira el plazo del ack

Las repeticiones que no son duplicados

No toda notificación que se parece a una que ya has visto es un reintento. Dos comportamientos de Apple envían eventos genuinamente nuevos que comparten una compra pero que deben procesarse cada uno, y agruparlos con una deduplicación ingenua descarta información que necesitabas.

Apple envía CONSUMPTION_REQUESTs nuevos, no reintentos

Durante una solicitud de reembolso abierta sobre un consumible, Apple no envía un único CONSUMPTION_REQUEST y espera. Envía nuevos periódicamente a lo largo de toda la ventana de reembolso abierta hasta que el reembolso se cierra. El personal de Apple ha confirmado que no son reintentos, y la señal reveladora es el campo por el que deduplicas: cada CONSUMPTION_REQUEST nuevo lleva un notificationUUID distinto. Así que una deduplicación indexada por notificationUUID hace lo correcto automáticamente. Agrupa los reintentos reales y conserva cada aviso distinto. Lo que no debes hacer es deduplicar por id de transacción y tipo de notificación, porque eso silenciaría todos los CONSUMPTION_REQUEST después del primero y te costaría la ventana de evidencia de 12 horas en los que descartaste.

Una transacción puede llevar más de una decisión

Un solo transactionId puede producir más de un resultado de reembolso a lo largo de su vida. Apple puede enviar un REFUND_DECLINED y luego, más tarde, un REFUND para la misma transacción, y los desarrolladores informan de que reciben tres o más notificaciones relacionadas con el reembolso para un mismo id de transacción. Cada una es un evento distinto con su propio notificationUUID. Si tu clave de deduplicación es el id de transacción, la segunda decisión parece un duplicado de la primera y nunca te enteras de que el reembolso se concedió al final. El id de transacción agrupa eventos. No los identifica.

Una pinza robótica levantando un paquete duplicado de una línea transportadora hacia un contenedor lateral, una imagen que representa la deduplicación de notificaciones de reembolso repetidas

Lo que realmente te cuesta un duplicado

Una notificación de reembolso no es una luz de estado. Desencadena acciones reales: revocas el acceso, descuentas un saldo de consumible, reviertes el pago a un creador, llamas a la App Store Server API o a la Play Developer API para confirmar el estado. Ejecuta cualquiera de esas una segunda vez sobre un duplicado y el coste es real.

Sigue el dinero. Revocar el acceso dos veces es inofensivo, porque el acceso ya no está. Descontar un saldo dos veces no lo es: un usuario que compró un paquete de monedas y lo reembolsó puede acabar con un saldo negativo que tu equipo de soporte tendrá luego que deshacer a mano. Revertir un pago dos veces reclama dinero que ya devolviste una vez, y ahora le debes a un creador una disculpa y una corrección. Y cada duplicado que vuelves a procesar contra una API de la tienda gasta cuota que Google te advierte explícitamente que protejas, así que una ráfaga de reentrega de Pub/Sub durante una caída puede llevarte al límite de tasa justo el día en que menos te lo puedes permitir.

Las ventanas de evidencia de reembolso elevan lo que está en juego del lado de Apple. Si una deduplicación ingenua silencia los CONSUMPTION_REQUESTs repetidos que Apple envía a lo largo de la ventana de reembolso abierta, puedes perder el que necesitabas responder, y un CONSUMPTION_REQUEST que no respondes dentro de 12 horas es un reembolso que Apple a menudo concede por defecto. Eso no es un doble cargo. Es una venta perdida más el cómputo, las llamadas a la API, el almacenamiento y los pagos que ya gastaste entregando la compra, nada de lo cual devuelve el reembolso.

Modo de falloQué sale malQué cuesta
Volver a descontar un saldo en un REFUND duplicadoEl saldo de consumible del usuario se vuelve negativoTiempo de soporte manual para reconciliar, y una mala experiencia de cliente
Revertir un pago dos vecesReclamas dinero que ya devolviste una vezUna corrección al creador y una limpieza contable
Volver a procesar contra una API de la tiendaLas llamadas duplicadas gastan cuota de Play Developer API o App Store Server APILímite de tasa durante la caída que causó la reentrega
Deduplicar de más los CONSUMPTION_REQUESTsDescartas un aviso de reembolso distinto como falso duplicadoUna ventana de 12 horas perdida, así que Apple concede el reembolso por defecto

Cómo gestionar las notificaciones de reembolso duplicadas sin actuar dos veces

El patrón es el mismo en ambas tiendas, con una clave diferente. Registra la entrega, comprueba la clave antes de actuar, actúa una sola vez, y dile siempre a la tienda que la recibiste.

  • Deduplica por el id de entrega de la tienda, no por la transacción. Usa notificationUUID para Apple y el messageId de Pub/Sub para Google Play. Almacénalo con una restricción de unicidad para que un duplicado concurrente pierda la carrera en lugar de actuar dos veces.
  • Haz que la acción posterior sea idempotente por sí misma. Indexar por el id de entrega detiene el reprocesamiento, pero escribe además el efecto de modo que revocar, descontar o revertir compruebe primero el estado actual y sea seguro ejecutarlo dos veces.
  • Persiste primero, luego confirma la recepción. Escribe el evento en tu base de datos antes de devolver 200 o confirmar el mensaje de Pub/Sub. Si confirmas primero y la escritura falla, la tienda considera el mensaje entregado y no vuelve a enviarlo nunca, y ahora lo has perdido para siempre.
  • Devuelve siempre un estado de éxito, incluso para un duplicado. Un 200 a 206 para Apple, un 200 al push de Pub/Sub para Google. Rechazar una repetición con un error solo hace que la tienda la reintente.
  • Agrupa por la entidad, identifica por el evento. Indexa tu estado de compra almacenado por el purchaseToken o el originalTransactionId para que las entregas desordenadas actualicen una sola fila, pero trata cada notificationUUID o messageId como su propio evento, porque una compra produce legítimamente varios.

Una breve lista de comprobación antes de confiar en tu webhook de reembolsos

  • Las entregas de Apple se deduplican por notificationUUID, y una repetición no escribe nada nuevo pero aun así devuelve 200.
  • Las entregas de Google Play se deduplican por el messageId de Pub/Sub, comprobado antes de cualquier procesamiento.
  • El estado de compra se indexa por purchaseToken o originalTransactionId, para que los eventos desordenados aterricen en un solo registro.
  • Cada efecto secundario de reembolso, revocar, descontar o revertir, es seguro de ejecutar más de una vez.
  • Tu gestor escribe el evento antes de confirmar la recepción, nunca después.
  • Los CONSUMPTION_REQUESTs repetidos se tratan como avisos distintos, no como duplicados, así que no se descarta ninguna ventana de reembolso abierta.

Pasa un duplicado por tu propio webhook a propósito y observa cómo no cambia nada la segunda vez. Esa es toda la prueba. Un gestor de reembolsos que es seguro golpear dos veces es uno del que puedes dejar de preocuparte en el momento en que una tienda decide golpearlo seis veces.

Preguntas frecuentes

¿Por qué mi servidor recibe la misma notificación de reembolso de App Store más de una vez?
Porque Apple reintenta una App Store Server Notification V2 hasta cinco veces, a las 1, 12, 24, 48 y 72 horas después del último intento, siempre que tu servidor no responda con un estado HTTP entre 200 y 206. Cada reintento lleva el mismo notificationUUID, así que puedes reconocerlo y omitirlo.
¿Qué campo debo usar para deduplicar las App Store Server Notifications?
Usa el notificationUUID. Un reintento real siempre repite el mismo notificationUUID, mientras que cada evento genuinamente nuevo, incluido cada CONSUMPTION_REQUEST nuevo, recibe uno distinto, así que deduplicar por notificationUUID omite las repeticiones sin descartar eventos distintos.
¿Son las notificaciones CONSUMPTION_REQUEST repetidas duplicados que debo ignorar?
No. Apple envía nuevas notificaciones CONSUMPTION_REQUEST periódicamente a lo largo de la ventana de reembolso abierta, y el personal de Apple confirma que no son reintentos. Cada una tiene su propio notificationUUID, así que procesa todas. Descartarlas arriesga perder la ventana de 12 horas que Apple te da para responder.
¿Cómo deduplico las Real-time Developer Notifications de Google Play?
Lee el messageId de Pub/Sub de cada notificación y compáralo con los que ya has procesado antes de actuar, porque Pub/Sub entrega al menos una vez y puede enviar el mismo mensaje más de una vez. Google lo recomienda explícitamente para evitar el procesamiento duplicado y el desperdicio de cuota de API.
¿Debo devolver un error para rechazar una notificación de reembolso duplicada?
No. Devolver un 4xx o 5xx le dice a la tienda que la entrega falló, así que la reintenta igualmente. Deduplica dentro de tu propia base de datos y devuelve siempre un estado de éxito, HTTP 200 a 206 para Apple o una confirmación 200 para el push de Google Play.

Fuentes y lecturas adicionales

RefundHalt

El piloto automático de reembolsos para App Store y Google Play

Seguir leyendo

La próxima solicitud de reembolso ya está en camino.

Configura RefundHalt en el tiempo que tardas en leer otro correo de soporte sobre un reembolso que no llegaste a impugnar.