Skip to main content
Unveily
Back to blog

Verifying In-App Purchases Server-Side: Google Play & App Store

By ActuallyWorks10 min read
  • In-App Purchase
  • Google Play
  • App Store
  • Security

Say your app sells a subscription or a one-time unlock. The user finishes paying, and the app receives a "purchase successful" result. So — do you unlock the feature right away?

If you do, you may have a problem. If you trust only what the app tells your server, a modified app on a rooted or jailbroken device can claim "purchase successful" without ever paying. That is why you need server-side verification.

First — what is server-side verification?

When a purchase finishes in the app, Google Play or the App Store hands the app some information that proves the purchase. But it is not safe for the app to check that information and unlock a paid feature by itself.

Instead, the app sends the purchase information to your server, and your server asks Google or Apple whether the payment is real.

In plain terms:

Your app
   ↓  "I paid."
Your server
   ↓  "Google / Apple — was this really paid?"
Google / Apple

That round-trip is server-side purchase verification. Let's see how to do it on Google Play and the App Store.

The basic flow

The overall shape is the same on both platforms:

  1. The user pays inside the app (Google Play Billing on Android, StoreKit on iOS).
  2. The app sends the purchase info to your server — usually a purchase token on Android, and transaction info on iOS.
  3. Your server calls Google's or Apple's API to confirm it is a real purchase.
  4. If it is valid — and you have not processed it yet — you grant the user the entitlement (the thing they bought). For a "Pro unlock" you turn on Pro; for a subscription you give access for the subscription period.

The one rule that matters:

Never treat the client as the final proof of payment. The app passes along the purchase info; the final decision must be based on what Google or Apple confirms.

Google Play

Preparing your server to reach Google Play

First, your server needs permission to ask Google Play about purchases. For that you use a Google Cloud service account — a dedicated account your server uses to reach the Google Play API.

You connect the service account to your Play Console and enable the Google Play Developer API. In code, you request the androidpublisher scope — the permission that lets your server use Google Play's app and billing APIs.

import { google } from "googleapis";

const auth = new google.auth.GoogleAuth({
  keyFile: "service-account.json",
  scopes: ["https://www.googleapis.com/auth/androidpublisher"],
});
const androidpublisher = google.androidpublisher({ version: "v3", auth });

Keep the service account's key file on the server only. Never put it inside the app or in a public repository.

Verifying a one-time product

When a user buys a one-time product, Google Play issues a purchase token. The app sends that token to your server, and your server uses the purchases.products.get API to ask Google Play: "does this purchase really exist and is it paid?"

const res = await androidpublisher.purchases.products.get({
  packageName: "com.example.app",
  productId: "pro_unlock",
  token: purchaseToken,
});

if (res.data.purchaseState === 0) {
  // The purchase is real and complete
  grantEntitlement(); // remember purchaseToken so you don't process it twice
}

purchaseState tells you the state of the purchase:

  • 0 : purchased
  • 1 : canceled
  • 2 : pending

If the payment is pending, do not unlock anything yet. Grant the entitlement only after it becomes purchased.

After verifying, you must acknowledge

On Google Play there is one more step after you verify and grant the feature: acknowledge. This tells Google "I have handled this purchase properly." It does not mean asking the user to approve the payment again.

  • For a non-consumable product, call purchases.products.acknowledge on the server.
  • For a consumable product, calling consume also acknowledges it.

If you do not acknowledge within 3 days, Google Play automatically refunds the purchase — so do not skip it.

Subscriptions are a little different

A subscription is not a one-and-done payment. It may be fine today and change later — renewed, failed payment, in a grace period, canceled, expired, or resubscribed.

Your server checks the current state with purchases.subscriptionsv2.get:

const sub = await androidpublisher.purchases.subscriptionsv2.get({
  packageName: "com.example.app",
  token: purchaseToken,
});
// read sub.data.subscriptionState — active, expired, in grace period, ...

A grace period is when a renewal payment failed, but Google keeps the subscription alive for a short time while it retries. So you must not check a subscription only once at the first payment.

How do you learn when the state changes later?

If you check subscription state only when the user opens the app, you have a problem: subscriptions renew, cancel, or refund even while the user is away.

For this, Google offers Real-Time Developer Notifications (RTDN) — Google tells your server whenever a purchase or subscription state changes. The notifications are delivered through Cloud Pub/Sub, a service that carries the message from Google to your server.

Subscription state changes

Google Play

RTDN

Cloud Pub/Sub

Your server

When your server receives a notification, it re-checks the current state with the Google Play API and updates the user's entitlement.

App Store (iOS)

Apple works on the same principle: don't trust the payment result the app sends — verify the transaction info Apple provides, on the server. The methods and terms are just a little different from Google.

StoreKit 2 and JWS

To build in-app purchases on iOS, you can use StoreKit 2 — Apple's purchase framework for iOS apps. The transaction info you get from StoreKit 2 can include Apple's digital signature.

A term that shows up here is JWS (JSON Web Signature) — purchase information that Apple has digitally signed. Because you can check the signature, you can tell whether it is real, and not fake data the app made up.

On the server, you can verify in two ways:

  1. Verify the signed transaction info from Apple directly, or
  2. Use the App Store Server API to ask Apple about the purchase or subscription state.

Calling the App Store Server API

For example, you can look up a specific transaction:

GET https://api.storekit.itunes.apple.com/inApps/v1/transactions/{transactionId}
Authorization: Bearer <JWT>

Here a JWT (JSON Web Token) appears — a token that shows Apple your server is allowed to use the API. You create the JWT on your server using an API key issued in App Store Connect, and send it with the request. Apple signs these JWTs with a method called ES256 — you can just treat it as "the signing method for Apple API tokens." You do not need to know the cryptography behind ES256 to ship in-app purchases.

How do you check the JWS Apple sends back?

The transaction info from the App Store Server API is also signed by Apple. When you read inside the JWS, you can see things like:

  • which product was bought,
  • when it was bought,
  • when the subscription expires,
  • whether it was canceled or refunded.

And you must confirm the signature was really created by Apple. This is where a certificate chain comes in — it is used to check that the JWS signature genuinely comes from Apple. You can write the verification yourself, or use Apple's App Store Server Library, which handles both the JWS check and reading the contents.

What about the old verifyReceipt?

If you look at older Apple examples, you'll see an API called verifyReceipt. In the past, the server would send the receipt data from the app to Apple and get back a verification result. That approach separates two environments:

  • Production: the real payment environment
  • Sandbox: the test payment environment

The old pattern was: call Production first, and if it returns 21007, retry against Sandbox.

For a new implementation, though, don't start from this. Apple has marked verifyReceipt as deprecated and recommends the App Store Server API and Apple-signed transaction info instead. It's worth knowing when you maintain an older system, but if you are implementing IAP for the first time, start from the modern approach.

The App Store also sends state-change notifications

Subscriptions keep changing over time on iOS too — renewing, canceling, refunding, or failing to charge, even while the user isn't in the app. To handle this, Apple provides App Store Server Notifications V2 — Apple tells your server when a subscription or payment state changes. The purpose is the same as Google's RTDN.

This pattern — an outside service automatically telling your server when something happens — is commonly called a webhook.

Google Play                    App Store
   ↓                              ↓
RTDN / Cloud Pub/Sub           App Store Server Notifications V2
   ↓                              ↓
Your server                    Your server

The implementations differ, but the goal is the same: your server should know the latest payment state even when the user never opens the app.

Pitfalls that bite people

1. Confusing Sandbox and Production. Test payments and real payments live in different environments. If you treat test data as a real purchase — or look for a real purchase in the test environment — real users can end up unable to use your service. Keep the two clearly separate.

2. Processing the same purchase twice. The same purchase info can reach your server more than once — for example, the app may resend it after a network error. If you run coins += 100 every time a purchase is verified, one payment could hand out the item many times. So always check whether you have already processed a purchase. Store the purchase identifier — purchaseToken on Android, transactionId on iOS — to block duplicates. On Android, avoid using orderId alone as the dedup key: not every purchase has an orderId.

3. Forgetting to acknowledge on Android. Passing verification is not the end. After you give the user the feature, you must also tell Google the purchase is handled — the acknowledge step. Miss it, and a valid purchase can be refunded later.

4. Assuming a subscription stays active forever. A subscription is not a one-time event; its state keeps changing. Active today does not mean active next month. You must keep reflecting renewals, expirations, refunds, cancellations, and failed payments — or you'll keep serving users who stopped paying, and cut off users who are still paying.

5. Trusting the device clock. Don't decide whether a subscription has expired using the time on the user's phone — a user can change it. Base the decision on the subscription state and expiry time you confirmed from Google or Apple.

What the server actually does, in the end

At first it looks simple:

Purchase successful

Unlock the feature

But in a real service it looks more like this:

Payment in the app

Purchase info

Your server

Confirm with Google / Apple that it's a real purchase

Check you haven't already processed it

Grant the entitlement

Purchase handled

And for a subscription, it does not end there:

Google / Apple

Renew · Cancel · Refund · Failed payment

Server notification

Update the subscription state on your server

Once you understand this shape, the different API names on Google and Apple stop being scary — the overall flow is the same.

This is a lot to build — and to keep running

Google Play and the App Store use different APIs, different ways to authenticate your server, and different ways to notify you when a payment state changes. On top of that you need to separate test and real environments, prevent double-processing of the same purchase, manage subscription state, handle cancellations and refunds, and manage your auth keys.

It is not something you cannot build. But it is also not code you write once and forget — as Google's and Apple's policies and APIs change, it stays a backend you have to maintain.

That is exactly the part Unveily's IAP API takes off your plate: instead of building a separate payment backend for each store, you connect purchase verification and payment-state handling through one API. When you would rather focus on your app's features than build and maintain two store integrations, that is what it is for.