
Tutorial
September 6, 2022
24 min read
September 6, 2022
42 min read
La validación del servidor (validación del recibo (receipt validation) del lado del servidor) es una forma de verificar la autenticidad de la compra. A diferencia de la validación basada en el dispositivo, la validación del servidor se produce -espera- en el lado del servidor. La validación significa que el dispositivo o el servidor hacen una solicitud a los servidores de Apple para averiguar si la compra se ha producido realmente y si ha sido válida.
Hay que tener en cuenta que la validación del servidor no es obligatoria: las compras dentro de la aplicación seguirán funcionando sin ella. Sin embargo, tiene algunas ventajas:
Según nuestra experiencia, la primera ventaja es suficiente para establecer el proceso de compra del servidor.
En general, el proceso de validación del recibo en iOS tiene el siguiente aspecto:
Para enviar una solicitud de validación de pago, tienes que incluir el secreto compartido para autorizar la solicitud. Puedes generar uno en App Store Connect.
El secreto compartido puede crearse para una aplicación específica (secreto específico de la aplicación) o para todas las aplicaciones de la cuenta (secreto principal).
Para generar un secreto específico para la aplicación, abre la página de la aplicación en App Store Connect, ve a Compras dentro de la aplicación → Manage y haz clic en App-Specific Shared Secret. En la ventana que se abre, podrás generar un nuevo token o copiar el existente.
Para recibir el secreto de todas las aplicaciones de tu cuenta, abre la página Users y Access y selecciona la pestaña Shared Secret.
Una vez que recibas el secreto compartido, puedes enviar los recibos para que sean validados en los servidores de Apple. Esto se realiza a través de la solicitud verifyReceipt. Tienes que enviar una solicitud POST a https://buy.itunes.apple.com/verifyReceipt. En el cuerpo JSON de la solicitud, pasa el secreto compartido en el campo passwordy el recibo en el campo receipt-data. También está el parámetro opcional exclude-old-transactions. Si tiene el valor true, entonces para cada suscripción autorrenovable, recibirás sólo la última transacción en lugar del historial completo de renovaciones.
Aquí está la carga útil de la solicitud de validación de compra:
{
"password": "f4d35830e3...52aae",
"receipt-data": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"exclude-old-transactions": false
}
Si estás trabajando en un entorno Sandbox, es decir, estás probando las compras, envía solicitudes de validación a https://sandbox.itunes.apple.com/verifyReceipt. El secreto compartido, así como la carga útil y los formatos de respuesta siguen siendo los mismos.
Es importante tener en cuenta que no podrás validar un recibo creado en el entorno Sandbox en un servidor de Producción, y vicerversa. Por eso, en los sistemas del mundo real, la mejor práctica es dirigir la primera solicitud al servidor de Producción y redirigirla al servidor Sandbox en caso de que la clave de estado devuelva el código de error 21007. Este comportamiento es imprescindible durante la revisión de la aplicación, ya que permite que los empleados de Apple prueben las compras y que los usuarios reales de tu aplicación las realicen.
Entre otros errores a tener en cuenta, está el código de error 21004 que significa que estamos utilizando un secreto incorrecto. Es importante tenerlo en cuenta, ya que tiene un impacto tanto en la experiencia del usuario como en la precisión de los análisis. En el peor de los casos, la aplicación puede ser eliminada de la App Store si el usuario nunca tiene acceso a las funciones premium después de haber pagado por ellas.
Si la validación fue exitosa (estado=0), la respuesta contendrá los detalles de las transacciones del usuario.
Aquí está la respuesta de la solicitud de validación del pago:
{
"environment": "Production",
"receipt": {
"receipt_type": "Production",
"adam_id": 123,
"app_item_id": 123,
"bundle_id": "com.adapty.sample_app",
"application_version": "1",
"download_id": 123,
"version_external_identifier": 123,
"receipt_creation_date": "2021-04-28 19:42:01 Etc/GMT",
"receipt_creation_date_ms": "1619638921000",
"receipt_creation_date_pst": "2021-04-28 12:42:01 America/Los_Angeles",
"request_date": "2021-08-09 18:26:02 Etc/GMT",
"request_date_ms": "1628533562696",
"request_date_pst": "2021-08-09 11:26:02 America/Los_Angeles",
"original_purchase_date": "2017-04-09 21:18:41 Etc/GMT",
"original_purchase_date_ms": "1491772721000",
"original_purchase_date_pst": "2017-04-09 14:18:41 America/Los_Angeles",
"original_application_version": "1",
"in_app": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "1000000831360853",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1619638918000",
"purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-05-05 19:41:58 Etc/GMT",
"expires_date_ms": "1620243718000",
"expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000397200750",
"is_trial_period": "true",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED"
}
]
},
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001020690335",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-08-04 19:41:58 Etc/GMT",
"purchase_date_ms": "1628106118000",
"purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-11 19:41:58 Etc/GMT",
"expires_date_ms": "1628710918000",
"expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000438372383",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
},
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001017218955",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-07-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1627501318000",
"purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-04 19:41:58 Etc/GMT",
"expires_date_ms": "1628106118000",
"expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000849023623",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
],
"latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"pending_renewal_info": [
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
],
"status": 0
}
La respuesta es bastante engorrosa y se ha simplificado en la nueva versión de la API del servidor de App Store, pero la implementación actual no es tan difícil de conseguir.
Recuperar el estado de la suscripción y el historial de transacciones
Para saber si el usuario tiene acceso a las funciones premium de la aplicación, necesitas una forma de determinar su estado de suscripción. En la versión actual de la API no hay una petición específica para recuperar el estado de la suscripción, así que tendrás que trabajar con el historial de transacciones en cualquier caso.
El conjunto latest_receipt_info, por defecto, contiene todas las transacciones de compras dentro de la aplicación de un usuario concreto, excepto los productos consumibles que se completan en el lado de la aplicación. De este modo, puedes recuperar todo el historial de compras del usuario. Esto es bastante útil tanto para los análisis como para determinar el estado actual de la suscripción.
Al parecer, las transacciones siempre vienen primero ordenadas como las más nuevas. Sin embargo, para estar seguro, sigo recomendando que implementes tu propio ordenamiento por fecha para las transacciones.
La carga útil de la transacción:
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "1000000831360853",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1619638918000",
"purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-05-05 19:41:58 Etc/GMT",
"expires_date_ms": "1620243718000",
"expires_date_pst": "2021-05-05 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000397200750",
"is_trial_period": "true",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
Para comprobar el estado actual de la suscripción, basta con recuperar la última transacción de la cadena y ver la propiedad expires_date. La excepción sería el periodo de gracia, del que hablaremos un poco más adelante.
Para fines analíticos, recomiendo guardar las siguientes propiedades:
Apple ha anunciado en la WWDC 2021 que tiene previsto añadir un campo appAccountToken a la información de las transacciones. Contendrá el identificador del usuario en tu sistema. Este identificador debe estar en formato UUID y se define en el lado de la aplicación cuando se inicializa la compra. Si se define, se devolverá en todas las transacciones de esta cadena (renovaciones de suscripción (subscription renewal), problemas de facturación, etc.), lo que significa que te será fácil entender qué usuario ha realizado la compra.
También debes tener en cuenta el parámetro subscription_group_identifier . Si el usuario tenía previamente alguna transacción con pruebas activas u ofertas de introducción (intro offers), entonces no debería tener acceso a éstas para el mismo grupo de suscripciones. Esto debe ser rastreado en el lado del servidor.
El conjunto pending_renewal_info almacena los datos de la renovación de la suscripción. Permite comprender lo que va a ocurrir con la suscripción en el siguiente periodo de facturación. Por ejemplo, si descubres que el usuario ha optado por la renovación automática, puedes sugerirle que cambie de plan o proponerle una oferta promocional. Estos eventos (events) se pueden rastrear fácilmente con las notificaciones del servidor (server notifications), de las que hablaré en breve.
Datos de renovación de la suscripción:
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
Si el usuario no tiene productos autorrenovables, el latest_receipt_info y el pending_renewal_info las claves no serán devueltas. En este caso, las transacciones se pueden encontrar en receipt → in_app. El formato de la transacción es similar al de las transacciones autorrenovables, pero no tiene campos para la caducidad, las renovaciones, las ofertas y otras propiedades exclusivas de las transacciones autorrenovables.
Hay que tener en cuenta que receipt → in_app llegará también para las transacciones autorrenovables, pero la mejor práctica es utilizar latest_receipt_info, ya que los datos de suscripción que contiene serán los más actualizados.
Anteriormente, tenías que idear un complejo sistema para hacer un seguimiento de los cambios de estado de la suscripción. Por ejemplo, para saber si la suscripción se había renovado o no, tendrías que enviar una solicitud de estado de la suscripción a los servidores de Apple cada hora a partir de las 24 horas anteriores al vencimiento de la suscripción. Con el tiempo, Apple fue añadiendo más y más notificaciones al servidor, y ahora éstas cubren prácticamente todos los eventos importantes que tienen que ver con las suscripciones. Esto es muy útil: cuando haya algún cambio en el lado de Apple, recibirás una notificación al respecto en tu propio servidor. Es decir, recibirás notificaciones sobre nuevas compras, renovaciones de suscripciones, problemas de facturación, etc. Esto te permite recopilar un análisis mucho más preciso, además de facilitar la gestión del estado de la suscripción.
Puedes activar las notificaciones del servidor en App Store Connect. Abre la página de la aplicación y ve a General -> App information. A continuación, pon el enlace en el campo URL para las notificaciones del servidor del App Store y guarda los cambios.
Notificación del servidor:
{
"notification_type": "DID_RENEW",
"password": "f4d35830e3...52aae",
"environment": "PROD",
"auto_renew_product_id": "basic_subscription_1_month",
"auto_renew_status": "true",
"unified_receipt": {
"status": 0,
"environment": "Production",
"latest_receipt_info": [
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001020690335",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-08-04 19:41:58 Etc/GMT",
"purchase_date_ms": "1628106118000",
"purchase_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-11 19:41:58 Etc/GMT",
"expires_date_ms": "1628710918000",
"expires_date_pst": "2021-08-11 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000438372383",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
},
{
"quantity": "1",
"product_id": "basic_subscription_1_month",
"transaction_id": "230001017218955",
"original_transaction_id": "1000000831360853",
"purchase_date": "2021-07-28 19:41:58 Etc/GMT",
"purchase_date_ms": "1627501318000",
"purchase_date_pst": "2021-07-28 12:41:58 America/Los_Angeles",
"original_purchase_date": "2021-04-28 19:41:58 Etc/GMT",
"original_purchase_date_ms": "1619638918000",
"original_purchase_date_pst": "2021-04-28 12:41:58 America/Los_Angeles",
"expires_date": "2021-08-04 19:41:58 Etc/GMT",
"expires_date_ms": "1628106118000",
"expires_date_pst": "2021-08-04 12:41:58 America/Los_Angeles",
"web_order_line_item_id": "230000849023623",
"is_trial_period": "false",
"is_in_intro_offer_period": "false",
"in_app_ownership_type": "PURCHASED",
"subscription_group_identifier": "272394410"
}
],
"latest_receipt": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"pending_renewal_info": [
{
"auto_renew_status": "1",
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853"
}
]
},
"bid": "com.adapty.sample_app",
"bvrs": "0"
}
El formato de notificación al servidor es similar al de la respuesta de validación del pago. Los detalles de la transacción se guardan en unified_receipt → latest_receipt_info. La clave de la contraseña guarda el secreto compartido de tu aplicación, lo que te permite verificar la autenticidad de la solicitud. La clave notification_type guarda el tipo de evento. En mi opinión, los más útiles son:
La validación del servidor puede potenciar tus análisis de los datos que recoges de tu aplicación. También dificulta el acceso de los defraudadores a tu contenido premium, además de permitirte hacer tus suscripciones multiplataforma. Al mismo tiempo, implementar la validación del servidor puede llevar bastante tiempo, especialmente si necesitas datos muy precisos. Esto requeriría tener en cuenta muchos casos secundarios: actualización de la suscripción, crossgrade de la suscripción, períodos de prueba, ofertas de promoción/de introducción, períodos de gracia, reembolsos, suscripciones familiares, etc. Asimismo, tienes que conocer y tener en cuenta varios matices, por ejemplo, que Apple tiene una política de reducción de la comisión del 30% al 15% para las suscripciones que se renuevan regularmente durante más de un año.
Further reading
Tutorial
September 6, 2022
24 min read
Tutorial
September 6, 2022
38 min read