Quick answer: Every major platform provides a sandbox or test mode for in-app purchases. Use iOS Sandbox Apple IDs, Google Play licensed tester accounts, and Steam’s test app configuration. Point your server-side receipt validation at each platform’s sandbox endpoint. Then systematically test edge cases: cancellation, network failure during fulfillment, refunds, and interrupted purchases.
In-app purchases are one of the most consequential features in a game to get wrong. A bug in your IAP flow can charge a player without delivering the item, deliver the item without processing payment, or create duplicate transactions that trigger chargebacks. Every one of these scenarios leads to refund requests, negative reviews, or platform policy violations that can get your game removed from the store. Yet many indie developers test IAP by going live with a real product listing and buying it themselves with their own credit card, which tells you almost nothing about how the flow behaves under real conditions. Each platform offers a proper test environment — use it.
iOS Sandbox Testing
Apple provides a sandbox environment that mirrors the production App Store purchase flow without processing real payments. To use it, create Sandbox Apple IDs in App Store Connect under Users and Access. These are separate from real Apple IDs and can only be used for testing. On your test device, sign out of the real App Store account and sign in with the sandbox account when prompted during a purchase. The purchase dialog will indicate that you are in the sandbox environment.
Sandbox transactions behave like real transactions in most respects. They return a receipt that you can validate against Apple’s sandbox verification endpoint. They trigger the same StoreKit delegate callbacks. Subscriptions renew on an accelerated schedule — a monthly subscription renews every five minutes in sandbox, which makes it practical to test renewal, expiration, and grace period behavior in a single testing session.
// Server-side receipt validation — use the sandbox URL during testing
const APPLE_VERIFY_URL = process.env.NODE_ENV === "production"
? "https://buy.itunes.apple.com/verifyReceipt"
: "https://sandbox.itunes.apple.com/verifyReceipt";
async function verifyAppleReceipt(receiptData) {
const response = await fetch(APPLE_VERIFY_URL, {
method: "POST",
body: JSON.stringify({
"receipt-data": receiptData,
"password": process.env.APP_SHARED_SECRET,
"exclude-old-transactions": true
})
});
const result = await response.json();
// status 0 = valid, status 21007 = sandbox receipt sent to production
return result;
}
A common pitfall: if your server sends a sandbox receipt to the production verification URL, Apple returns status code 21007. Your validation logic should detect this and automatically retry against the sandbox URL, because during app review, Apple’s reviewers use sandbox accounts. If your server only talks to the production endpoint, it will reject all purchases during review and your app will be rejected.
Google Play Test Tracks
Google Play offers three levels of test distribution: internal testing (up to 100 testers), closed testing (invited users), and open testing (anyone can join). For IAP testing, internal testing is usually sufficient. Upload your APK or AAB to the internal test track in the Google Play Console, add your testers’ Google accounts as licensed testers, and distribute the build via the internal test link.
Licensed testers can make purchases using test cards that appear in the payment sheet alongside real payment methods. Test card transactions return valid purchase tokens that can be verified against the Google Play Developer API, but no real charges are processed. The API responses include the same fields as production purchases — purchaseState, orderId, purchaseToken — so your server-side validation code works without modification.
Google also provides static response test SKUs for unit testing your purchase flow without uploading a build. The product ID android.test.purchased always succeeds, android.test.canceled simulates a cancellation, and android.test.item_unavailable simulates a product that cannot be purchased. These are useful for automated tests but do not exercise your server-side validation, so they complement rather than replace test track testing.
Steam Test Configuration
Steam handles microtransactions differently from mobile platforms. If your game uses Steam’s microtransaction API, you can test by enabling the Steamworks sandbox environment for your app. Set your app to use Steam’s test mode by calling ISteamMicroTxn methods against the sandbox endpoint rather than the production endpoint. Transactions in sandbox mode appear in the Steamworks partner dashboard under the test section and do not charge real money.
For games that use Steam’s DLC model instead of microtransactions, testing is simpler: add the DLC app IDs to your Steamworks configuration, grant them to your test accounts via dev comp packages, and verify that your game correctly detects ownership via BIsDlcInstalled. For testing the actual purchase flow, you can create a free-to-claim DLC that mimics the purchase path without monetary transactions.
Testing Edge Cases That Break in Production
The happy path — player taps buy, purchase succeeds, item is delivered — is the easiest flow to get right and the least important to test thoroughly. The cases that cause support tickets and chargebacks are the edge cases. Build a test checklist that covers each of these scenarios:
Cancellation mid-flow: the player opens the purchase dialog, then taps cancel or presses the back button. Your game should return to its previous state cleanly without granting the item or showing an error message. Some engines fire a purchase callback with a “canceled” status; others throw an exception. Know which behavior your engine uses and handle it explicitly.
Network failure after payment but before fulfillment: the platform charges the player, but your game loses connectivity before confirming the delivery. This is the most dangerous edge case. Your server must be able to reconcile: when the game reconnects, it should check for pending purchases and fulfill any that were paid but not delivered. Both Apple and Google provide APIs to query unfulfilled transactions for exactly this purpose.
// Reconciliation on app launch — check for unfinished transactions
func reconcilePendingPurchases() {
let pending = getPendingTransactions() // platform-specific API call
for tx in pending {
let verified = verifyReceiptOnServer(tx.receipt)
if verified.isValid && !itemAlreadyGranted(tx.productId, tx.transactionId) {
grantItem(tx.productId)
markTransactionComplete(tx) // tells the platform to stop re-delivering
}
}
}
Refund processing: when a player requests a refund through the platform, your game should revoke the purchased item. Apple sends server-to-server notifications for refunds if you configure your App Store Server Notifications endpoint. Google sends real-time developer notifications via Cloud Pub/Sub. If you do not handle these, players can buy an item, use it, refund it, and keep it — a pattern that will be exploited within hours of discovery.
Duplicate purchase prevention: for non-consumable items (such as “remove ads” or a cosmetic unlock), your game should check whether the player already owns the item before initiating a purchase. Both platforms maintain ownership records that you can query, but the check must also exist on your server to prevent race conditions where two devices attempt the same purchase simultaneously.
Automating IAP Tests in CI
Manual testing covers the interactive flows, but your server-side validation logic should be tested automatically. Create a test suite that sends sample receipts — valid, expired, tampered, and from the wrong environment — to your validation endpoint and asserts the correct behavior. Store sample receipts as fixtures in your test directory. Apple’s sandbox receipts are base64-encoded and can be captured during manual testing, then replayed in automated tests. Google’s purchase tokens can be mocked by stubbing the Google Play Developer API responses.
Test your webhook handlers similarly. Simulate a refund notification from Apple or Google by sending the expected payload to your notification endpoint and verifying that the item is revoked in your database. Simulate a subscription renewal by advancing the expiration time and sending the renewal notification. These tests should run on every deployment to ensure that code changes do not break payment processing.
“We shipped with a bug where network failure during purchase would silently eat the transaction. The player was charged, but the item never appeared. We didn’t find it in sandbox testing because sandbox rarely has network issues. Once we added a test that kills the connection mid-purchase, we caught the missing reconciliation logic and fixed it before launch.”
Related Resources
For broader mobile testing strategies, see automated bug reporting for mobile games. To learn about tracking purchase-related issues in your bug tracker, read bug reporting metrics every game studio should track. For tips on handling platform-specific crashes during purchase flows, check out how to handle platform-specific crash reports.
Add a reconciliation check on every app launch this week. The next time a purchase is interrupted by a lost connection, the player will get their item instead of a support ticket.