BlogRight arrowTutorialRight ArrowLas compras dentro de la aplicación de iOS, parte 4: validación de las compras del lado del servidor
BlogRight arrowTutorialRight ArrowLas compras dentro de la aplicación de iOS, parte 4: validación de las compras del lado del servidor

Las compras dentro de la aplicación de iOS, parte 4: validación de las compras del lado del servidor

Las compras dentro de la aplicación de iOS, parte 4: validación de las compras del lado del servidor
Listen to the episode
Las compras dentro de la aplicación de iOS, parte 4: validación de las compras del lado del servidor

¿Qué es la validación de compra del servidor?

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.

⭐️ Download our guide on in-app techniques which will make in-app purchases in your app perfect

¿Por qué validar las compras?

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:

  1. 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. Si no hay procesamiento de compras en el servidor, no podrás recuperar el estado actual de la suscripción y saber si el usuario ha renovado la suscripción o la ha cancelado, si hay problemas de facturación, etc.
  2. 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.
  3. 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. 
  4. 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 proceso de compra del servidor.

Validación de compra

En general, el proceso de validación del recibo en iOS tiene el siguiente aspecto:

Generar el secreto compartido (shared secret)

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.

Generar un secreto compartido específico de la aplicación

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.

Cómo encontrar un secreto compartido para todas las aplicaciones

Solicitar la validación del pago

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.

Start for free

You don't need to write server code yourself,

because we did it for you. Try Adapty SDK!

Start for free

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:

  • product_id: el identificador de texto del producto comprado.
  • transaction_id: el identificador numérico único de la transacción. Cada compra o renovación tendrá su propio identificador que puede utilizarse para saber si la transacción se ha procesado previamente. 
  • original_transaction_id: el identificador numérico único de la cadena de transacciones. Coincidirá con transaction_id al activar la suscripción o la prueba. Sin embargo, al renovar la suscripción, transaction_id cambiará, mientras que original_transaction_id se mantendrá igual. Esto es útil para saber cuántas veces se ha renovado la suscripción.
  • purchase_date y original_purchase_date: la fecha de la transacción y la fecha de la transacción original. Esto funciona con la misma lógica que la propiedad anterior. 
  • expires_date: la fecha de vencimiento de la suscripción.
  • cancellation_date: la fecha del reembolso, no la de la cancelación de la suscripción (subscription cancellation), como el nombre podría sugerir. Si la respuesta tiene este campo, significa que puedes cancelar el acceso del usuario a su suscripción, así como contabilizar la cancelación en tus analíticas: no recibirás ningún pago por esta transacción.
  • is_in_intro_offer_period: la marca que muestra si la oferta de introducción se utilizó al activar la suscripción.
  • is_trial_period: la marca que muestra si hubo un periodo de prueba al activar la suscripción.
  • offer_code_ref_name: el código de oferta que se utilizó al activar la suscripción.
  • promotional_offer_id: el identificador de texto de la oferta promocional que se utilizó al ingresar al período de facturación actual.
  • in_app_ownership_type: el tipo de propiedad de la suscripción. Revela si el usuario ha comprado el producto él mismo o lo ha recibido como parte de la suscripción familiar. Algunos valores posibles son:
  • PURCHASED: el usuario ha comprado el producto por sí mismo.
  • FAMILY_SHARED: el usuario ha recibido el producto como parte de la suscripción familiar.

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.

Información de la renovación de la suscripción, período de gracia (grace period) y problemas de facturación

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"
}
  • product_id: el identificador de texto del producto que se ha comprado.
  • auto_renew_product_id: el identificador de texto del producto que se activará en el próximo periodo de facturación. Si es diferente del actual (product_id), significa que el usuario ha cambiado su tipo de suscripción.
  • auto_renew_status: la marca que muestra si la suscripción debe continuar en el siguiente periodo de facturación.
  • expiration_intent: el motivo de la caducidad de la suscripción. Algunos valores posibles son:
  • 1 - el usuario ha cancelado él mismo la suscripción,
  • 2 - la suscripción se ha cancelado por problemas de facturación (billing issue),
  • 3 - el usuario no aceptó el aumento de precio,
  • 4 - el producto de suscripción no estaba disponible para su renovación, por ejemplo, si se había eliminado de App Store Connect.
  • 5 - una razón desconocida.
  • grace_period_expires_date: la fecha de caducidad del periodo de gracia si existe para tu aplicación. Si es así, el usuario debería tener acceso a las funciones premium hasta la fecha especificada aquí, que no es la de la propia transacción. Si tienes esta clave, puedes notificar al usuario que necesita actualizar la información de su tarjeta o recargar su saldo.
  • is_in_billing_retry_period: la marca que muestra si la suscripción tiene el estado de reintento de facturación. Significa que la suscripción no se ha cancelado, sino que Apple no ha podido cargar el pago de la renovación y lo seguirá intentando durante 60 días.
  • offer_code_ref_name: el código de oferta que se utilizará en el siguiente periodo de facturación.
  • promotional_offer_id: el identificador de texto de la oferta promocional que se utilizará en el siguiente periodo de facturación.
  • price_consent_status: la marca que muestra si el usuario está de acuerdo con el próximo aumento del precio de la suscripción. Si tiene el valor 0, debes ofrecer al usuario algún producto diferente o una oferta promocional para que no cancele la suscripción.

Suscripciones consumibles, no consumibles y no renovables

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 receiptin_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.

Notificaciones de transacciones del servidor

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_receiptlatest_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:

  • DID_CHANGE_RENEWAL_STATUS: significa que el usuario desactivó o activó la renovación automática de la suscripción (esto último es mucho más raro). Si el usuario optó por la renovación automática, puedes animarle a que se reincorpore a tus suscriptores activos.
  • DID_FAIL_TO_RENEW: la suscripción no pudo ser renovada por problemas de facturación. Debes notificarlo al usuario para que la suscripción no se cancele automáticamente.
  • DID_RENEW: la suscripción se ha renovado con éxito. 
  • REFUND: la suscripción ha sido reembolsada. Deberías restringir el acceso del usuario a las funciones premium que esta compra iba a conceder y contabilizar el reembolso (es decir, la pérdida de este dinero) en tus análisis.

Conclusión

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

Adapty April Updates: all new cohorts, SearchAdsHQ integration and hiring new team members!
Adapty April Updates: all new cohorts, SearchAdsHQ integration and hiring new team members!
May 5, 2022
3 min read
Growing mobile in-app subscriptions: the right way
Growing mobile in-app subscriptions: the right way
June 22, 2020
15 min read
Mobile app paywall A/B testing: How to get started
Mobile app paywall A/B testing: How to get started
November 3, 2022
18 min read