Las compras dentro de la aplicación de Android, parte 5: Validación de las compras del lado del servidor
Updated: marzo 20, 2023
La validación del lado del servidor (server-side) puede ayudarte a validar la autenticidad de la compra. El dispositivo hará una petición a los servidores de Google para averiguar si la compra se realizó realmente y si es válida.
En esta guía, hablaremos de cómo configurar la validación del lado del servidor para las aplicaciones de Android.
¿Por qué validar las compras?
Cabe señalar que la validación del lado del servidor no es obligatoria: las compras dentro de la aplicación seguirán funcionando sin la misma. Sin embargo, tiene algunas ventajas importantes:
- Análisis de pagos adelantados, lo que es especialmente importante para las suscripciones, ya que todo lo que ocurre después de la activación no es procesado por el dispositivo. Sin un procesamiento de compras del lado del servidor, no podrás recuperar el estado actual de la suscripción y saber si el usuario renovó la suscripción o la canceló, si hay problemas de pago, etc.
- Poder verificar la autenticidad de la compra. Estarás seguro de que la transacción no es fraudulenta, y que el usuario ha pagado realmente por tu producto.
- Suscripciones multiplataforma. Al poder comprobar el estado de la suscripción del usuario en tiempo real, podrás sincronizarlo con otras plataformas. Por ejemplo, el usuario que compró la suscripción desde un dispositivo iOS podrá utilizarla en Android, en la Web y en otras plataformas.
- Podrás controlar el acceso al contenido desde el lado del servidor, lo que te protegerá de los usuarios que intenten acceder a los datos sin estar suscritos, simplemente ejecutando solicitudes al servidor.
Según nuestra experiencia, la primera ventaja es suficiente para establecer el procesamiento de compras en el lado del servidor.
Validación del pago
Podemos resumir la validación del pago (purchase validation) en Android con este esquema:
Autenticación para las solicitudes de la API para desarrolladores de Google Play
Para utilizar Google Play Developer API, primero tendrás que generar una clave para firmar las solicitudes. En primer lugar, tendrás que vincular tu cuenta de Google Play Console (donde gestionas tu aplicación) con tu cuenta de Google Cloud (donde generarás una clave para la firma de solicitudes). Una vez que todo esté configurado, tendrás que conceder al usuario los derechos de gestión de compra. Haría falta un artículo dedicado a describir este proceso. Por suerte, ya lo describimos en una guía paso a paso que se encuentra en la documentación de Adapty.
Ten en cuenta que normalmente tendrás que esperar 24 horas o más después de generar una clave para que empiece a funcionar. Para evitarlo, sólo tienes que actualizar la descripción de cualquier producto o suscripción en la app, lo que activará instantáneamente la clave.
Utilizamos la biblioteca oficial google-api-python-client para utilizar la Google Play Developer API. Esta biblioteca está disponible para la mayoría de los lenguajes populares, y te recomiendo que la utilices ya que soporta todos los métodos que puedas necesitar.
Validación de transacciones de suscripción
A diferencia de la validación del lado del servidor de iOS, en Android, tanto la validación de las suscripciones como la de otros productos se implementan utilizando diversos métodos. Por tanto, al validar una transacción, necesitas saber si se trata de un producto o de una suscripción. En la práctica, esto significa que tendrás que transferir estos datos desde la aplicación móvil, así como mantener la marca en la base de datos en caso de que sea necesaria la revalidación del token.
La segunda diferencia importante es que mientras cada transacción tiene su propio token en Android, todas las transacciones de iOS utilizan un secreto compartido específico de la aplicación para almacenar todo el historial de transacciones. Esto significa que si quieres poder restaurar las compras del usuario en cualquier momento, tendrás que almacenar todos los tokens de compra, en lugar de elegir uno solo arbitrariamente.
Para validar la suscripción, tendrás que invocar el método purchases.subscriptions.get.Básicamente, se trata de una llamada de solicitud GET:
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}
Todos los parámetros son obligatorios:
- packageName: Identificador de la aplicación, por ejemplo, com.adapty.sample_app.
- subscriptionId: Identificador de la suscripción a validar, por ejemplo, com.adapty.sample_app.weekly_sub.
- token: Token de transacción único. Aparecerá una vez que la compra se procese en el lado de la aplicación móvil.
En primer lugar, examinemos los mensajes de error de los que debes ocuparte para asegurarte de que todo funciona como es debido:
- 400, Invalid grant: account not found: Este mensaje de error significa que la clave de autenticación de la solicitud se generó incorrectamente. Asegúrate de que tus cuentas están vinculadas, de que utilizas la correcta, que tiene suficientes permisos y de que todas las API necesarias están activadas. Consulta la sección siguiente para obtener una guía sobre cómo configurarlo todo. Observa la sugerencia sobre la actualización de la descripción del producto.
- 400, The purchase token does not match the package name: Este mensaje de error suele aparecer en las transacciones fraudulentas. Si lo ves durante las pruebas, asegúrate de que no estás utilizando un token de compra de la aplicación que pertenece a otra aplicación.
- 403, Quota exceeded for quota metric ‘Queries’ and limit ‘Queries per day’ of service ‘androidpublisher.googleapis.com’: Esto significa que se superó la cuota diaria de solicitudes de Google API. Por defecto, puedes ejecutar hasta 200.000 solicitudes al día. Esta cuota puede aumentarse, pero debería ser suficiente para la mayoría de las aplicaciones. Si te encuentras con este límite, probablemente deberías volver a comprobar la lógica de tu aplicación y asegurarte de que todo es correcto.
- 410, The subscription purchase is no longer available for query because it has been expired for too long: Este mensaje de error aparece en las transacciones en las que la suscripción caducó hace más de 60 días. No es un mensaje de error real y no debería procesarse como tal.
Transacción de suscripción
Si la validación fue exitosa, recibirás los datos de la transacción como respuesta.
Datos de la transacción (para una suscripción):
{
"expiryTimeMillis": "1631116261362",
"paymentState": 1,
"acknowledgementState": 1,
"kind": "androidpublisher#subscriptionPurchase",
"orderId": "GPA.3382-9215-9042-70164",
"startTimeMillis": "1630504367892",
"autoRenewing": true,
"priceCurrencyCode": "USD",
"priceAmountMicros": "1990000",
"countryCode": "US",
"developerPayload": ""
}
Para saber si el usuario puede acceder a las opciones premium que ofrece la aplicación, es decir, si tiene una suscripción activa, necesitas:
- Comprobar los parámetros startTimeMillis y expiryTimeMillis. La hora actual debe estar entre éstas.
- Además, tienes que asegurarte de que el parámetro paymentState no tiene el valor «0». Esto significa que la compra de la suscripción aún está pendiente, por lo tanto, aún no es necesario conceder al usuario el acceso a la función premium.
- Si la transacción tiene la propiedad autoResumeTimeMillis, la suscripción está en pausa. Esto significa que el usuario no debería tener acceso a la función premium antes de la fecha especificada.
Veamos las propiedades clave de una transacción de suscripción:
- kind: Tipo de transacción. Para la suscripción, siempre tiene el valor androidpublisher#subscriptionPurchase. Con este parámetro, puedes entender si se trata de una suscripción o de un producto, y elegir en consecuencia la lógica de procesamiento.
- paymentState: Estado de pago. Esta propiedad no está presente en las transacciones caducadas. Los valores posibles son:
0: Esta compra aún no está procesada. En algunos países, el usuario puede pagar in situ la suscripción. Es decir, el usuario iniciará la compra de la suscripción desde su dispositivo y la pagará en un terminal cercano. En general, es un caso bastante raro, pero hay que tenerlo en cuenta.
1: La suscripción se compró.
2: La suscripción está en periodo de prueba.
3: La suscripción se ampliará o reducirá en el siguiente periodo. Esto significa que el plan de suscripción debe cambiar.
- acknowledgementState: Estado de confirmación de la compra. Es un parámetro importante que reconoce si el usuario ha recibido el acceso a lo que ha pagado. El valor «0» significa que no lo han hecho, y «1» significa que lo han hecho. El desarrollador es responsable de definir este estado, lo que puede hacerse tanto en la aplicación móvil como en el servidor. Si no confirmas la compra en los 3 días siguientes a su realización, se reembolsará automáticamente. Yo recomiendo implementar esta lógica: una vez que recibas una transacción que contenga acknowledgementState=0, el parámetro será cambiado por el servidor. A continuación te explico cómo hacerlo.
- orderId: Identificador de transacción único. Cada compra o renovación de una suscripción tendrá su propio identificador, que puede utilizarse para saber si esta transacción ya se ha procesado anteriormente. Cada identificador de renovación tendrá una primera mitad constante, a la que se añaden dos puntos y el recuento de la renovación de la suscripción (subscription renewal) (que empieza por 0). Si la suscripción tenía el identificador GPA.3382-9215-9042-70164 al activarse, la primera renovación se identificará con GPA.3382-9215-9042-70164..0, la segunda con GPA.3382-9215-9042-70164..1, etc. De esta manera, puedes construir cadenas de transacciones y hacer un seguimiento del recuento de renovaciones.
- startTimeMillis: Fecha de inicio de la suscripción.
- expiryTimeMillis: Fecha de vencimiento de la suscripción.
- autoRenewing: Marca que muestra si la suscripción debe renovarse o no en el siguiente periodo.
- priceCurrencyCode: Moneda de compra en un formato de tres letras, por ejemplo, USD.
- priceAmountMicros: Precio de compra. Para obtener el valor del precio normal, divide este valor entre 1000000. Es decir, 1990000 significa en realidad 1,99.
- countryCode: País de compra en formato de dos letras, por ejemplo, US
- purchaseType: Tipo de compra. Esta clave no estará presente en la mayoría de los casos. No obstante, es importante tenerlo en cuenta, porque te ayuda a entender si la compra se hizo en un entorno Sandbox. Los valores posibles son:
0: La compra se realizó en un entorno Sandbox, por lo que no debería incluirse en los datos del análisis.
1: La compra se ha realizado con un código promocional.
- autoResumeTimeMillis: Fecha de renovación de la suscripción. Sólo está presente para las suscripciones que han sido pausadas previamente. Si este parámetro está presente, no necesitas conceder al usuario el acceso a la función premium antes de la fecha indicada.
- cancelReason: La razón por la que no se va a renovar la suscripción. Los valores posibles son:
0: El usuario ha cancelado la renovación automática de la suscripción.
1: La suscripción fue cancelada por el sistema. La causa más frecuente es un problema de facturación.
2: El usuario ha cambiado de plan de suscripción.
3: El desarrollador ha cancelado la suscripción.
- userCancellationTimeMillis: Datos de cancelación de la renovación de la suscripción. Sólo está presente si cancelReason es 0. La suscripción puede seguir activa; para asegurarte, consulta el valor del parámetro expiryTimeMillis.
- cancelSurveyResult: Objeto que almacena el motivo de la cancelación de la suscripción (subscription cancellation), que estará presente si el usuario ha dejado algún comentario al respecto.
- introductoryPriceInfo: Objeto que almacena los datos del precio de lanzamiento. Por ejemplo, puede ser una oferta especial de 1 mes con un 50% de descuento.
- promotionType: Tipo de código promocional que se ha utilizado para activar la suscripción. Los valores posibles son:
0: Código promocional de una sola vez.
1: Código promocional personalizado que puede ser aplicado por múltiples clientes. Este tipo de códigos se suele utilizar para las asociaciones de bloggers. - promotionCode: Código promocional personalizado que se ha utilizado para activar la suscripción. Este parámetro no está presente para los códigos de promoción de una sola vez.
priceChange: Objeto que almacena los datos de los futuros cambios de precios, así como si el usuario los ha aceptado.
Confirmación de la suscripción
Como ya se ha mencionado anteriormente, en caso de que la suscripción no se reconozca en los 3 días siguientes a la compra, se cancelará y se reembolsará automáticamente. Para ser sincero, no entiendo muy bien la lógica que hay detrás de esto, y nunca lo he encontrado en ningún otro sistema de procesamiento de pagos, incluido el de iOS. Aun así, si recibes una transacción que contenga acknowledgementState=0, debes confirmar la suscripción.
Para ello, tendrás que invocar el método purchases.subscriptions.acknowledge. Este método ejecuta una solicitud POST.
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}:acknowledge
Los parámetros son los mismos que para la validación de la solicitud. Si la solicitud se ejecuta con éxito, la suscripción será confirmada, lo que significa que no perderás tu dinero.
Si la suscripción aún no ha sido adquirida en su totalidad, no es necesario confirmarla.
Cancelación, revocación, reembolso y aplazamiento de la renovación de la suscripción
Aparte de la validación y confirmación de la suscripción, la Google Play Developer API también se puede utilizar para otras operaciones de suscripción. Hay que tener en cuenta que éstas son bastante escasas y que, a excepción de la renovación, están soportadas por Google Play Console. Aun así, las enumeraré para que tengas una idea general del alcance de las soluciones de la API. Todas estas solicitudes tienen los mismos parámetros requeridos que los métodos mencionados anteriormente, es decir, packageName, subscriptionId y token.
- Cancelación de la renovación (renewal cancellation). El método purchases.subscriptions.cancel. Cancela la auto-renovación de la suscripción seleccionada. Sin embargo, la suscripción seguirá estando disponible durante el periodo de facturación actual.
- Reembolso de la suscripción (subscription refund). El método purchases.subscriptions.refund. Reembolsa la suscripción. Sin embargo, el usuario seguirá conservando el acceso a la suscripción, y ésta se renovará automáticamente en el siguiente periodo. En la mayoría de los casos, también debes cancelar la suscripción al emitir un reembolso.
- Revocación de la suscripción (subscription revocation). El método purchases.subscriptions.revoke. Revoca inmediatamente la suscripción, lo que significa que el usuario no podrá acceder a las funciones premium. La suscripción no se renovará. Este método se suele invocar junto con la emisión de un reembolso.
- Aplazamiento de la compra de la suscripción (subscription purchase deferral). El método purchases.subscriptions.defer. Prolonga la suscripción hasta la fecha especificada. En la solicitud, especifica la fecha de vencimiento de la suscripción, así como la fecha por la que quieres sustituirla. Esta última debe dar lugar a un periodo de suscripción más largo que la primera.
Validación del producto (sin suscripción)
La validación del producto es similar a la validación de la suscripción. Tienes que invocar el método purchases.products.get para ejecutar la solicitud GET.
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}
Ya estamos familiarizados con todos estos parámetros al ver los ejemplos expuestos anteriormente.
Datos de la transacción (para un producto):
{
"purchaseTimeMillis": "1630529397125",
"purchaseState": 0,
"consumptionState": 0,
"developerPayload": "",
"orderId": "GPA.3374-2691-3583-90384",
"acknowledgementState": 1,
"kind": "androidpublisher#productPurchase",
"regionCode": "RU"
}
Las transacciones del producto incluyen muchas menos propiedades que las transacciones de suscripción. Veamos algunas importantes:
- kind: Tipo de transacción. En los productos, siempre tiene el valor androidpublisher#productPurchase. Con este parámetro, puedes entender si se trata de una suscripción o de un producto, y elegir en consecuencia la lógica de procesamiento.
- purchaseState: Estado de pago. Ten en cuenta que los valores clave aquí son diferentes del parámetro paymentState para las suscripciones. Los valores posibles son:
0: La compra se ha completado.
1: La compra fue cancelada. Esto significa que la compra estaba pendiente, pero el usuario nunca la pagó.
2: La compra está pendiente. En algunos países, el usuario puede pagar in situ la suscripción. Es decir, el usuario iniciará la compra de la suscripción desde su dispositivo y la pagará en un terminal cercano. En general, es un caso bastante raro, pero hay que tenerlo en cuenta.
- acknowledgementState: Estado de confirmación de la compra. Es un parámetro importante que reconoce si el usuario ha recibido el acceso a lo que ha pagado. El valor «0» significa que no lo han hecho, y «1» significa que lo han hecho. El desarrollador es responsable de definir este estado, lo que puede hacerse tanto en la aplicación móvil como en el servidor. Si no confirmas la compra en los 3 días siguientes a su realización, se reembolsará automáticamente. Yo recomiendo implementar esta lógica: una vez que recibas una transacción que contenga acknowledgementState=0, el parámetro será cambiado por el servidor. A continuación te explicaré cómo hacerlo.
- consumptionState: Estado de consumo del producto. Es lo que iOS llama «consumible». Se define en el lado de la aplicación móvil. Si tiene el valor «0», significa que el producto no se ha consumido; si es «1«, entonces sí. Si estás vendiendo acceso de por vida a tu aplicación o alguna función premium específica, entonces dicho producto no debería ser consumido, es decir, debería tener el estado 0. Si vendes monedas que el usuario puede comprar una y otra vez, esos productos deben consumirse, es decir, deben tener el estado 1. consumptionState=0 significa que el producto sólo puede comprarse una vez, mientras que consumptionState=1 significa que se puede comprar muchas veces.
- orderId: Identificador de transacción único. Cada compra o renovación de una suscripción tendrá su propio identificador, que puede utilizarse para saber si esta transacción ya se ha procesado anteriormente.
- purchaseTimeMillis: Fecha de compra.
- regionCode: País de compra en formato de dos letras, por ejemplo, US. Ten en cuenta que el nombre de este parámetro es diferente al de las suscripciones, donde se denomina countryCode.
- purchaseType: Tipo de compra. Esta clave no estará presente en la mayoría de los casos. No obstante, es importante tenerlo en cuenta, porque te ayuda a entender si la compra se hizo en un entorno Sandbox. Los valores posibles son:
0: La compra se ha realizado en un entorno Sandbox, por lo que no debería incluirse en los datos del análisis.
1: La compra se ha realizado con un código promocional.
2: La compra se ha concedido por una acción dirigida, por ejemplo, ver un anuncio dentro de la aplicación en lugar de pagar.
Como puedes ver, la validación del producto es bastante similar a la validación de la suscripción. Sin embargo, hay que tener en cuenta algunos aspectos:
- El precio no se devuelve, aunque sería muy útil para el análisis.
- Los valores del parámetro purchaseState son significativamente diferentes de los valores del parámetro paymentState que se encuentran en las suscripciones. Si no se tiene en cuenta, se producirán errores.
- regionCode se devuelve, aunque se denomine countryCode para las suscripciones.
Al igual que las compras de suscripción, las compras del producto necesitan ser confirmadas. Para ello, invoca el método purchases.products.acknowledge que ejecutará una solicitud POST
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:acknowledge
Si la compra aún no se ha completado, no es necesario confirmarla.
Seguimiento del reembolso de las suscripciones y productos
Un análisis de alta calidad es imposible sin tener en cuenta los reembolsos. Desgraciadamente, los datos de los reembolsos no están presentes en la transacción ni se solicitan como un evento separado, tal y como funciona en iOS. Para recibir la lista de transacciones reembolsadas, tendrás que invocar el método purchases.voidedpurchases.list de forma regular, por ejemplo, una vez al día. Este método ejecutará una solicitud GET:
https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/voidedpurchases
En respuesta a la solicitud, recibirás la lista de todas las transacciones reembolsadas. Te recomiendo que busques las transacciones en la base de datos por el parámetro orderId, en lugar de por el purchaseToken. En primer lugar, esto te llevará menos tiempo. En segundo lugar, todas las renovaciones de la suscripción compartirán el mismo token, y sólo tendrás que buscar el más reciente.
Notificaciones del servidor (server notifications) para las transacciones
Las notificaciones del servidor (notificaciones del desarrollador en tiempo real) te ayudan a conocer los eventos ocurridos en el lado de Google, en tu servidor, y casi en directo. Una vez configuradas, recibirás notificaciones sobre nuevas compras, renovaciones, problemas de pago, etc. Esto puede ayudarte a recopilar mejores análisis, así como a facilitar la gestión del estado de los suscriptores.
Para empezar a recibir notificaciones del servidor, tienes que crear un tema en Google Cloud Pub/Sub, que enviará las notificaciones a la dirección que desees. Este tema debe indicarse entonces en la sección de configuración de la monetización de Google Play Console. Para una guía detallada con capturas de pantalla incluidas, consulta los documentos de Adapty.
Notificación del servidor:
{
"message": {
"data": "eyJ2ZXJzaW9uIjoiMS4wIiwicGFja2FnZU5hbWUiOiJjb20uYWRhcHR5LnNhbXBsZV9hcHAiLCJldmVudFRpbWVNaWxsaXMiOiIxNjMwNTI5Mzk3MTI1Iiwic3Vic2NyaXB0aW9uTm90aWZpY2F0aW9uIjp7InZlcnNpb24iOiIxLjAiLCJub3RpZmljYXRpb25UeXBlIjo2LCJwdXJjaGFzZVRva2VuIjoiY2o3anAuQU8tSjFPelIxMjMiLCJzdWJzY3JpcHRpb25JZCI6ImNvbS5hZGFwdHkuc2FtcGxlX2FwcC53ZWVrbHlfc3ViIn19",
"messageId": "2829603729517390",
"message_id": "2829603729517390",
"publishTime": "2021-09-01T20:49:59.124Z",
"publish_time": "2021-08-04T20:49:59.124Z"
},
"subscription": "projects/935083/subscriptions/adapty-rtdn"
}
Nos interesa sobre todo la clave de datos que contiene los datos de la transacción codificados con base64. La clave messageId se puede utilizar para la deduplicación de mensajes, de modo que no tengas que procesar mensajes duplicados.
Aquí tienes una transacción en una notificación del servidor:
{
"version": "1.0",
"packageName": "com.adapty.sample_app",
"eventTimeMillis": "1630529397125",
"subscriptionNotification": {
"version": "1.0",
"notificationType": 6,
"purchaseToken": "cj7jp.AO-J1OzR123",
"subscriptionId": "com.adapty.sample_app.weekly_sub"
}
}
La clave packageName te ayuda a entender a qué aplicación pertenece este evento. La clave subscriptionId te indica de qué suscripción se trata, y purchaseToken te ayuda a encontrar la transacción concreta. Con las suscripciones, siempre buscarás la última transacción de la cadena de renovación, ya que es a la que pertenecerá este evento. La clave notificationType contiene el tipo de evento. En mi opinión, éstas son las más útiles para las suscripciones:
- (2) SUBSCRIPTION_RENEWED: La suscripción se ha renovado correctamente.
- (3) SUBSCRIPTION_CANCELED: El usuario ha desactivado la renovación automática de la suscripción. Si la renovación automática está desactivada, tendrás que intentar que el usuario vuelva a ser un suscriptor activo.
- (5) SUBSCRIPTION_ON_HOLD, (6) SUBSCRIPTION_IN_GRACE_PERIOD: La suscripción no ha podido ser renovada por problemas de pago. Deberías notificarlo al usuario para que su suscripción no se cancele automáticamente.
- (12) SUBSCRIPTION_REVOKED: la suscripción ha sido revocada. Esto significa que el usuario debe perder el acceso a las funciones premium que la suscripción le otorgaba anteriormente.
En los productos (sin suscripciones), recibirás oneTimeProductNotification en lugar de la clave subscriptionNotification. Asimismo, contendrá la clave sku en lugar de la clave subscriptionId. Además, sólo recibirás 2 tipos de eventos para los productos:
- (1) ONE_TIME_PRODUCT_PURCHASED: Compra del producto con éxito.
- (2) ONE_TIME_PRODUCT_CANCELED: La compra del producto ha sido cancelada, ya que el usuario no lo había pagado.
Conclusión
La validación del lado del servidor potencia los análisis que podrás recopilar para tu aplicación. Hace más difícil que los defraudadores accedan al contenido premium y se puede utilizar para implementar suscripciones multiplataforma. Sin embargo, la validación del lado del servidor puede llevar bastante tiempo de implementación, especialmente si se requiere una alta precisión de los datos. Para proporcionar datos de alta calidad, tendrías que tener en cuenta una variedad de casos secundarios, como la actualización de la suscripción, el crossgrade de la suscripción, los periodos de prueba, las ofertas promocionales (promo offer) y las ofertas introductorias (intro offer), el periodo de gracia, los reembolsos, etc. También tendrías que conocer y tener en cuenta todas las minucias de la política, como que Google sólo cobra una comisión del 15% (en lugar del 30%) en las suscripciones que se renuevan durante más de un año.