Purchase Testing

Test purchases in TestFlight against Apple's sandbox and verify the webhook fired.

You can't complete a real StoreKit purchase in the simulator against live products — Apple's purchase sheet needs a real device, a TestFlight build, and a sandbox tester account. This page walks the full loop: buy in sandbox → confirm RevenueCat sees the entitlement → confirm the webhook wrote your subscriptions row.

Do this only after RevenueCat Setup is complete and Apple has finished syncing products (offerings load in the app). Sandbox purchases never charge real money.

Two Apple concepts, configured separately

ConceptWhat it isWhere
TestFlight testerInstalls the beta buildApp Store Connect → TestFlight
Sandbox testerA fake Apple ID that makes test purchasesApp Store Connect → Users and Access → Sandbox Testers

The same person uses both, but they're set up in different places.

1. Ship a TestFlight build

Archive & upload

Archive the app in Xcode and upload it (see App Store Release for the full archive flow). Wait for the build to finish processing in TestFlight.

Create an internal test group

In TestFlightCreate Group (internal) → add yourself as a tester → add the build. Internal groups skip external beta review. If the build shows Missing Compliance, complete the export-compliance prompt first.

Use an appl_ SDK key, never a test_ key, in TestFlight/Release builds. A simulated-store key trips RevenueCat's release guard and the build crash-loops on launch (EXC_BREAKPOINT in checkForSimulatedStoreAPIKeyInRelease). Set REVENUECAT_IOS_KEY = appl_…, bump the build number, and re-upload.

2. Create a sandbox tester & sign in

Create the tester

In Users and Access → Sandbox Testers → +, enter an email that is not already an Apple ID, and pick a region.

Sign in on the device

Install the app from TestFlight first, then sign into the sandbox account on the iPhone. The path varies by iOS version — commonly Settings → App Store → Sandbox Account or Settings → Developer → Sandbox Apple Account. (If Developer is missing, connect the device to Xcode once to enable Developer Mode.)

3. Run the purchase test

Buy

Launch the app → sign in to your app account → open the paywall → confirm the offering loads → tap Continue → confirm Apple's sandbox sheet. The paywall should dismiss and the app should flip to the unlocked state.

Confirm RevenueCat saw it

In RevenueCat, switch to sandbox data and find the customer (their app_user_id is your Supabase user UUID). The configured entitlement (REVENUECAT_PRO_ENTITLEMENT_ID, default pro) should be active.

Confirm the webhook wrote the row

This is the account-bound half. In Supabase, check the subscriptions table for a row keyed on the same user_id with status = active (or trialing) and the right product_type. If it's there, EntitlementController reads it and every PremiumGate unlocks cross-platform.

A sandbox event only ever touches that tester's own row — RLS + the user_id-keyed upsert mean one tester can't affect another. The app's pollAfterPurchase() loop (0.8s → 1.6s → 3.0s) absorbs the few seconds between the purchase and the webhook write.

4. Test restore

Verifies an already-purchased account re-unlocks:

  1. Delete and reinstall the TestFlight app.
  2. Sign into the same sandbox account.
  3. Open the paywall → tap Restore purchases → the entitlement unlocks.

To test the first-purchase flow again, use a fresh sandbox tester or clear the current tester's purchase history in App Store Connect.

Common failures

SymptomLikely causeFix
Build crashes on launch (SIGTRAP)test_ SDK key in a Release/TestFlight buildUse the appl_ App Store key, bump build, re-upload
Paywall empty / "Couldn't load options"Offering inactive, no packages, or products still Missing MetadataActivate default, attach packages, complete product metadata
Error code 23 (config error)StoreKit can't fetch the productsMatch product IDs + bundle id exactly; complete agreements/tax/banking; wait for Apple sync (up to hours)
RevenueCat product "Could not check"Missing/weak App Store Connect API keyUpload the API key with App Manager role; verify Key ID + Issuer ID
Bought, but nothing unlocksEntitlement identifierREVENUECAT_PRO_ENTITLEMENT_IDMatch the identifier (not display name); default pro
"Already purchased", no sheetSandbox tester already owns itUse a new tester, or clear its purchase history
Entitlement active in RevenueCat, but subscriptions row missingWebhook URL/secret wrong, or app_user_id not a UUIDRe-check the webhook URL + REVENUECAT_WEBHOOK_AUTH; confirm the app logs in with the Supabase UUID

Deeper, non-App-Store-specific issues (Keychain session, deep links, xcconfig substitution) live in Troubleshooting. This page keeps the purchase-flow-specific ones.

Useful logs

For verbose RevenueCat logs on a real device during a debug build, the template already sets Purchases.logLevel = .warn in DEBUG. To go louder temporarily:

#if DEBUG
Purchases.logLevel = .debug
#endif

A healthy pre-purchase log shows empty entitlements; after a buy/restore the active list includes your entitlement id:

activeEntitlements=[]              // before
activeEntitlements=["pro"]         // after purchase/restore

In Xcode, watch the debug console (View → Debug Area → Activate Console) or device logs (Window → Devices and Simulators → Open Console) filtered for RevenueCat / Purchases / StoreKit.

On this page