App Store Receipt Verification Tutorial: The Basics

August 10, 2020
Dan Burcaw
A MacBook Pro screen and keyboard glow at night
Photo by
Carlos Perez

One of the more tricky parts of adding in-app purchases to your Apple App is verifying receipts on a server.  Apple strongly recommends that you use a server to parse and validate receipts.  This can helps to reduce the risk of fraud.  But how do you handle things like the call to verifyReceipt or SKReceiptRefreshRequest?  We’ll break this all down by showing you how to create a basic server to do iOS receipt validation.

Apple’s StoreKit framework provides a mechanism for selling in-app purchases or subscriptions through the App Store.

An essential artifact, the App Store receipt, is used to verify purchases and understand purchase activity. In this multi-part series, we will go beyond Apple’s documentation to demystify the receipt by coding a simple Python receipt validation script then progressively building out a server-side receipt validation app using Python, Flask, and Docker. This will make it easy for you to modify and deploy to a cloud service like AWS, GCP, or Azure.

Before we jump into the Python, let’s quickly talk about how to access the receipt on the client-side.

Accessing the Receipt from your App

The primary way you will access an App Store receipt is from your app code.

From your Xcode project, you can use the Bundle.main.appStoreReceiptURL to access the Base 64 encoded receipt data.

Here’s an example:

The code above is not guaranteed to return a receipt. Whether a receipt is returned depends on the app build:

  • Debug or Ad-Hoc builds - The receipt will only exist after a test in-app purchase takes place.
  • App Store distribution - The receipt is created when you app is downloaded, even if it is free. Practically speaking, this means your code will always find a receipt if the user downloaded the app from the App Store.

Now that you know how to retrieve the encoded receipt, next we’ll talk about receipt validation.

Choosing a Receipt Validation Approach

Now that you know how to retrieve the encoded receipt, next you need to validate it.

It’s possible to validate a receipt from the client-side or the server-side. Server-side receipt validation is more complicated, but the benefits are numerous. Especially if you offer auto-renewing subscriptions, server-side validation is strongly encouraged. You and read more about choosing a receipt validation technique from Apple’s documentation.

An App Store receipt provides a record of the sale of an app or any purchase made from within the app, and you can authenticate purchased content by adding receipt validation code to your app or server. - Apple Developer Documentation

For this series, we’re going to employ server-side receipt validation. To do that, we're going to lean on Python, Flask, and Docker to consume an encoded receipt passed up from your app’s client code. Then, we’ll dig deeper into interpreting the decoded receipt, as well as what response to send back to your client.

If you’re ready to head straight into the details of a decoded receipt, jump to our definitive guide, an element-by-element breakdown of a decoded receipt.

First, let’s build a rudimentary script to to better understand the receipt verification workflow.

Building a Simple Command Line Receipt Validator with Python

Apple provides a verifyReceipt service to be used for server-side receipt validation. The basic request and response pattern for this service is pretty straightforward, which we can demonstrate with a simple Python CLI script that does the following:

  1. Load a encoded receipt from a local file
  2. Send a validation request to verifyReceipt
  3. Print the receipt validation status

Let’s get started!

We will be using Python 3 with standard libraries, so the first thing we need to do is import the modules we will need.

Next, we need create a global variables for the verifyReceipt endpoint. There are actually endpoints: Sandbox ( & Production ( Our script will support receipts from both environments, so let’s set global variables defining each endpoint.

To determine which endpoint needs to be used, you need to know what kind of app build was used to make the purchase.

  • Debug & Ad-Hoc builds - Generates Sandbox receipts
  • TestFlight or App Store builds  - Generate Production receipts

Since this script will take in command-line arguments, let’s create a simple method to handle sending the verify receipt request to Apple. Our method will accept several arguments including whether or not we should use the Sandbox endpoint.

Best Practice: Apple recommends first sending a receipt to Production. If the receipt is for Sandbox, the response will contain a status field with the value 21007. This is your signal to try the Sandbox endpoint instead.

Next, we need to construct a valid requestBody which consists of a JSON data structure contains the Base 64 encoded receipt and a password field which is required for auto-renewable subscriptions. To locate your app’s hexadecimal shared secret via App Store Connect, check out this guide.

Now we are ready to send the HTTP POST request to Apple.

If the request was successful, you will receive a HTTP 200 OK response code. This means we can expect to receive a JSON responseBody.  The first thing we need to inspect from the responseBody is the status field. If status is 0, the receipt is valid and many other fields will be present in the responseBody as well.  We’ll dive deeper into the various elements of the receipt later in this series.

For now, we will use the status value to print a message explaining whether the receipt was validated or not.  Just in case we don’t receive that HTTP 200 OK we were expecting, we’ll also catch and print any unexpected HTTP response codes here as well. In production, you can expect to see non-200 responses from Apple so you will need to add logic to handle this and retry if need be.

Here are the most common status values you will encounter:

  • 0 - The receipt is valid
  • 21002 - The encoded receipt passed in to the requestBody’s receipt-data property is malformed
  • 21004 - The shared secret provided in the requestBody’s password property does not match what on file with Apple
  • 21007 - The receipt is from the Sandbox environment, but it was sent to the Production verifyReceipt endpoint
  • 21008 - The receipt is from the Production environment, but it was sent to the Sandbox verifyReceipt endpoint

There are others which are much more rare that you can read about here.

Now we’re ready to prepare our command-line arguments. We expect a file containing an encoded receipt to be passed to this script. If we don’t at least see one command-line argument, let’s print a helpful message.

Let’s try to read in the encoded receipt data from the file path provided in that first argument.

We need to see if any optional command-line arguments were provided. This code supports a --secret argument to pass in the hexadecimal shared secret discussed previously. Additionally, --use_sandbox tells the script to use the Sandbox verifyReceipt endpoint. Otherwise, it will default to Production.

Finally, we construct our verify_receipt method call.

You now should have a good sense for what’s involved to send a receipt validation request to Apple and the basic response codes you can expect to encounter. Head on over to GitHub for the complete source code and examples for the Python CLI covered in this tutorial.

In the next part of this series, we dig deeper into the receipt responseBody.

Until next time, happy validating!

Sign up to our newsletter

Get the latest articles delivered straight to your inbox.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Nami® logo


Are you struggling with StoreKit?

Here are the answers that will solve your problem.

Nami® logo

Maximize your App's Potential

Accelerate app revenue with Nami subscriptions.

Portrait photo of blog author
Dan Burcaw

Dan Burcaw is Co-Founder & CEO of Nami ML. He built a top mobile app development agency responsible for some of the most elite apps on the App Store and then found himself inside the mobile marketing industry after selling his last company to Oracle.

Similar articles

Read similar articles to this one

Quotes mark


Some client stories

"We spent hours researching the best ways to implement subscriptions and after many failed attempts we found Nami. We were able to go live with subscriptions in our Apple and Android apps in a matter of days."
Client portrait
Brian Pedone
Quiet Punch
Quiet Punch
"Nami helped us achieve a cross-platform solution for managing and sellingsubscriptions on Apple and Google. The Nami platform was flexible enough to handleour business requirements for in-app purchasing, allowing us to focus on our client'score domain and domain logic.”
Client Name
Client role
Company name
"Nami helped us achieve a cross-platform solution for managing and selling subscriptions on Apple and Google. The Nami platform was flexible enough to handle our business requirements for in-app purchasing, allowing us to focus on our client's core domain and domain logic."
Melody Morgan
Director, Engineering
"We spent hours researching the best ways to implement subscriptions and after many failed attempts we found Nami. We were able to go live with subscriptions in our Apple and Android apps in a matter of days."
Brian Pedone
Quiet Punch
Quiet Punch
"It took a couple of hours to incorporate their easy to use SDK. Nami provides a monetization machine learning solution, a paywall displaying what a user can purchase, and a whole suite of other useful features. As a result, it saved me development cycles so I could focus on other important things."
Mark Lapasa
Android Developer
Toronto App Factory
Toronto App Factory
"After spending a few days trying to implement subscriptions, I found Nami ML. I was able to complete in-app subscriptions within less than 3 hours."
Tanin Rojanapiansatith
iOS Developer

The best subscription experience starts with Nami

Get connected with one of our product experts to get started with your journey with Nami today.