Server-Side Receipt Validation for App Subscriptions: Apple and Google, Demystified

Here's the mistake we see in almost every subscription app codebase we're asked to review: the client asks the app store "is this user subscribed?", the store SDK says yes, and the app unlocks premium. It works in every demo. It ships. And then reality arrives — a renewal fails and the app doesn't notice for days, a refund goes through and the user keeps premium forever, a user reinstalls on a new phone and loses access they paid for, or someone with a patched client simply tells your app they're subscribed and your app believes them. Meanwhile your own database has no idea who's paying you, so your revenue dashboard is whatever App Store Connect says it is, reconciled never.

The fix is a single architectural decision: the client never decides entitlement — your server does. Purchases happen on the device because the stores require it, but a purchase only becomes an entitlement after your backend has verified it with Apple or Google directly and recorded it. We run a production app in both stores — shipped from one Capacitor codebase, which is its own story — with exactly this architecture: Apple JWS verification on one side, a Google service account on the other. This article is the map we wish we'd had, in plain language first and mechanics second.

The mental model: receipts are claims, your server issues the verdict

Whatever the platform, the flow is the same shape. The app performs the purchase through the store's native billing UI and receives a proof — a signed transaction from Apple, a purchase token from Google. The app sends that proof to your backend. Your backend verifies it with the platform, not with the client: checking the cryptographic signature or calling the platform's API, confirming the product ID, the expiry date, and that this purchase belongs to this account. Only then does your backend write a row in its own subscriptions table and grant access. From that moment on, the store is an event source and a billing processor; your database is the source of truth for who gets what.

This buys you four things that client-side checking structurally cannot: security (a patched client can lie to itself, but it can't forge Apple's signature or Google's API response), cross-device consistency (entitlement follows the user's account, not the phone), lifecycle awareness (renewals, cancellations and refunds reach your server even when the app is never opened again), and a subscriptions table you can actually query for revenue, churn and support.

The Apple side: JWS, verified on your server

Modern Apple subscriptions (StoreKit 2 and the App Store Server API) speak JWS — JSON Web Signature. Every transaction Apple gives you is a signed token: payload plus an x5c certificate chain rooted in Apple's own root certificates. Verification on your server means walking that chain to Apple's root, checking the signature, and only then trusting the payload — which contains everything you need: product ID, original transaction ID, purchase and expiry dates, and the environment flag telling you whether this is a sandbox or production transaction (mixing those up is a classic launch-week bug; sandbox receipts hitting the production endpoint confuse everyone).

The second half — the one people skip — is App Store Server Notifications V2: a webhook endpoint you register, to which Apple pushes signed JWS payloads for every lifecycle event. DID_RENEW when a renewal succeeds. EXPIRED when one lapses. DID_FAIL_TO_RENEW with billing-retry and grace-period signals. REFUND when Apple refunds a user — and note that Apple grants refunds directly, without asking you, so if you don't process this event, refunded users keep premium forever at your expense. Each notification is verified exactly like a transaction and then applied to your subscriptions table. The old advice you'll still find everywhere — POST the receipt blob to verifyReceipt — refers to the deprecated legacy endpoint; new builds should be on the JWS/V2 stack.

The Google side: a service account and a mandatory acknowledgment

Google's model is API-first. The app's purchase yields a purchase token; your server authenticates to the Google Play Developer API as a service account — a machine identity you create in Google Cloud, grant limited Play Console permissions, and whose JSON key becomes a backend secret with all the rotation-and-storage hygiene that implies — and calls the subscriptions endpoint to ask what that token currently represents. The response gives you state (active, canceled, in grace period, on hold, paused — Android has more states than iOS, and your entitlement logic should handle them explicitly), expiry time and renewal intent.

Two Google-specific traps that bite real apps. First, acknowledgment: every purchase must be acknowledged within three days, or Google automatically refunds it. Acknowledge server-side, after your verification succeeds and your database row is written — that ordering makes the whole flow transactional. Teams that miss this discover it as a mysterious wave of refunds in week one. Second, ongoing events arrive via Real-Time Developer Notifications, delivered through a Cloud Pub/Sub topic rather than a plain webhook — slightly more setup, same principle. Golden rule on both platforms: a notification is a doorbell, not a verdict. When an event arrives, re-query or re-verify the platform for current state, then update your database from that.

The part that's actually hard: your own entitlement model

The platform mechanics are documented, if scattered. The engineering that determines whether your subscription system is pleasant or cursed is on your side of the line:

  • Bind purchases to accounts, not devices. Verification is the moment to link a store transaction to a user in your system — Apple's app account token and Google's obfuscated account ID exist for round-tripping your user ID through the purchase. Get this right and restore-purchases, reinstalls and cross-device access all fall out naturally.
  • Make every handler idempotent. Both platforms redeliver notifications, retries happen, and the app may re-submit the same proof. Processing the same event twice must produce the same single result. Key everything on the platform's transaction identifiers.
  • Model the in-between states. "Subscribed or not" is a lie. Real subscriptions are canceled-but-paid-until-March, in billing retry, in grace period, refunded mid-cycle. Decide deliberately what each state unlocks — being generous during grace periods, for instance, measurably saves subscriptions that a hard cutoff would kill.
  • Log every raw event before processing it. When a user writes in saying they paid and can't access — and they will — an append-only ledger of everything Apple and Google ever told you turns a support nightmare into a five-minute lookup.
A subscription isn't real when the app store says so. It's real when your server has verified it, recorded it, and can prove it — everything else is a UI animation.

An honest trade-off note: you can buy most of this. RevenueCat and similar services wrap both platforms behind one API and are a reasonable choice for a small team that wants to ship this week — we have no quarrel with that path. The costs are a percentage of revenue forever, a third party inside your most sensitive data flow, and the fact that you still need to understand the model above to use them correctly. We built ours directly because we run multiple products on shared infrastructure and wanted the ledger in our own schema; a solo developer's math can legitimately come out the other way.

The takeaway

Server-side validation isn't a security nicety you add at scale — it's the difference between operating a subscription business and merely rendering one. The client shows buttons; your server verifies Apple's JWS signatures, queries Google as a service account, listens to both platforms' lifecycle events, and maintains the one table that knows who is entitled to what and why. Build it before launch, because retrofitting it onto a live subscriber base means migrating entitlements you never recorded. If you're planning a subscription app — or living with one whose renewals and refunds are currently on the honor system — talk to us: this exact stack runs in production for our own app, and we've made the mistakes so you can skip them.

We solve these problems on our own products every day

Free 30-min discovery call · No hard sell · Reply within one business day

Start a project

← More from the blog