Contact

Type to explore Drawk studio productions

Discover cross-platform games, mobile apps, architecture articles, and interactive GLSL labs.

← Back to Posts

Cross-Platform In-App Purchases, Store Progression & Automation: Apple StoreKit 2, Google Play Billing, Steam & Epic

TL;DR: Cross-platform monetization and player progression collapse when game engines treat stores as generic payment switches. Apple StoreKit 2 (signed JWS tokens), Google Play Billing (BillingClient 7+ with mandatory 3-day server acknowledgment), Steamworks Microtransactions (ISteamMicroTxn), and Epic Online Services (EOS) enforce fundamentally incompatible transaction lifecycles, refund hooks, and progression rules. Building a unified studio facade (IStoreBilling, IStoreProgression) backed by an automated catalog reconciler eliminates transaction loss, handles pending states gracefully, and turns cold store checkouts into playful, celebratory player moments.

Cross-Platform Store Billing Matrix

Platform Core SDK & API Product Archetypes Canonical SKU Format Server Verification Protocol Sandbox & Test Mechanism Revocation & Refund Mechanism
Apple App Store StoreKit 2 / App Store Server API v2 Consumables, Non-Consumables, Auto-Renewable Subscriptions, Non-Renewing Subscriptions Reverse DNS: com.drawk.game.sku Signed JWS decoded on your backend; verified via Apple Root CA App Store Connect Sandbox Testers & StoreKit Configuration (.storekit) files in Xcode App Store Server Notifications v2 (REVOKE, REFUND, DID_CHANGE_RENEWAL_STATUS)
Google Play Google Play Billing Library 7+ / Android Publisher API v3 In-App Products (Consumable / Non-Consumable), Subscriptions (Base Plans + Offers) Lowercase alphanumeric: sku_id or com.drawk.game.sku Real-time Developer Notifications (RTDN via Google Cloud Pub/Sub) + purchases.products.get License Testing Accounts & Internal App Sharing Google Cloud Pub/Sub RTDN webhooks (ONE_TIME_PRODUCT_CANCELED, SUBSCRIPTION_REVOKED)
Steam (PC / Mac) Steamworks SDK / Steamworks Web API Microtransactions, DLC packages, Steam Inventory Service items Steam App ID + Definition Index / ItemDefID ISteamMicroTxn/InitTxn and ISteamMicroTxn/FinalizeTxn server callbacks Steam Microtransaction Sandbox (sandbox.steamcommunity.com) & test user accounts Steamworks Web API refund and chargeback status callbacks
Epic Games Store Epic Online Services (EOS) SDK Consumables, Durables (Entitlements), Digital Subscriptions Lowercase catalog item ID / Offer ID EOS Ecom Interface + Backend Auth Token verification Epic Developer Portal Sandbox deployment & test groups Epic Online Services Ecom Ownership Token invalidation

Visual Architecture: The Cross-Platform Purchase & Entitlement Loop

A real cross-platform store architecture must never trust client-side claims. The payment flow moves through five discrete phases: catalog discovery, interactive payment sheet, server-to-server validation, entitlement granting, and transaction closure.

flowchart TD subgraph CLIENT["Client Game Runtime (Unity / Godot / Custom Engine)"] UI["Player Taps Purchase (Storefront UI)"] --> INIT["Request Store Order (IStoreBilling Facade)"] INIT --> SHEET["Native Store Sheet Presented (StoreKit / Google Play / Steam)"] SHEET --> WAIT["Receive Signed Receipt / JWS Token"] WAIT --> POST_SRV["Dispatch Receipt Payload to Backend Server"] GRANT_LOCAL["Apply Local Player State (Playful Celebration FX)"] --> FINISH["Acknowledge & Finish Transaction (StoreKit finish / Play acknowledge)"] end subgraph SERVER["Game Cloud Backend (ASP.NET Core / Azure Microservice)"] POST_SRV --> VAL_AUTH{"Verify Transaction Authenticity"} VAL_AUTH -->|StoreKit 2| JWS["Verify Apple Root CA & JWS Signature"] VAL_AUTH -->|Google Play| RTDN["Call Android Publisher API (purchases.products.get)"] VAL_AUTH -->|Steam| STEAM["Call ISteamMicroTxn/FinalizeTxn"] JWS --> IDEM{"Idempotency & Replay Check"} RTDN --> IDEM STEAM --> IDEM IDEM -->|Valid & Unique| GRANT_DB["Persist Entitlement in Player DB"] GRANT_DB --> ACK_CLIENT["Return Verified Order Status (200 OK)"] end ACK_CLIENT --> GRANT_LOCAL
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│                           CROSS-PLATFORM COMMERCE LIFECYCLE FLOW                                │
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                  │
│   [ Client Engine ]           [ Platform Store ]                  [ Studio Game Backend ]        │
│          │                           │                                       │                   │
│          │─── Initiate Purchase ────►│                                       │                   │
│          │    (Unified SKU)          │                                       │                   │
│          │                           │── Authenticate User                   │                   │
│          │                           │   & Authorize Payment                 │                   │
│          │◄── Signed Payload ────────│                                       │                   │
│          │    (JWS / PurchaseToken)  │                                       │                   │
│          │                           │                                       │                   │
│          │───────────────────────────── Dispatch Payload for Validation ────►│                   │
│          │                           │                                       │── Cryptographic   │
│          │                           │◄──── Server Verification Call ────────│   Verification    │
│          │                           │      (Apple Server API / PubSub)      │── Idempotency Check│
│          │                           │───── Verified Token Response ────────►│── Save Entitlement│
│          │                           │                                       │                   │
│          │◄──────────────────────────── 200 OK: Entitlement Confirmed ───────│                   │
│          │                                                                                       │
│          │── Trigger Celebratory Unlocking FX (Audio Chime + Particles)                          │
│          │── Acknowledge / Finish Transaction ──► [ Store Queue Cleared ]                         │
│                                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

1. The Core Commerce Anatomy: Consumables, Non-Consumables, and Subscriptions

Every store models in-app products into three fundamental archetypes. Confusing their contracts guarantees store rejection or customer support headaches.

Consumables (Gold, Gems, Tickets, Energy Refills)

  • Contract: Can be bought repeatedly. The player spends or burns the balance inside the game economy.
  • Platform Obligation: The store tracks only the financial transaction, not the player's balance. Once purchased, the game runtime must consume or acknowledge the purchase so the player can buy it again.
  • Restore Purchases Rule: Consumables are never restored through platform store buttons. If a player deletes and reinstalls the app on a new device, consumable balances must be restored from your own remote cloud account, not the Apple or Google store queue.

Non-Consumables / Durables (Remove Ads, Full Game Unlock, Character Packs)

  • Contract: Bought once per user account, granting permanent entitlement.
  • Platform Obligation: Apple and Google remember ownership forever across all devices tied to that user's Apple Account or Google Account.
  • The Mandatory "Restore Purchases" Gate: Apple strictly enforces App Store Review Guideline 3.1.1: your app must provide a visible "Restore Purchases" button in settings or the store view. If a user swaps to a new iPhone or reinstalls your game, tapping Restore must query AppStore.sync() or store.RestoreTransactions() and unlock the content without prompting for payment. Google Play automatically restores active non-consumable purchases during queryPurchasesAsync, but having a unified Restore button keeps the experience identical across mobile platforms.

Auto-Renewable Subscriptions (Season Passes, VIP Clubs)

  • Contract: Recurring billing cycles (weekly, monthly, annual) managed by the platform.
  • Platform Obligation: Stores manage trial periods, grace periods, account holds (credit card expiration), and price increase consents.
  • Cross-Store Difference: On Apple, subscriptions belong to a Subscription Group where tiers and upgrade/downgrade rules are defined. On Google Play, a Subscription entity contains multiple Base Plans (e.g., auto-renewing vs prepaid) and Offers (introductory pricing, free trials).

2. Server-Side Validation: StoreKit 2 vs. Google Play vs. Steam

Local on-device receipt validation is obsolete. Client-side cryptographic checks (such as legacy OpenSSL PKCS#7 parsing or obfuscated byte arrays) are routinely bypassed on rooted Android devices or jailbroken iOS hardware with runtime injection hooks like Frida.

Apple StoreKit 2: Signed JWS Representations

StoreKit 2 delivers transactions wrapped in RFC 7515 JSON Web Signatures (JWS). The receipt string is a compact three-part token (header.payload.signature):

  1. Header: Contains the signing algorithm (ES256) and the x.509 certificate chain (x5c).
  2. Payload: Contains verified purchase fields: transactionId, originalTransactionId, productId, purchaseDate, revocationDate, and appAccountToken.
  3. Backend Verification: Your server validates that the certificate chain roots to the official Apple Root CA, checks that bundleId matches your application, and verifies that the transaction has not expired or been revoked.
// Example backend validation flow in C# (ASP.NET Core)
public async Task<bool> ValidateAppleTransactionAsync(string jwsPayload)
{
    // Decode and verify the JWS signature against Apple Public Keys
    var handler = new JsonWebTokenHandler();
    var validationParameters = new TokenValidationParameters
    {
        ValidIssuer = "https://appleid.apple.com",
        ValidAudience = "com.drawk.retro.dudedrop",
        IssuerSigningKeyResolver = (token, securityToken, kid, parameters) => 
            AppleKeyProvider.GetApplePublicKeys(kid)
    };

    var result = await handler.ValidateTokenAsync(jwsPayload, validationParameters);
    if (!result.IsValid) return false;

    // Verify transactionId has not been recorded previously (idempotency)
    var transactionId = result.ClaimsIdentity.FindFirst("transactionId")?.Value;
    return await _entitlementStore.TryRecordTransactionAsync(transactionId);
}

Google Play Billing: The Mandatory 3-Day Acknowledgment Trap

The most dangerous trap in mobile monetization lives inside Google Play Billing:

The 3-Day Rule: When a player purchases a product on Google Play, your game runtime or backend must explicitly acknowledge the purchase via BillingClient.acknowledgePurchase() or AndroidPublisher.Purchases.Products.Acknowledge. If you fail to acknowledge the purchase within 3 days, Google automatically refunds the player and revokes the transaction.

// Handling Google Play purchase acknowledgment after backend verification
public async Task ProcessGooglePurchaseAsync(Purchase purchase)
{
    if (purchase.PurchaseState == PurchaseState.Purchased)
    {
        // 1. Send purchase.PurchaseToken and purchase.PackageName to game backend
        var verified = await GameBackend.VerifyGooglePurchaseAsync(
            purchase.PackageName, 
            purchase.Products.First(), 
            purchase.PurchaseToken
        );

        if (verified)
        {
            // 2. Acknowledge immediately so Google does not auto-refund
            var acknowledgeParams = AcknowledgePurchaseParams.NewBuilder()
                .SetPurchaseToken(purchase.PurchaseToken)
                .Build();

            await _billingClient.AcknowledgePurchaseAsync(acknowledgeParams);
            GrantPlayerContent(purchase.Products.First());
        }
    }
}

Google Real-time Developer Notifications (RTDN)

Rather than polling Google Play servers, production architectures subscribe to Google Cloud Pub/Sub. When a subscription renews, a grace period starts, or a customer requests a refund through Google Play customer support, Google pushes a notification directly to your server:

  • SUBSCRIPTION_PURCHASED / SUBSCRIPTION_RENEWED: Grant or extend subscription benefits.
  • SUBSCRIPTION_IN_GRACE_PERIOD: Keep content active while Google retries payment (usually 3 to 16 days).
  • SUBSCRIPTION_ON_HOLD: Suspend content access until payment is updated.
  • SUBSCRIPTION_REVOKED / ONE_TIME_PRODUCT_CANCELED: Immediately remove entitlement from the player profile to prevent refund fraud.

Steamworks Microtransactions (ISteamMicroTxn)

On PC via Steam, in-game purchasing operates via a two-phase server handshake:

  1. InitTxn: Your server makes an authenticated HTTPS POST to https://partner.steam-api.com/ISteamMicroTxn/InitTxn/v3/ passing the Steam ID, order ID, product description, and currency amount.
  2. Player Authorization: Steam presents its native checkout overlay inside the client.
  3. FinalizeTxn: When the player authorizes payment, Steam sends your backend a callback or webhook. Your backend posts to ISteamMicroTxn/FinalizeTxn/v2/ to lock the transaction and credit the player's account.

3. Store Progression Systems: Achievements, Stats, and Leaderboards

Monetization and player progression share the same architectural spine: both connect in-game milestones to store platform identity.

flowchart TD subgraph ENGINE["Game Engine Logic"] EVT["Player Achieves Milestone (Score, Boss Defeated, Level 10)"] --> ADAPT["Unified Progression Facade (IStoreProgression)"] end subgraph ADAPTERS["Platform Progression Adapters"] ADAPT -->|iOS / macOS| GC["Apple Game Center Adapter"] ADAPT -->|Android| GP["Google Play Games Services Adapter"] ADAPT -->|PC / Mac| ST["Steamworks Stats Adapter"] ADAPT -->|Cross-Store| EOS["Epic Online Services Adapter"] end subgraph PLATFORMS["Platform Services & APIs"] GC --> GC_API["GameKit SDK (GKAchievement / GKLeaderboard)"] GP --> GP_API["Play Games SDK (AchievementsClient / LeaderboardsClient)"] ST --> ST_API["Steamworks API (SetAchievement / StoreStats)"] EOS --> EOS_API["EOS Connect & Progression Interface"] end
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│                             CROSS-PLATFORM PROGRESSION ADAPTER                                   │
├──────────────────────────────────────────────────────────────────────────────────────────────────┤
│                                                                                                  │
│   Unified Call:  progression.UnlockAchievement("defeat_dusk_titan")                              │
│                                                                                                  │
│   ├── Apple Game Center  ──► GKAchievement("com.drawk.titan_slayer", percentComplete: 100.0)    │
│   ├── Google Play Games  ──► PlayGames.Achievements.Unlock("CgkI-8fZ1eMMEAIQAQ")                │
│   ├── Steamworks         ──► SteamUserStats.SetAchievement("ACH_DEFEAT_DUSK_TITAN") + StoreStats()│
│   └── Epic Online Serv.  ──► EOS_Achievements_UnlockAchievements(...)                            │
│                                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────────────────────┘

Store Progression & Identity Comparison

Platform Identity & Authentication Achievement Rules & Caps Leaderboard Capabilities Cloud Save & State Sync Icon Automation via REST API
Apple Game Center Local player authentication via GameKit (authenticateHandler); scoped team player IDs Max 1,000 points per game; max 100 points per achievement. Supports incremental percentages (0.0 to 100.0). Classic, recurring (daily, weekly), and localized score sets. Ranked high-to-low or low-to-high. iCloud Key-Value Storage & CloudKit private database records Supported (gameCenterAchievementImages / gameCenterLeaderboardImages in ASC API)
Google Play Games Automatic silent sign-in via Play Games Services v2 (GamesSignInClient) Max 100,000 XP per game (500 to 20,000 XP per achievement). Incremental achievements require explicit integer step counts. Public and Friends-only leaderboards. Daily, weekly, and all-time time spans. High-to-low or low-to-high. Play Games Saved Games API (SnapshotsClient) with binary payload conflict resolution Permanently Console Only (512x512 PNG icons cannot be uploaded via REST)
Steamworks 64-bit Steam ID (CSteamID); verified via Steam session tickets Up to 128 achievements by default (expandable on request). Binary unlocks and integer/floating stats. Global leaderboards with download filters (Global, Around User, Friends). Supports attached ghost replay data. Steam Cloud (ISteamRemoteStorage) with byte quota allocation per application Supported via Steamworks Web API (ISteamLeaderboards / Partner Schema Ingest)
Epic Online Services EOS Connect interface; maps platform accounts (Steam, Apple, Google, Console) to EOS Product User IDs Cross-platform achievement definitions synced across console, PC, and mobile clients. Global leaderboards with temporal resets and aggregated player rank queries. EOS Player Data Storage (encrypted cloud key-value and title storage) Supported via Epic Developer Portal REST Management APIs

4. The Store Automation Boundary: REST APIs vs. Web Console Residue

When building continuous delivery pipelines for games, engineering teams often attempt to automate every single store configuration task. Understanding the boundary between what store REST APIs allow and what consoles permanently block is essential for stable tooling.

flowchart TD subgraph REST_API["AUTOMATABLE VIA REST APIS (ASC API / Publisher API / Steamworks)"] R1["IAP Product SKU Registration"] R2["Localized Product Titles & Descriptions"] R3["Price Tier Assignments & Base Currency Rules"] R4["Game Center Achievements & Leaderboard Definitions"] R5["Game Center 512x512 Icon Asset Uploads"] R6["Steamworks Inventory Schema & Stats JSON Ingest"] end subgraph PORTAL_GATE["MANDATORY WEB CONSOLE RESIDUE (Human Setup Required)"] P1["Tax ID, Banking Forms & Paid App Agreements"] P2["EU Digital Services Act (DSA) Trader Status Declaration"] P3["Initial Apple IAP Review Screenshot (Mandatory 1024x768 / Screen capture)"] P4["Google Play Subscription Base Plans & Offer Phase Setup"] P5["Google Play Games 512x512 Achievement Icons (Permanently Console Only)"] P6["Reviewer Demo Credentials & Store Sandbox Configuration"] end
┌──────────────────────────────────────────────────────────────────────────────────────────────────┐
│                         STORE COMMERCE & PROGRESSION AUTOMATION DIVIDE                           │
├─────────────────────────────────────────────────┬────────────────────────────────────────────────┤
│   AUTOMATED VIA REST APIS                       │   MANDATORY WEB CONSOLE RESIDUE                │
│   (App Store Connect & Android Publisher)      │   (Human Verification & Security Gates)        │
├─────────────────────────────────────────────────┼────────────────────────────────────────────────┤
│ • In-App Purchase SKU creation & pricing        │ • Paid Applications Developer Agreement        │
│ • Localized product titles & descriptions       │ • Banking, wire routing, and W-8BEN/W-9 tax    │
│ • Subscription group hierarchies                │ • EU DSA Trader status identification          │
│ • Apple Game Center achievement & leaderboard   │ • Apple first-submission IAP review screen     │
│   definitions AND icon images                   │ • Google Play subscription base plans & offers │
│ • Google Play Games achievement metadata        │ • Google Play Games 512×512 achievement icons │
│ • Steamworks stats and microtransaction schema  │ • App Reviewer demo account credentials        │
└─────────────────────────────────────────────────┴────────────────────────────────────────────────┤

Lessons from Studio Tooling: Adopt-Before-Create

In our cross-platform reconcile engine, automation scripts that blindly attempt to create store entities fail with 409 Conflict whenever a product has already been touched in a web console.

  1. Query Remote State First: Before issuing a POST /v1/inAppPurchasesV2, the reconciler queries existing products by canonical SKU.
  2. Adopt Remote Identifiers: If the product exists remotely, the local manifest adopts the store's generated internal ID and syncs only modified fields (such as updated localized strings or price points).
  3. Structured Residue Reporting: Instead of failing a release build when an un-automatable task is encountered, our tooling outputs actionable residue:
    • "Play Games achievement icons need manual upload (24 items)"
    • "Apple IAP review screenshot missing for com.drawk.remove_ads"
    • "Google Play Paid Apps agreement pending review in Payments Profile"

5. Interlinking Your Store Architecture

Monetization and progression do not exist in isolation. They connect directly to your studio's existing store, legal, and build practices:


6. Bringing Playfulness and Tactile Joy to Store UX

A game purchase should never feel like paying a utility bill. When a player chooses to support your game, the transaction should feel celebratory, crisp, and deeply rewarding.

       ★  ★  ★  LEVEL COMPLETED!  ★  ★  ★
   ┌───────────────────────────────────────────┐
   │                                           │
   │      [!] RETRO PACK UNLOCKED! [!]         │
   │                                           │
   │      * 12 New Synthwave Tracks            │
   │      * 4 Custom Neon Avatars              │
   │      * Zero Ad Interruptions              │
   │                                           │
   │   ┌───────────────────────────────────┐   │
   │   │     CLAIM NOW  -  $2.99 USD       │   │
   │   └───────────────────────────────────┘   │
   │          (Restore Purchases)              │
   └───────────────────────────────────────────┘

The Anatomy of Juicy Store Feedback

  1. Harmonic Audio Cues: Replace generic mechanical clicks with satisfying musical chimes. A purchase confirmation should trigger a crisp, synthesized major chord or sparkling chime that resolves nicely with your background score.
  2. Particle Celebration Bursts: When a transaction confirms, reward the screen with celebratory visual sparks, particle confetti, or expanding neon rings. Give the player an immediate visual acknowledgment that something special was unlocked.
  3. Card Flip Unlocking Sequences: For character or item unlocks, avoid popping raw numbers into a label. Animate the item card flipping over, highlighting its rarity badge, and sliding cleanly into the player's inventory tray.
  4. Haptic Snaps: On mobile devices, use subtle tactile feedback (HapticFeedbackType.Success) right as the store confirmation returns, pairing the sound and visual burst with physical feedback in the player's hand.

Player-First UX Principles

  • Respectful Cancel Paths: If a player taps the close button on an offer sheet, close it instantly. Never punish the player with nagging confirm-dialogs ("Are you SURE you want to miss out?").
  • Transparent Pending States: When Ask-to-Buy or deferred payments trigger, display a clear, reassuring status: "Purchase sent for parent approval. Your items will unlock automatically once approved!" Never freeze the game or leave the player wondering if their payment failed.
  • Instant Offline Entitlement with Graceful Sync: If a player buys an entitlement or unlocks an achievement while network connectivity drops, record the local entitlement immediately and queue the server verification for the next network reconnect.

References, Standards & Official Documentation

← Back to All Posts