How to do App store IAP verification on server side with S2S(server-to-server) notifications

Explore about how to verify In-app purchases with Appstore...
Jul 27 2021 · 5 min read

Introduction

There are four types of in-app purchases provided by apple, as described here. This article describes how to verify Auto‑Renewable Subscriptions on the server side and implement S2S notifications to stay informed of our app users’ subscription statuses using Ruby on Rails.

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

There are a few gems like candy_check and venice, but here we’re doing IAP manually (using Apple’s IAP verification URL).

Here, I’m assuming that you’ve already set up your application on the App Store. Generate app shared secret ad given in the link, which will be needed later. The whole process is divided into two parts.

Receipt validation

Validation can be performed in the following steps :

Verify receipt with app store URL

The receipt validation URL is different for the sandbox and production environment, as described below:

  1. For sandbox environment https://sandbox.itunes.apple.com/verifyReceipt
  2. For production environment
    https://buy.itunes.apple.com/verifyReceipt

For details, look at Apple’s official documentation.

If your app is live on the app store already, then you might need to verify with the production URL (https://buy.itunes.apple.com/verifyReceipt), otherwise, it can be verified with the sandbox URL (https://sandbox.itunes.apple.com/verifyReceipt), but here we’ll test both the environments sequentially, in case we’re not sure about the environment in which our app live.

Know more here about the testing environment provided by Apple.

For receipt validation, create a class apple_receipt_verifier.rb.

i. Create verify_receipt method for validation as below,

def verify(receipt)
url = "https://buy.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
if response["status"] == 21007
url = "https://sandbox.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
end
return response
end

where,

  • url = verification URL
  • receipt = retrieved receipt_data from request
  • APP_STORE_SHARED_SECRET = Apple’s shared secret for the app

ii. First it will verify the requested receipt verification in a production environment, if it returns status code 21007, it’ll verify with the sandbox environment. For status code return by Apple receipt validation visit here.

def verify(receipt)
  url = "https://buy.itunes.apple.com/verifyReceipt"
  response = validate_receipt(url, receipt)
  if response["status"] == 21007 
    url = "https://sandbox.itunes.apple.com/verifyReceipt"
    response = validate_receipt(url, receipt)
  end
 return response
end

Full code for apple_receipt_verifier.rb :

require 'net/http'
class AppleReceiptVerifier

def verify(receipt)
url = "https://buy.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
if response["status"] == 21007
url = "https://sandbox.itunes.apple.com/verifyReceipt"
response = validate_receipt(url, receipt)
end
return response
end
def validate_receipt(url, receipt)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
if uri.scheme == 'https'
request = Net::HTTP::Post.new(uri.request_uri)
request["Accept"] = "application/json"
request.content_type = "application/json"
request.body = {
    "receipt-data" => receipt,
    "password" => ENV.fetch('APP_STORE_SHARED_SECRET') {
        'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
    }
}.to_json
response = http.request(request).body
return JSON.parse(response)
end
end


iii. Include the above-created apple_receipt_verifier into the verification code that will decide whether the given receipt is valid or not and accordingly update the user with the latest subscription information.

You can check the response object from receipt validation, it consists of a lot of purchase history, among which we need to select the latest receipt info for retrieving our newest subscription expiry date.

require 'apple_receipt_verifier'
response = AppleReceiptVerifier.new.verify(receipt_data)
unless response["status"] == 0
error = ""
case response["status"]
when 21002
error = "The data in the receipt_data property was malformed or the service experienced a temporary issue"
when 21004
error = "The shared secret does not match the shared secret on file for your account"

when 21003
error = "The receipt could not be authenticated"
when 21006
error = "Receipt is valid but subscription has expired"
else
  error = "Error in validating receipt"
end
return render json: {
  errors: [error, status: response["status"]]
}, status:: bad_request
else
  latest_receipt_info = get_latest_receipt_info(response)
if latest_receipt_info["expires_date_ms"].to_i < (Time.now.to_f * 1000).to_i
return render json: {
  errors: ['Subscription is expired']
}, status:: forbidden
end
end

Get the latest receipt from the receipt validation response.

def get_latest_receipt_info(receipt_data)
# use in_app array incase of latest_receipt_info not available
if receipt_data["latest_receipt_info"].blank ?
  receipt_info = receipt_data["in_app"]
else
  receipt_info = receipt_data["latest_receipt_info"]
end
# neither in_app nor latest_receipt_info is set
if receipt_info.blank ?
  return render json: {
    errors: ['latest receipt info not found']
  }, status:: forbidden
end
result = receipt_info.sort_by!{
    | x | x['expires_date_ms']
  }.reverse!
  return result[0]
end

Get a receipt from the app, parse it, and then validate it

if request.raw_post.blank ?
  return render json: {
    errors: ['empty request']
  }, status:: bad_request
end
info = JSON.parse(request.raw_post)
if info["receipt_data"].blank ?
  return render json: {
    errors: ['receipt not provided']
  }, status:: bad_request
end
receipt_data = info["receipt_data"]

Yay 🎉 🍰, we‘re done with app store receipt validation…have a look at the final code below.

def appstore_verification
require 'apple_receipt_verifier'
if request.raw_post.blank ?
  return render json: {
    errors: ['empty request']
  }, status:: bad_request
end
receipt = JSON.parse(request.raw_post)
if receipt["receipt_data"].blank ?
  return render json: {
    errors: ['receipt not provided']
  }, status:: bad_request
end
receipt_data = receipt["receipt_data"]
response = AppleReceiptVerifier.new.verify(receipt_data)
unless response["status"] == 0
error = ""
case response["status"]
when 21002
error = "The data in the receipt_data property was malformed or the service experienced a temporary issue"
when 21004
error = "The shared secret does not match the shared secret on file for your account"

when 21003
error = "The receipt could not be authenticated"
when 21006
error = "Receipt is valid but subscription has expired"
else
  error = "Error in validating receipt"
end
return render json: {
  errors: [error, status: response["status"]]
}, status:: bad_request
else
  latest_receipt_info = get_latest_receipt_info(response)
if latest_receipt_info["expires_date_ms"].to_i < (Time.now.to_f * 1000).to_i
return render json: {
  errors: ['Subscription is expired']
}, status:: forbidden
end
# prevent receipt duplication with different user account
user_with_same_receipt = User::find_by(app_store_transaction_id: latest_receipt_info["original_transaction_id"])
if !user_with_same_receipt.blank ? && user_with_same_receipt.id != @current_user.id
return render json: {
  errors: ['Duplicate receipt']
}, status:: forbidden
end
// update user with receipt validation response data
return render json: {
  message: "Appstore receipt validated successfully"
}, status:: ok
end
end

Receive notifications about subscription status 
P.S. This step is required only if implementing auto-renewable type subscriptions

Now is the time, when the App Store notifies the backend server(RoR here) if any change occurs in the subscriptions. Look at app store S2S notifications and configuration steps, before going ahead.

Appstore sends different types of events for status change, as described here, what we need to do is just receive and handle them.

It looks cumbersome task initially, but it’s very handy to be done with. let’s start the code for receiving notifications.

The main difference is we will get receipt_data (unified_receipt) from the app store, instead of the user.

Note: The App Store has a unique identifier per user(email) named original_transaction_id, which will always remain unique for a lifetime.

def appstore_change
puts "appstore_subscription_status_change_data"
if request.raw_post.blank?
   return render json: { errors: ["bad request"] }, status: :bad_request
end
puts request.raw_post
requestData = JSON.parse(request.raw_post)
receipt_info = get_latest_receipt_info(requestData["unified_receipt"])
case requestData["notification_type"]
  when "CANCEL", "DID_FAIL_TO_RENEW", "REFUND"
  when "DID_CHANGE_RENEWAL_PREF", "DID_RECOVER", "DID_RENEW"
  when "DID_CHANGE_RENEWAL_STATUS"
  when "INITIAL_BUY"
  when "INTERACTIVE_RENEWAL"
  when "PRICE_INCREASE_CONSENT"
  when "REVOKE"
else
  puts "notification is of different type than handled types"
end
 // call receipt validation method to verify latest receipt
  render json: { "message": "User's subscription info updated successfully" }, status: :ok
end

Finally, we’re done with the app store auto-renewable subscription verification!


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.