How to do Playstore IAP verification with RTDN(Real Time Developer Notification)

Explore how to verify In-app purchases with Playstore...
Jun 17 2021 · 6 min read

Introduction 

When we’ve production-ready application builds, all we want is to implement IAP for it (not for all, but on 90% of those though).

This article will dive deep into how to verify Play Store IAP using it on the server side. I had taken RoR as my backend you can implement it for other platforms by referring to it. Gems like candy_check provide inbuilt functionality for purchase token verification, but I’d like to verify it manually using Google APIs.

We are what we repeatedly do. Excellence, then, is not an act, but a habit. Try out Justly and start building your habits today!

IAP verification contains mainly two parts.

1. Verify purchase token

With an assumption, you already had configured the app on the play store, Let’s jump directly to the purchase token verification process on the server side. Following values we need handy for play store purchase token verification, If you haven’t generated them yet, then go with the flow described here to get it.

  1. App client ID (eg. 12XXXXXXXXXX-f2edxxxxxxxxxxxxxxxxxxxxxxxxxa1s2.apps.googleusercontent.com)
  2. App secret (eg. lW-W3xxxxxxxxxxxxxxxxxXq)
  3. Google Refresh token
  4. Google Access token

Here, a Google refresh token will be needed to renew the Google Access token, as it comes with a very short lifetime of 1hr.
If Initial purchase token verification returns a 400 status code, it means the Google Access Token passed into headers with the Authorization key is expired, in that case, we must renew it using the Google Refresh token. Let’s do it step by step.

a. In request, we’ll need three parameters.

payload = {
  package_name: params[:package_name],  #app pkg (eg. "com.myapp")
  product_id: params[:product_id],      #app SKU/product_id
  purchase_token: params[:purchase_token]  #purchaseToken
}

But, we have to pass a valid Google Access Token into headers with key Autorization . For that need to define the global variable $accessToken as below. Later on, we’ll override its value by renewing it if gets expires.

$accessToken = "ya29.axxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2EGD"

b. The requested purchase token will be verified using the below function, by invoking Google play store API.

# purchaseToken verification
def playstore_subscription(payload)url = "https://www.googleapis.com/androidpublisher/v3/applications/#{payload[:package_name]}/purchases/subscriptions/#{payload[:product_id]}/tokens/#{payload[:purchase_token]}"uri = URI.parse(url)
uri.port = Net::HTTP.https_default_port()
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = truerequest = Net::HTTP::Get.new(uri.request_uri,"Content-Type" => "application/json","Authorization" => "Bearer #{$accessToken}"
)response = http.request(request)
result = JSON.parse(response.body)return result
end

where, $accessToken = Google Access Token

c. Renew the Google Access token with the below method, if the above method returns a 400 status code.

client_id = ENV.fetch('GOOGLE_KEY') { '12XXXXXXXXXX-    f2edxxxxxxxxxxxxxxxxxxxxxxxxxa1s2.apps.googleusercontent.com' }client_secret = ENV.fetch('GOOGLE_SECRET') { 'lW-W3xxxxxxxxxxxxxxxxxXq' }refresh_token = ENV.fetch('GOOGLE_REFRESH_TOKEN') { '1//0423rXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXX-XXXXXXE' }# renew access-token
def renew_access_token (client_id, client_secret, refresh_token) url = "https://accounts.google.com/o/oauth2/token? grant_type=refresh_token&client_id=#{client_id}&client_secret=#{client_secret}&refresh_token=#{refresh_token}" uri = URI.parse(url)
 uri.port = Net::HTTP.https_default_port()
 http = Net::HTTP.new(uri.host, uri.port)
 http.use_ssl = true
 request = Net::HTTP::Post.new(uri.request_uri) response = http.request(request)
 result = JSON.parse(response.body) return result['access_token']     #returns renewed access-token
end

We need to repeat case b. with the renewed token, now you can see it verifies the purchase token with status code 200.

The below snippet represents the full code of Play Store IAP verification.

verification_controller.rb

# Request payload
payload = {
  package_name: params[:package_name], #app pkg (eg. "com.myapp")
  product_id: params[:product_id],     #app SKU/product_id
  purchase_token: params[:purchase_token]  #app purchase token
}

# Verify IAP 
def verify_playstore_subscription (payload)
  client_id = ENV.fetch('GOOGLE_KEY') { '12XXXXXXXXXX-    f2edxxxxxxxxxxxxxxxxxxxxxxxxxa1s2.apps.googleusercontent.com' }   client_secret = ENV.fetch('GOOGLE_SECRET') { 'lW-W3xxxxxxxxxxxxxxxxxXq' }    refresh_token = ENV.fetch('GOOGLE_REFRESH_TOKEN') { '1//0423rXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXX-XXXXXXE' } 
 
 result = playstore_subscription(payload)
  if result && !result['orderId']
   case result['error']['code']
   when 401  #renew access-token   
  new_access_token = renew_access_token(client_id, client_secret, refresh_token)if new_access_token  #override $accessToken (global variable) 
  $accessToken = new_access_token
end 

#repeat subscription verification
result = playstore_subscription(payload)  
if result && !result['orderId']
   err_code = result['error']['code']
   return err_code == 400 ? { status_code: :bad_request, data: result } : { status_code: :internal_server_error, data: result }
end when 400
 return { status_code: :bad_request, data: result }
else
return { status_code: :internal_server_error, data: result }
end
end

# Return expired subscription error
 if result['expiryTimeMillis'].to_i < (Time.now.to_f * 1000).to_i
  return { status_code: :forbidden, data: { errors: ['Subscription is expired'] } }
 end
  return { status_code: :ok, data: result }
 end
 
 # purchaseToken verification
def playstore_subscription(payload)url = "https://www.googleapis.com/androidpublisher/v3/applications/#{payload[:package_name]}/purchases/subscriptions/#{payload[:product_id]}/tokens/#{payload[:purchase_token]}"uri = URI.parse(url)
uri.port = Net::HTTP.https_default_port()
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = truerequest = Net::HTTP::Get.new(uri.request_uri,"Content-Type" => "application/json","Authorization" => "Bearer #{$accessToken}"
)response = http.request(request)
result = JSON.parse(response.body)return result
end

#renew access-token
def renew_access_token (client_id, client_secret, refresh_token)url = "https://accounts.google.com/o/oauth2/token? grant_type=refresh_token&client_id=#{client_id}&client_secret=#{client_secret}&refresh_token=#{refresh_token}"uri = URI.parse(url)
 uri.port = Net::HTTP.https_default_port()
 http = Net::HTTP.new(uri.host, uri.port)
 http.use_ssl = true
 request = Net::HTTP::Post.new(uri.request_uri)response = http.request(request)
 result = JSON.parse(response.body)return result['access_token']     #returns renewed access-token
end

$accessToken = Google Access token, which i’ve taken global (global variable in ruby starts with $ sign).

With this, we’ve completed purchase token verification and get our user info from it. Now, it’s time to go for the next step below, to continuously monitor IAP status without running any scheduling tasks or cronjob repeatedly.

2. RTDN (Real-Time Developer Notification)

Real-Time Developer Notifications allow us to react immediately to subscription state changes. For that, we need to use Google Cloud Pub/Sub which leverages RTDN by pushing notifications from the cloud to our backend server. It comes with a restricted quota though, if gets accessively used. Look at How to configure Google Cloud Pub/Sub for deep details. Kindly take note of the project_id, subscriber, and the topic from the developer console, once you’re done with the setup.

There are two types of subscriptions :

  1. Push: In this type of subscription Google Cloud will send push notifications to our pre-setted URL to the developer console.
  2. Pull: While using this case we need to set up a client library to our backend server, which will call play store developer API and pull generated notifications from there.

In this article, we’re going to implement the subscription’s Pull method.

Assuming you had all set up for Google Pub/Sub, we’ll go to the implementation now.

Since we’re using the pull method, let’s set up the google/cloud/pubsub gem, which will run the process in the background and will invoke Playstore API, for pulling notifications out of it!

a. Let’s add the required gems and files to the initializers/pub_sub.rb and verify that it gets executed on app startup!

require "google/cloud/pubsub"
require "./app/controllers/verification_controller.rb"
puts "initializing google cloud pub/sub-subscriber"

b. Initialize Google cloud PubSub with google service account key.

if ENV["GOOGLE_SERVICE_ACCOUNT_KEY"].blank?
 return
end

# initialize google cloud pub/sub
 pubsub = Google::Cloud::PubSub.new(
 project_id: 'my_project',
 credentials:ENV["GOOGLE_SERVICE_ACCOUNT_KEY"]
)
  • ENV[“GOOGLE_SERVICE_ACCOUNT_KEY”] is a service account key generated from the Google developer console, if you don’t have it already, follow these steps to generate it.
  • project_id = Get it from Google Play Store console

c. Let’s assign a subscriber of Google PubSub in order to pull notifications published over there.

subscription = pubsub.subscription "your_subscriber_name"

subscriber_name = Get it also from google developer console
for ex. projects/my_project/subscriptions/my-iap-sub

d. Finally, let’s pull notifications from the subscriber, published by PubSub topic, and update the user’s info according to it.

subscriber = subscription.listen do |received_message|# Update user according to received notification from server
User::VerificationController.new.update_user_playstore_subscription_info(received_message.message.data)# Acknowledge received message
  received_message.acknowledge!
end

P.S. : received_message.acknowledge! is a must to mark the message as received. You can get more info here.

See the full code of the pub-sub initializer below :

pub_sub.rb

require "google/cloud/pubsub"
require "./app/controllers/verification_controller.rb"
puts "initializing google cloud pub/sub-subscriber"if ENV["GOOGLE_SERVICE_ACCOUNT_KEY"].blank?
 return
end

# initialize google cloud pub/sub
 pubsub = Google::Cloud::PubSub.new(
 project_id: 'my_project',
credentials:ENV["GOOGLE_SERVICE_ACCOUNT_KEY"]
)

# assign google cloud subscriber(created using developer console)
 subscription = pubsub.subscription "your_subscriber_name"
 
 # listen published content by topic (topic_name = projects/my_project/topics/my-iap)
subscriber = subscription.listen do |received_message| 

# Update user according to received notification from server
User::VerificationController.new.update_user_playstore_subscription_info(received_message.message.data)

# Acknowledge received message
  received_message.acknowledge!
end

# Handle exceptions from listener
subscriber.on_error do |exception|
 puts "Exception: #{exception.class} #{exception.message}"
end

# all received messages have been processed or 10 seconds have passed.
at_exit do
 subscriber.stop!(10)
end

# Start background threads that will call the block passed to listen.
subscriber.start

Hurray !! Had successfully set RTDN and user update with the given info. Last but not least, Playstore pushes the same payload in every type of RTDN, which is the most important part of handling RTDNs. The sample payload received in RTDN is below.

{
  "version":"1.0",
  "packageName":"com.some.thing",
  "eventTimeMillis":"1503349566168", #notification receiving time
  "subscriptionNotification":
  {
    "version":"1.0",
    "notificationType":4, # type of notification
    "purchaseToken":"PURCHASE_TOKEN", # IAP purchaseToken
    "subscriptionId":"my.sku" #SKU/productId of your app
  }
}

You can look here for more information about notification payload too, though.

After receiving RTDN from the Google Play Store, to update users with the latest IAP status, we must again verify the user purchase token(received in RTDN), as we’re not getting any user information except the purchase token from RTDN.

That’s all about IAP verification with RTDN using RoR as a backend server.

Similar Articles


nidhi-d image
Nidhi Davra
Web developer@canopas | Gravitated towards Web | Eager to assist


nidhi-d image
Nidhi Davra
Web developer@canopas | Gravitated towards Web | Eager to assist

canopas-logo
We build products that customers can't help but love!
Get in touch

Talk to an expert
get intouch
Our team is happy to answer your questions. Fill out the form and we’ll get back to you as soon as possible
footer
Subscribe Here!
Follow us on
2024 Canopas Software LLP. All rights reserved.