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
| Concept | What it is | Where |
|---|---|---|
| TestFlight tester | Installs the beta build | App Store Connect → TestFlight |
| Sandbox tester | A fake Apple ID that makes test purchases | App 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 TestFlight → Create 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:
- Delete and reinstall the TestFlight app.
- Sign into the same sandbox account.
- 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
| Symptom | Likely cause | Fix |
|---|---|---|
Build crashes on launch (SIGTRAP) | test_ SDK key in a Release/TestFlight build | Use the appl_ App Store key, bump build, re-upload |
| Paywall empty / "Couldn't load options" | Offering inactive, no packages, or products still Missing Metadata | Activate default, attach packages, complete product metadata |
| Error code 23 (config error) | StoreKit can't fetch the products | Match 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 key | Upload the API key with App Manager role; verify Key ID + Issuer ID |
| Bought, but nothing unlocks | Entitlement identifier ≠ REVENUECAT_PRO_ENTITLEMENT_ID | Match the identifier (not display name); default pro |
| "Already purchased", no sheet | Sandbox tester already owns it | Use a new tester, or clear its purchase history |
Entitlement active in RevenueCat, but subscriptions row missing | Webhook URL/secret wrong, or app_user_id not a UUID | Re-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
#endifA healthy pre-purchase log shows empty entitlements; after a buy/restore the active list includes your entitlement id:
activeEntitlements=[] // before
activeEntitlements=["pro"] // after purchase/restoreIn Xcode, watch the debug console (View → Debug Area → Activate Console) or
device logs (Window → Devices and Simulators → Open Console) filtered for
RevenueCat / Purchases / StoreKit.
