Implementing In-App Purchases for Mobile Games
IAP in mobile games isn't just "add a buy button." Apple and Google require receipt verification, server architecture, and restore handling. Skip any requirement and you either lose money or get rejected.
StoreKit 2 vs StoreKit 1
StoreKit 1 (pre-iOS 15) has SKPaymentQueue, SKProduct, SKPaymentTransactionObserver. Legacy: callbacks via delegate, manual transaction management, verbose.
StoreKit 2 (iOS 15+) is different: async/await, Product.products(for:), product.purchase(), Transaction.updates AsyncSequence. Cleaner code.
If targeting iOS 14 and below, use StoreKit 1 or cross-platform abstraction. iOS 15+ — use StoreKit 2.
Android: BillingClient (Play Billing 6+). Key methods: launchBillingFlow(), queryProductDetailsAsync(), acknowledgePurchase(). Google requires acknowledging each purchase within 3 days, otherwise auto-refunds. This is policy, not a bug. Call consumeAsync() for consumables (coins, lives) and acknowledgePurchase() for non-consumables (remove ads, premium).
Server-Side Receipt Verification — Mandatory
Client verification is insecure. Jailbroken devices + iap-receipt-generator = fake receipts. Correct scheme:
- App gets receipt/token from StoreKit/BillingClient
- Sends to your server:
POST /api/purchases/verify - Server verifies via Apple
/verifyReceiptor Google Play Developer API - Server grants the item to the user
- Returns result to client
Apple deprecates /verifyReceipt in favor of JWS transactions — on server, decode signedTransactionInfo (JWT), verify Apple Root CA signature. Libraries: appstore-connect-sdk (Node.js), apple-receipt-verifier (Python/Go).
Purchase Types in Games
| Type | Example | Consumable | Logic |
|---|---|---|---|
| Game Currency | 1000 coins | Yes | consume after crediting |
| Lives/Energy | +5 lives | Yes | consume, no duplicates |
| Remove Ads | Ad-free | No | acknowledge, restore |
| Season Pass | Battle Pass | Subscription | check expiry |
| One-time Content | Character Skin | No | acknowledge, restore |
For subscriptions (autoRenewableSubscription) — separate logic: renewalInfo, grace period, billing retry state. If expired but in grace period — don't revoke access immediately.
Restore Purchases
StoreKit 2: for await transaction in Transaction.currentEntitlements — all active non-consumables and subscriptions. "Restore Purchases" button is mandatory per App Store Review Guideline 3.1.1. Without it — rejected.
Android: queryPurchasesAsync(QueryPurchasesParams) for INAPP and SUBS — all active purchases offline.
Offline and Edge Cases
User bought, server unavailable for verification. Correct scheme: save pending transaction locally (Core Data / Room), retry verification on next launch with exponential backoff. Don't finish transaction until server verifies.
Process
Configure products in App Store Connect and Google Play Console → server verification → client IAP module → Sandbox/Test testing → restore testing → QA edge cases (payment interruption, duplicate purchase) → submit.
Timeline
Basic integration (consumables + non-consumables, server verification): 3–4 days. Subscriptions with grace period, billing retry, receipt migration: +2 days. Cross-platform Flutter/React Native with StoreKit and BillingClient abstraction: +1–2 days.







