Achats intégrés sous iOS, partie 4 : validation des achats côté serveur
Updated: mars 20, 2023
Qu’est-ce que la validation d’achat par le serveur ?
La validation par le serveur (validation du reçu (receipt validation) côté serveur) est un moyen de vérifier l’authenticité de l’achat. Contrairement à la validation basée sur le dispositif, la validation du serveur se produit – attendez – du côté du serveur. La validation signifie que l’appareil ou le serveur fait une requête aux serveurs d’Apple pour savoir si l’achat a réellement eu lieu et s’il était valide.
Pourquoi valider les achats ?
Il convient de noter que la validation du serveur n’est pas obligatoire – les achats intégrés fonctionneront toujours sans elle. Il présente cependant quelques avantages :
- Analyse avancée des paiements, ce qui est particulièrement important pour les abonnements puisque tout ce qui se passe après l’activation n’est pas traité par le dispositif. Sans traitement des achats sur le serveur, vous ne pourrez pas récupérer l’état actuel de l’abonnement et savoir si l’utilisateur a renouvelé ou annulé l’abonnement, s’il y a des problèmes de facturation, etc.
- Pouvoir vérifier l’authenticité de l’achat. Vous aurez la certitude que la transaction n’est pas frauduleuse et que l’utilisateur a réellement payé votre produit.
- Abonnements multi-plateformes. Si vous pouvez vérifier l’état de l’abonnement de l’utilisateur en temps réel, vous pouvez le synchroniser avec d’autres plateformes. Par exemple, l’utilisateur qui a acheté l’abonnement à partir d’un appareil iOS pourra l’utiliser sur Android, le Web et d’autres plateformes.
- La possibilité de contrôler l’accès au contenu du côté du serveur, ce qui vous protège des utilisateurs qui tentent d’accéder aux données sans abonnement en exécutant simplement des requêtes au serveur.
D’après notre expérience, le premier avantage suffit à lui seul à mettre en place le traitement des achats par serveur.
Validation des achats
En général, le processus de validation des reçus sous iOS ressemble à ceci :
Génération du secret partagé (shared secret)
Pour envoyer une demande de validation de paiement, vous devez inclure le secret partagé pour autoriser la demande. Vous pouvez en générer un dans App Store Connect.
Le secret partagé peut être créé pour une application spécifique (secret spécifique à l’application) ou pour toutes les applications du compte (secret principal).
Pour générer un secret spécifique à l’application, ouvrez la page de l’application dans App Store Connect, allez dans Achats intégrés → Gérer et cliquez sur Secret partagé spécifique à l’application. Dans la fenêtre qui s’ouvre, vous pourrez générer un nouveau jeton ou copier le jeton existant.
Pour recevoir le secret pour toutes les applications de votre compte, ouvrez la page Users and Access et sélectionnez l’onglet Secret partagé.
Demande de validation de paiement
Une fois que vous avez reçu le secret partagé, vous pouvez envoyer des reçus pour les faire valider sur les serveurs d’Apple. Cela se fait par le biais de la requête verifyReceipt. Vous devez envoyer une requête POST à https://buy.itunes.apple.com/verifyReceipt. Dans le corps de requête JSON, transmettez le secret partagé dans le champ password et le reçu dans le champ receipt-data. Il y a aussi le paramètre optionnel exclude-old-transactions. S’il s’agit de la valeur réelle, alors pour chaque abonnement auto-renouvelable, vous recevrez uniquement la dernière transaction en lieu et place de l’historique complet des renouvellements.
Voici la charge utile de la demande de validation d’achat :
{
"password": "f4d35830e3...52aae",
"receipt-data": "MIIUVQY...4rVpL8NlYh2/8l7rk0BcStXjQ==",
"exclude-old-transactions": false
}
Si vous travaillez dans un environnement Sandbox – c’est-à-dire que vous testez des achats -, envoyez des demandes de validation à https://sandbox.itunes.apple.com/verifyReceipt. Le secret partagé, ainsi que les formats de la charge utile et de la réponse restent les mêmes.
C’est pourquoi, dans les systèmes réels, la meilleure pratique consiste à diriger la première requête vers le serveur de Production et à la rediriger vers le serveur Sandbox au cas où la clé d’état renvoie le code d’erreur 21007. C’est pourquoi, dans les systèmes réels, la meilleure pratique consiste à diriger la première requête vers le serveur de Production et à la rediriger vers le serveur Sandbox au cas où la clé d’état renvoie le code d’erreur 21007. Ce comportement est indispensable lors de la révision de l’application, car il permet aux employés d’Apple de tester les achats tout en permettant aux utilisateurs réels de votre application de les effectuer.
Parmi les autres erreurs à mentionner, il y a le code d’erreur 21004 qui signifie que l’on utilise le mauvais secret. Il est important d’en assurer le suivi, car il a un impact à la fois sur l’expérience des utilisateurs et sur la précision des analyses. Dans le pire des cas, l’application peut être retirée de l’App Store si l’utilisateur n’a jamais accès aux fonctionnalités premium après avoir payé pour celles-ci.
Si la validation a réussi (statut=0), la réponse contiendra le détail des transactions de l’utilisateur.
Voici la réponse à la demande de validation du paiement :
{
"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 réponse est assez lourde et a été simplifiée dans la nouvelle version de App Store Server API mais l’implémentation actuelle n’est pas si difficile à maîtriser.
Récupération de l’état de l’abonnement et de l’historique des transactions
Pour savoir si l’utilisateur a accès aux fonctionnalités premium de l’application, vous devez pouvoir déterminer le statut de son abonnement. La version actuelle de l’API ne comporte pas de requête dédiée à la récupération de l’état de l’abonnement. Vous devrez donc travailler avec l’historique des transactions dans tous les cas.
Le tableau latest_receipt_info contient, par défaut, toutes les transactions d’achat intégrées à l’application d’un utilisateur particulier, à l’exception des produits consommables qui sont effectués du côté de l’application. De cette façon, vous pouvez retrouver l’historique complet des achats de l’utilisateur. C’est très utile à la fois pour l’analyse et pour déterminer l’état actuel de l’abonnement.
Il semble que les transactions arrivent toujours déjà triées en premier. Pour être sûr, cependant, je recommande toujours d’implémenter votre propre tri par date pour les transactions.
La charge utile de la transaction :
{
"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"
}
Pour vérifier l’état actuel de l’abonnement, il suffit de récupérer la dernière transaction de la chaîne et de regarder la propriété expires_date. L’exception à cette règle est le délai de grâce, dont nous parlerons un peu plus tard.
À des fins d’analyse, je recommande de sauvegarder les propriétés suivantes :
- product_id : l’identifiant textuel du produit acheté.
- transaction_id : l’identifiant numérique unique de la transaction. Chaque achat ou renouvellement aura son propre identifiant qui pourra être utilisé pour comprendre si la transaction a déjà été traitée.
- original_transaction_id : l’identifiant numérique unique de la chaîne de transactions. Il correspondra à transaction_id lors de l’activation de l’abonnement ou de l’essai. Cependant, lors des renouvellements ultérieurs de l’abonnement, l’identifiant de la transaction changera, tandis que l’identifiant de la transaction originale restera le même. C’est pratique pour savoir combien de fois l’abonnement a été renouvelé.
- purchase_date и original_purchase_date : la date de la transaction et la date de la transaction originale. Cela fonctionne selon la même logique que la propriété précédente.
- expires_date: date d’expiration de l’abonnement.
- cancellation_date: la date du remboursement, pas l’annulation de l’abonnement (subscription cancellation) comme le nom pourrait le suggérer. Si la réponse contient ce champ, cela signifie que vous pouvez mettre fin à l’accès de l’utilisateur à son abonnement, ainsi qu’au compte pour l’annulation dans vos analyses – vous ne recevrez aucun paiement de cette transaction.
- is_in_intro_offer_period : Le drapeau indiquant si l’offre d’introduction a été utilisée lors de l’activation de la souscription.
- is_trial_period : le drapeau indiquant s’il y avait une période d’essai lors de l’activation de l’abonnement.
- offer_code_ref_name : le code d’offre qui a été utilisé lors de l’activation de l’abonnement.
- promotional_offer_id : l’identifiant textuel de l’offre promotionnelle (promo offer) qui a été utilisée lors de la saisie de la période de facturation actuelle.
- in_app_ownership_type : le type de propriété de l’abonnement. Il révèle si l’utilisateur a acheté le produit lui-même ou s’il l’a reçu dans le cadre de l’abonnement familial. Parmi les valeurs possibles, citons :
- PURCHASED : l’utilisateur a acheté le produit lui-même.
- IFAMILY_SHARED : l’utilisateur a reçu le produit dans le cadre de l’abonnement familial.
Apple a annoncé lors de la WWDC 2021 qu’elle prévoyait d’ajouter un champ appAccountToken aux informations sur les transactions. Il contiendra l’identifiant de l’utilisateur dans votre système. Cet identifiant doit être au format UUID et est défini du côté de l’application lorsque l’achat est initialisé. S’il est défini, il sera renvoyé dans toutes les transactions de cette chaîne (renouvellements d’abonnement (subscription renewal), problèmes de facturation, etc.), ce qui signifie qu’il vous sera facile de comprendre quel utilisateur a effectué l’achat.
Vous devez également tenir compte du paramètre subscription_group_identifier. Si l’utilisateur a déjà effectué des transactions avec des essais actifs ou des offres de lancement (intro offers), il ne devrait pas y avoir accès pour le même groupe d’abonnements. Cela devrait être suivi du côté du serveur.
Infos sur le renouvellement de l’abonnement, le délai de grâce (grace period) et les problèmes de facturation
Le tableau pending_renewal_info stocke les données de renouvellement de l’abonnement. Il permet de comprendre ce qu’il adviendra de l’abonnement lors de la prochaine période de facturation. Par exemple, si vous avez découvert que l’utilisateur a refusé le renouvellement automatique, vous pouvez lui suggérer de passer à un autre plan ou lui présenter une offre promotionnelle. Ces événements (events) peuvent être facilement suivis grâce aux notifications du serveur (server notifications), dont je parlerai bientôt.
Données relatives au renouvellement de l’abonnement :
{
"auto_renew_product_id": "basic_subscription_1_month",
"product_id": "basic_subscription_1_month",
"original_transaction_id": "1000000831360853",
"auto_renew_status": "1"
}
- product_id : l’identifiant textuel du produit qui a été acheté.
- auto_renew_product_id : l’identifiant texte du produit qui sera activé dans la prochaine période de facturation. S’il est différent de l’actuel (product_id), cela signifie que l’utilisateur a changé de type d’abonnement.
- auto_renew_status : le drapeau indiquant si l’abonnement doit être poursuivi au cours de la prochaine période de facturation.
- expiration_intent : la raison de l’expiration de l’abonnement. Parmi les valeurs possibles, citons :
- 1 – l’utilisateur a annulé lui-même l’abonnement,
- 2 – l’abonnement a été annulé en raison de problèmes de facturation (billing issue),
- 3 – l’utilisateur n’a pas accepté une augmentation de prix,
- 4 – le produit d’abonnement n’était pas disponible pour le renouvellement, par exemple s’il avait été retiré d’App Store Connect.
- 5 – une raison inconnue.
- grace_period_expires_date : la date d’expiration de la période de grâce si elle existe pour votre application. Si c’est le cas, l’utilisateur devrait avoir accès aux fonctionnalités premium jusqu’à la date indiquée ici, et non celle de la transaction elle-même. Si vous disposez de cette clé, vous pouvez informer l’utilisateur qu’il doit mettre à jour les informations de sa carte ou recharger son solde.
- is_in_billing_retry_period : le drapeau indiquant si l’abonnement a le statut de relance de facturation (billing retry status). Cela signifie que l’abonnement n’a pas été annulé, mais qu’Apple n’a pas pu facturer le paiement du renouvellement et qu’elle va continuer à essayer de le faire au cours des 60 jours.
- offer_code_ref_name : le code d’offre qui sera utilisé dans la prochaine période de facturation.
- promotional_offer_id : l’identifiant textuel de l’offre promotionnelle qui sera utilisée lors de la prochaine période de facturation.
- price_consent_status : le drapeau indiquant si l’utilisateur a accepté l’augmentation du prix de l’abonnement à venir. S’il a la valeur 0, vous devez proposer à l’utilisateur un autre produit ou une offre promotionnelle pour qu’il n’annule pas son abonnement.
Abonnements consommables, non consommables et non renouvelables
Si l’utilisateur n’a pas de produits auto-renouvelables (auto-renewable products), les clés latest_receipt_info et pending_renewal_info ne seront pas retournées. Dans ce cas, les transactions peuvent être trouvées dans receipt → in_app. Le format de la transaction est similaire à celui des transactions auto-renouvelables mais ne comporte pas de champs pour l’expiration, les renouvellements, les offres et autres propriétés exclusives aux transactions auto-renouvelables.
Il convient de noter que receipt → in_app arrivera également pour les transactions auto-renouvelables, mais la meilleure pratique consiste à utiliser latest_receipt_info, car les données d’abonnement qu’il contient seront les plus à jour.
Notifications des transactions du serveur
Il y a quelque temps, vous deviez concevoir un système complexe pour suivre les changements d’état des abonnements. Par exemple, pour savoir si l’abonnement a été renouvelé ou non, il faudrait envoyer une demande d’état d’abonnement aux serveurs Apple toutes les heures à partir de 24 heures avant l’expiration de l’abonnement. Au fil du temps, Apple a ajouté de plus en plus de notifications de serveur, et celles ci couvrent désormais pratiquement tous les événements importants qui ont trait aux abonnements. C’est très utile : dès que des changements seront apportés du côté d’Apple, vous en serez informé sur votre propre serveur. En d’autres termes, vous serez informé des nouveaux achats, des renouvellements d’abonnement, des problèmes de facturation, etc. Cela vous permet de recueillir des données analytiques beaucoup plus précises et facilite la gestion de l’état de l’abonnement.
Vous pouvez activer les notifications du serveur dans App Store Connect. Ouvrez la page de l’application et allez à General -> App information. Ensuite, placez le lien dans le champ URL pour les notifications du serveur App Store et enregistrez vos modifications.
Notification du serveur :
{
"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"
}
Le format de notification du serveur est similaire à celui de la réponse de validation du paiement. Les détails de la transaction sont stockés dans unified_receipt → latest_receipt_info. La clé du mot de passe stocke le secret partagé pour votre application, ce qui vous permet de vérifier l’authenticité de la demande. La clé notification_type stocke le type d’événement. À mon avis, les plus utiles d’entre eux sont :
- DID_CHANGE_RENEWAL_STATUS : cela signifie que l’utilisateur a désactivé ou activé le renouvellement automatique de l’abonnement (ce dernier cas est beaucoup plus rare). Si l’utilisateur a choisi de ne pas participer au renouvellement automatique, vous pouvez l’encourager à rejoindre vos abonnés actifs.
- DID_FAIL_TO_RENEW : l’abonnement n’a pas pu être renouvelé en raison de problèmes de facturation. Vous devez en informer l’utilisateur afin que l’abonnement ne soit pas annulé automatiquement.
- DID_RENEW : l’abonnement a été renouvelé avec succès.
- REFUND : l’abonnement a été remboursé. Vous devez restreindre l’accès de l’utilisateur aux fonctions premium que cet achat devait octroyer et tenir compte du remboursement (c’est-à-dire de la perte de cet argent) dans vos analyses.
Conclusion
La validation du serveur peut donner un coup de fouet à l’analyse des données que vous recueillez dans votre application. Il est également plus difficile pour les fraudeurs d’accéder à votre contenu premium, ainsi que pour vous permettre de rendre vos abonnements multiplateformes. En même temps, la mise en œuvre de la validation du serveur peut prendre beaucoup de temps, surtout si vous avez besoin de données très précises. Il faudrait pour cela tenir compte de nombreux cas particuliers : mise à niveau d’un abonnement, déclassement d’un abonnement, périodes d’essai, offres promotionnelles/de lancement, délais de grâce, remboursements, abonnements familiaux, etc. Il faut également connaître et prendre en compte diverses nuances, par exemple la politique d’Apple consistant à réduire la commission de 30 % à 15 % pour les abonnements qui sont renouvelés régulièrement pendant plus d’un an.