Implementing linkedPurchaseToken correctly to prevent duplicate subscriptions
Do you use Google Play subscriptions? Make sure your back-end server implements them correctly.
The subscription REST APIs are the source of truth for managing user subscriptions. The Purchases.subscriptions API response contains an important field called linkedPurchaseToken. Proper treatment of this field is critical for ensuring the correct users have access to your content.
How does it work?
As outlined in the subscriptions documentation, every new Google Play purchase flow–initial purchase, upgrade, downgrade, and resignup¹–generates a new purchase token. The linkedPurchaseToken field makes it possible to recognize when multiple purchase tokens belong to the same subscription.
[Update, March 2021. Note: the “resignup” action is no longer a concern with the introduction of the Resubscribe feature in Google Play Billing, available to all users. linkedPurchaseToken is still important for “upgrade” and “downgrade” flows.]
For example, a user buys a subscription and receives a purchase token A. The linkedPurchaseToken field (grey circle) will not be set in the API response because the purchase token belongs to a brand new subscription.
If the user upgrades their subscription, a new purchase token B will be generated. Since the upgrade is replacing the subscription from purchase token A, the linkedPurchaseToken field for token B (shown in the grey circle) will be set to point to token A. Notice it points backwards in time to the original purchase token.
Purchase token B will be the only token that renews. Purchase token A should not be used to grant users access to your content.
Note: at the time of upgrade, both purchase token A and B will indicate they are active if you query the Google Play Billing server. We will talk about this more in the next section.
Now, let’s suppose a different user performs the following actions: subscribe, upgrade, downgrade. The original subscription will create purchase token C, the upgrade will create purchase token D, and the downgrade will create purchase token E. Each new token will link backward in time to the previous one.
Let’s add a third user to the example. This user keeps changing their mind. After an initial subscription, the user cancels and re-subscribes (does a resignup) three times in a row. The initial subscription will create purchase token F, and the resignups create G, H, and I. The purchase token I is the most recent token.
The most recent tokens–B, E, and I–represent the subscriptions that users 1, 2, and 3, respectively, are entitled to and paying for. Only these most recent tokens are valid for entitlement. However, all of the tokens in the chain are “valid” as far as Google Play is concerned, if the initial expiry date has not yet passed.
In other words, if you query the Subscriptions Get API for any of the tokens, including A, C, D, F, G, or H in the diagram above, you will get a Subscription Resource Response that indicates that the subscription has not expired and that the payment has been received, even though you should only grant entitlement for the latest tokens.
This may seem odd at first: why would the original tokens appear to be valid even after they have been upgraded? The short answer is that this implementation offers developers more flexibility when providing content and services to their users and helps Google protect user privacy. However, it does require you to do some important bookkeeping on your back-end server.
Every time you verify a subscription, your back-end should check if the linkedPurchaseToken field is set. If it is, the value in that field represents the previous token that has now been replaced. You should immediately mark that previous token as invalid so that users cannot use it to access your content.
So for User 1 in the example above, when the back-end receives the purchase token A for the initial purchase, with an empty linkedPurchaseToken field, it enables entitlement for that token. Later, when the back-end receives the new purchase token B after the upgrade, it checks the linkedPurchaseToken field, sees that it is set to A, and disables entitlement for purchase token A.
In this way, the back-end database is always kept up-to-date with which purchase tokens are valid for entitlement. In the case of User 3, the state of the database would evolve as follows:
Pseudo-code for checking linkedPurchaseToken:
Clean up an existing database
Now your back-end will be kept up-to-date with new, incoming purchase tokens, you will check each new purchase for the linkedPurchaseToken field, and any tokens corresponding to a replaced subscription will be correctly disabled. Awesome!
But what if you have an existing database of subscriptions which did not account for the linkedPurchaseToken field? You will need to run a one-time clean-up algorithm on your existing database.
In many cases, the most important thing when cleaning up a database is whether or not a given token is entitled to content/services. In other words: it may not be necessary to recreate the upgrade/downgrade/resignup purchase history for each subscription, only to determine the correct entitlement for each individual token. A one-time clean-up of the database will get things into shape and, moving forward, new incoming subscriptions just need to be handled as described in the previous section.
Imagine the purchase tokens for our three users above are stored in a database. These purchases may have happened over time and could appear in any order. If the clean-up function does this right, tokens B, E, and I should end up marked as valid for entitlement and all the other tokens should be disabled.
Pass one time through the database and check each element. If the linkedPurchaseToken field is set, then disable the token contained in that field. For the diagram below, we move through from top to bottom:
Element A: linkedPurchaseToken not set, move to next
Element D: linkedPurchaseToken == C, disable C
Element G: linkedPurchaseToken == F, disable F
Element E: linkedPurchaseToken == D, disable D
Element F: linkedPurchaseToken not set, move to next
Pseudo-code for cleaning-up existing database:
After running this one-time clean up, all the old tokens will be disabled and your database will be ready to go.
To further help protect against suspicious activity, it is also a good idea to set the accountId field in your app using the BillingFlowParams.Builder’s setAccountId method. You should set this to a queryable value that is unique to each user but that obfuscates any user data, like a one-way secure hash of the user’s account name.
Simple but important
Now that you understand how the linkedPurchaseToken field works, make sure to handle it correctly in your back-end. Every app with subscriptions should be checking this field. Correctly keeping track of entitlement is crucial to ensuring the right user is granted the right entitlement at the right time.
- Google Play Billing Library
- Subscription upgrades and downgrades
- Subscriptions API
- ClassyTaxi end-to-end subscriptions sample app
¹Resignup refers to when a user subscribes, cancels their subscription, and then re-subscribes before the original subscription has expired. Although they have not lost entitlement and the new subscription will be the same as the previous one, they will go through another purchase flow as they are committing to future payments. They will receive a new purchase token and the linkedPurchaseToken field will be set, as in the case of an upgrade or downgrade. Update: note, this occurs for a resignup from within an application only. If a user re-subscribes from the Google Play Store Subscription Center, a new purchase token will not be issued and this field will not be set — the original token will be used.
All code found here is licensed under the Apache 2.0 license. Nothing here is part of any official Google product and is for reference only.
Token image at start of article was copied from this url. Attribution: The Portable Antiquities Scheme/ The Trustees of the British Museum. Licensed under the Creative Commons Attribution-Share Alike 2.0 Generic license.