Implementing linkedPurchaseToken correctly to prevent duplicate subscriptions

Emilie Roberts
Aug 23, 2018 · 7 min read

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.

Image for post
Image for post

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.

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.

Image for post
Image for post

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.

Image for post
Image for post

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.

Image for post
Image for post

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.

Handling linkedPurchaseToken

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.

Image for post
Image for post

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:

Image for post
Image for post

Pseudo-code for checking linkedPurchaseToken:

You can see an example of this in the Firebase back-end of Classy Taxi, an open-source end-to-end subscription app. Specifically, see the disableReplacedSubscription method in PurchasesManager.ts.

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:

Image for post
Image for post
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
Etc.

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.

Extra security

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.

Resources

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

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.

Android Developers

The official Android Developers publication on Medium

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store