Implementing Server-Side Purchase Verification (Receipt Validation)
A client submitted a bug report: a user bought Premium, got a transaction token, then restored the app from a backup on another device — and Premium is active again without re-payment. Classic. The reason — validation only on the client: the app checks the local receipt or trust flag from StoreKit, without consulting the server.
Why client-side validation is not validation
On iOS StoreKit 2 returns a Transaction with Apple's signature. You can verify the signature locally through Transaction.verificationResult, but this doesn't protect against replay attacks: an attacker intercepts a valid receipt from one user and substitutes it in another account. On Android the situation is analogous — BillingClient.queryPurchasesAsync() returns Purchase objects that the client should not treat as confirmation without server verification of purchaseToken.
The most common fraud scheme is "receipt sharing": one receipt is distributed between users via forums. Without a server database that records which originalTransactionId (iOS) or orderId (Android) has already been used, this can't be detected.
How proper server verification is structured
iOS side (App Store Server API). The old approach — POST to https://buy.itunes.apple.com/verifyReceipt with base64-encoded receipt-data — is outdated. Apple promotes App Store Server API v1: client sends server the transactionId from Transaction.id (StoreKit 2), server makes GET /inApps/v1/history/{transactionId} with a JWT token (signed with ES256 key from App Store Connect). Response — JWSTransaction, which needs to be decoded and signature verified through Apple Root CA.
In parallel, subscribe to App Store Server Notifications V2: Apple pushes events (DID_RENEW, EXPIRED, REFUND, GRACE_PERIOD_EXPIRED) to your endpoint. Without this, subscription status on the server becomes stale — user canceled the subscription, but you still have them as Premium.
Android side (Google Play Developer API). For one-time purchases — purchases.products.get with packageName, productId, purchaseToken. For subscriptions — purchases.subscriptions.v2.get. Authorization through Service Account with Financial data viewer role — this is the minimum necessary permissions, don't give Editor access to the entire project. Response contains purchaseState (0 = Purchased, 1 = Canceled, 2 = Pending) and acknowledgementState — if 0, need to call purchases.products.acknowledge, otherwise Google returns money automatically after 3 days.
Idempotency and replay protection. Store in a table purchase_receipts with a unique index on original_transaction_id + product_id. On each verification request, first check if the record exists — if we already verified with a different user_id, return an error. This is receipt sharing protection.
purchase_receipts
id uuid PK
user_id uuid FK
platform enum('ios','android')
original_transaction_id varchar UNIQUE (per product)
product_id varchar
purchase_state smallint
expires_at timestamptz -- for subscriptions
raw_payload jsonb -- original response from Apple/Google
verified_at timestamptz
Stack and integration
Server-side usually Node.js (library app-store-server-api) or Python (google-auth + googleapiclient). For Node, the node-apple-receipt-verify package is convenient for legacy endpoints, but better to use app-store-server-api from Apple directly — supports JWT authorization and JWS verification out of the box.
On the iOS client side — minimal code: get Transaction.id from Transaction.all or from the updates stream, send to backend. Don't pass the entire appStoreReceiptURL — this is legacy, and the file may be invalid on the simulator.
On Android client sends purchaseToken and productId from Purchase.purchaseToken. Important: the token can be the same for multiple productId on subscription upgrade — account for this in the logic.
Workflow
Start with an audit of the current validation scheme — where exactly the receipt is checked, whether there is a server database of purchases, whether Server Notifications are handled. Then design the database schema and API endpoints, implement verification for each platform, configure webhook handler for Server Notifications, cover with tests using mock responses from Apple/Google. Separate phase — load testing of the verification endpoint, because at peak launches (promotion, feature in top App Store) it gets everything at once.
Estimated time — 2 to 5 days, depends on the presence of server infrastructure and the number of types of purchases (one-time, subscriptions, consumable, non-consumable). If the server already exists and you only need to add verification — closer to 2 days. Full architecture from scratch plus migrating existing users — up to 5.







