Entitlements

Recuperar el acceso después de un reembolso es un trabajo que ni Apple ni Google hacen por ti

El movimiento de dinero y el fin del acceso son eventos separados en ambas tiendas, y solo uno ocurre automáticamente. Google deja la eliminación del entitlement detrás de una opción desactivada por defecto; Apple no puede tocar un consumible que ya se gastó. Todo lo que tu servidor no detecte sigue funcionando con ingresos que ya no tienes.

5 min read
Recuperar el acceso después de un reembolso es un trabajo que ni Apple ni Google hacen por ti

El movimiento de dinero y el fin del acceso son eventos separados en ambas plataformas, y solo uno de ellos ocurre automáticamente. Google deja la eliminación del entitlement detrás de una opción que está desactivada por defecto. Apple gestiona las licencias, pero no puede tocar un consumible que alguien ya gastó. Todo lo que tu servidor no limpie sigue funcionando con ingresos que ya no tienes.

Puntos clave

  • Reembolsar un pedido de Play deja intacto el acceso del cliente, a menos que indiques lo contrario. O se marca "Remove entitlement" dentro de Play Console, o tu llamada a la API incluye revoke=true. Ninguna de las dos ocurre por sí sola.

  • Los reembolsos emitidos sin esa opción de revocación están completamente ausentes de la Voided Purchases API. Cualquier proceso de revocación construido únicamente sobre ese feed los pasará por alto a todos, en silencio.

  • Las licencias de los no consumibles y las suscripciones son algo que Apple puede retirar por ti. Los consumibles se consideran entregados en el momento en que se gastan, por lo que no queda nada que StoreKit pueda retirar, de modo que restar el saldo recae en tu propio servidor.

  • Apple revierte sus propios reembolsos en ocasiones. Una notificación REFUND_REVERSED indica que un reembolso otorgado fue revertido, revocationDate vuelve a ser null, y todo lo que eliminaste debe restablecerse.

  • Nueve valores distintos conforman el voidedReason de Google, que van desde remorse y accidental_purchase hasta fraud, friendly_fraud y unacknowledged_purchase, de modo que un mismo feed tanto te indica cuándo cortar el acceso como te señala qué casos tienen olor a abuso.

  • Mientras una compra reembolsada conserve el acceso, estás financiando su cómputo, su almacenamiento y sus llamadas a la API con ingresos que ya volvieron al comprador.

Por qué el acceso sobrevive a un reembolso por defecto

Ambas empresas tratan el pago y el entitlement como operaciones separadas. El pago sigue la política de la tienda, y una vez aprobado, el reembolso queda liquidado y el libro de registros lo refleja. El entitlement es un asunto distinto, a veces opcional, a veces a cargo tuyo, con frecuencia ambas cosas según la plataforma y el tipo de producto. Ninguna tienda presupone que devolver el dinero signifique terminar el acceso, porque muchas situaciones legítimas, un reembolso parcial, un crédito de cortesía, un gesto de soporte, están pensadas para que el cliente conserve lo que compró.

Google Play: la casilla que pasa inadvertida

Play Console divide el reembolso en dos decisiones. Primero el monto, y después, de forma totalmente separada, si se marca "Remove entitlement". La mayoría de los agentes de soporte no la tocan, lo cual es razonable dado que la mayoría de las solicitudes de reembolso son genuinamente de cortesía y el cliente está destinado a conservar lo adquirido. Lo que la pantalla de confirmación nunca menciona es que esa misma casilla sin marcar es la razón por la que alguien termina conservando una suscripción o un artículo dentro de la app después de un reembolso que pretendías que fuera definitivo.

La misma trampa existe en la API

El código presenta la misma bifurcación. El endpoint orders.refund tiene un parámetro de consulta opcional llamado revoke, y establecerlo en true termina el acceso al artículo de inmediato, cancelando además todos los pagos futuros cuando se trata de una suscripción recurrente. Si omites el parámetro, obtienes false por defecto: el reembolso se registra, el registro de la compra cambia, y el cliente continúa exactamente como antes.

  • Reembolso en Play Console con "Remove entitlement" sin marcar, y el acceso continúa. El pedido tampoco llega nunca a la Voided Purchases API.

  • Reembolso en Play Console con "Remove entitlement" marcado, y el acceso termina de inmediato. El pedido sí aparece en la Voided Purchases API.

  • Llamar a orders.refund y omitir el parámetro revoke, de modo que toma el valor false por defecto, y el acceso continúa. De nuevo, nada llega a la Voided Purchases API.

  • Llamar a orders.refund con revoke=true, y el acceso termina de inmediato, con el pedido apareciendo en la Voided Purchases API.

Un proceso de revocación que lea solamente la Voided Purchases API nunca encontrará un reembolso emitido sin la marca revoke. El registro no está retrasado. Simplemente nunca llega, y el pedido queda fuera de ese feed como si el reembolso nunca hubiera ocurrido.

Esa última fila es lo que rompe las integraciones. Construir un pipeline de revocación sin vigilar manualmente Play Console es precisamente para lo que existe la Voided Purchases API, pero su alcance abarca pedidos genuinamente anulados con revocación. Los reembolsos procesados sin marcar la casilla, o sin revoke=true, son invisibles ahí por diseño, no por ningún defecto.

Entendiendo voidedReason

Las compras anuladas llegan con un voidedReason que explica la causa entre nueve valores posibles: other, remorse, not_received, defective, accidental_purchase, fraud, friendly_fraud, chargeback y unacknowledged_purchase. Tratarlo como un mero registro desperdicia su valor. Remorse y accidental_purchase describen reembolsos sin nada fuera de lo común. Fraud, friendly_fraud y chargeback merecen una cola de revisión por fraude en lugar de un revoke-and-close automático, porque las cuentas que producen esos valores se repiten con una tasa estadísticamente más alta.

Cualquiera que construya directamente contra este feed necesita conocer un número operativo: purchases.voidedpurchases.list permite 6,000 consultas diarias y 30 dentro de cualquier intervalo de 30 segundos. Los bucles de sondeo ajustados agotan esa cuota rápidamente. El diseño previsto son extracciones programadas usando tokens de paginación.

Apple: las licencias se pueden retirar, los consumibles no

App Store Server Notifications V2 entrega una notificación REFUND en el instante en que se liquida un reembolso, cubriendo consumibles, no consumibles, suscripciones de renovación automática y suscripciones sin renovación, sin distinción. Lo que debería suceder a continuación diverge por completo según el tipo de producto, porque solo dos de esos cuatro caen dentro de la propia maquinaria de revocación de StoreKit.

Por qué los consumibles quedan fuera de ella

Los entitlements permanentes pertenecen a los no consumibles y las suscripciones, y los sistemas de Apple pueden desactivarlos. Los consumibles no tienen nada de eso. Gastar uno lo marca como entregado, lo que deja a StoreKit sin nada que reclamar. Un cliente que compró 500 monedas, las gastó, y luego obtuvo un reembolso, ya recibió todo desde la perspectiva de Apple. El tuyo es el único libro de registros donde esas monedas todavía existen. Esa notificación REFUND es tu único aviso para localizar el transaction ID original correspondiente y restar el saldo tú mismo, porque nada del lado de Apple lo hará.

  • REFUND significa que una compra consumible, no consumible, de renovación automática o sin renovación ha sido reembolsada. Retira de inmediato el acceso de los no consumibles y las suscripciones, y resta tú mismo cualquier saldo de consumibles.

  • REFUND_DECLINED significa que Apple rechazó la solicitud de reembolso del cliente. No hay nada que hacer, ya que el acceso nunca estuvo en riesgo.

  • REFUND_REVERSED significa que un reembolso que Apple ya había otorgado fue revertido. Restablece todo lo que la notificación REFUND original te llevó a eliminar.

  • CONSUMPTION_REQUEST significa que Apple quiere tu evidencia antes de decidir. Responde dentro de las 12 horas, y ten en cuenta que esta llega antes de un REFUND, no después.

La notificación que casi todos ignoran

REFUND_REVERSED es la que las integraciones omiten habitualmente, ya que representa a Apple revirtiendo una decisión propia, algo que casi nadie diseña desde el principio. Su llegada devuelve revocationDate a null, lo que significa que Apple ahora vuelve a considerar la compra perfectamente válida. Todo lo que tu servidor hizo con el REFUND original, eliminar un entitlement, cancelar el registro de una suscripción, poner en cero un saldo de consumible, necesita revertirse.

No esperes que un reembolso revertido se parezca a una compra nueva, porque no lo hará. Bloqueos aplicados, acceso retirado, saldos marcados como gastados: REFUND_REVERSED es la instrucción para revertir precisamente esas cosas, y no llegará ninguna notificación de seguimiento más adelante para recordártelo.

Cerrando la brecha, paso a paso

  • Convierte revoke=true en el valor predeterminado para todas las rutas de reembolso de Play, tanto en Console como en la API, reservando la alternativa para los casos de soporte que específicamente justifiquen que el cliente conserve el acceso.

  • Consulta purchases.voidedpurchases.list de forma programada usando tokens de paginación en lugar de hacer bucles ajustados, manteniéndote por debajo de 6,000 solicitudes diarias y 30 por cada 30 segundos.

  • Envía los valores de voidedReason fraud, friendly_fraud y chargeback a una cola de revisión en lugar de un manejo automático de revoke-and-close.

  • Suscríbete a App Store Server Notifications V2 en Apple, y retira el acceso de no consumibles y suscripciones en cuanto una notificación REFUND los identifique.

  • Mantén almacenado el transaction ID original asociado a cada compra de consumible, de modo que una notificación REFUND pueda localizar el saldo exacto a restar sin que nadie tenga que buscarlo manualmente.

  • Implementa REFUND_REVERSED de forma deliberada, y registra cada revocación contra su transaction ID o order ID, de modo que una reversión restablezca exactamente lo que fue retirado.

Lo que la brecha realmente cuesta

Aquí el problema no es la equidad hacia el cliente. El problema es lo que sigue funcionando una vez que el ingreso ya se fue. Una suscripción reembolsada que todavía golpea tu API, que todavía retiene datos, que todavía consume cómputo no presenta ninguna dificultad moral; presenta una factura. La infraestructura se pagó con una compra cuyo ingreso ya devolviste. Ninguna de las dos tiendas rastrea ese gasto en tu nombre, porque desde su perspectiva el reembolso concluyó en el instante en que el dinero se movió. Controlar el lado del acceso es un gasto que te corresponde gestionar por completo.

Por eso, Refund Sensor trata la revocación como parte del pipeline de reembolsos, en lugar de algo añadido después. Un evento REFUND y su correspondiente cambio de acceso se escriben como una sola acción, y un evento REFUND_REVERSED restablece el estado anterior por sí solo, en lugar de esperar a que alguien note que un cliente quedó bloqueado de algo que Apple ya le devolvió días atrás.

Dónde están documentadas estas reglas

Preguntas frecuentes

No lo hace. El acceso termina solo cuando se marcó "Remove entitlement" en Play Console, o cuando la llamada a orders.refund incluyó revoke=true. Como ninguna de las dos es la opción predeterminada, los reembolsos se registran con frecuencia mientras los clientes siguen usando el producto.

Llega una notificación REFUND, pero StoreKit no puede revocar un consumible porque gastarlo se considera como entrega. Localizar la transacción original y restar el saldo es un trabajo que tu propio servidor debe realizar.

Significa que un reembolso que Apple ya había otorgado fue revertido. revocationDate vuelve a ser null y la compra vuelve a contar como válida. Cualquier acceso o saldo que hayas eliminado a causa del REFUND original debe restablecerse, y muchísimas integraciones nunca se suscriben a esta notificación.

Porque ese endpoint solo devuelve pedidos anulados con revocación. Los reembolsos procesados sin la opción de revocación no se incluyen, por lo que cualquier pipeline que dependa únicamente de ese feed los pasará por alto por completo.

Seis mil consultas por día, y no más de 30 dentro de cualquier ventana de 30 segundos. El endpoint está diseñado para extracciones programadas con tokens de paginación en lugar de consultas continuas.

#refunds#entitlements#voided purchases#refund_reversed#revoke access#play billing
Refund SensorRefund Sensor TeamRefund defense for App Store and Google Play developers