Restoring Premium After In-App Purchase Failures: A Playbook
A diagnostic and recovery workflow for purchases that fail to unlock premium, covering store configuration, backend validation, and restore failures.
When an in-app purchase completes but premium never unlocks
A subscription mobile app takes payment in the store, the store shows the purchase as complete, yet the account never flips to premium. This playbook takes a stressed responder from that symptom to a confirmed root cause with read-only diagnostics first, then a least-to-most-invasive fix menu. The first fork is the one that saves the most time: decide early whether the store returned no products at all (a store-configuration precondition) or the purchase completed and the backend rejected it (a validation bug) [279][1127]. Work top to bottom; do not ship a client change before you have read the store-to-backend handshake.
When this applies
Use this playbook when a purchase flow completes in the store but the app never grants entitlement, or when a customer is charged (or sees "already subscribed") yet premium features stay locked [278][279].
- The store shows a completed purchase, but backend validation rejects the token and entitlement never activates; re-buying then dead-ends at "already subscribed," which means restore is also broken [278][279].
- The purchase UI never gets far enough to buy anything because the store returned no products to sell [1127].
Severity & impact
Classify by whether money is moving against locked entitlement, then by blast radius.
| Severity | Condition | Example |
|---|---|---|
| SEV-1 | Every purchase on a platform is charged but no one gets premium, or restore is broken for all buyers | Backend validation returns 400 on a platform enum mismatch and re-buy shows "already subscribed" [278][279] |
| SEV-2 | A whole platform cannot sell at all, no charges yet | Store returns no products because a paid-app agreement is still pending [1127] |
| SEV-3 | Isolated to a single build, account, or test device not reproducible on a store-installed build | Behaviour seen only in a simulator or dev build [277][278] |
Roles
Assign these before touching anything. Keep names generic; fill from whoever is on rotation.
- Incident owner (on-call): runs the triage tree, decides mitigations, owns the timeline.
- Comms lead: posts status updates, fields the "I paid and got nothing" customer questions, shields the owner.
- Backend owner: subject expert for the validation API, the purchase-token check, and the platform enum mapping [278][279].
- Store/account owner: holds access to the store consoles and can see whether an agreement or paid-app precondition is still pending [1127].
For a SEV-3 the owner may hold every role. For a platform-wide SEV-1, split comms out immediately.
Triage steps
Run diagnostics in this order. Everything here is read-only or state-safe. Do not push a client build, re-map an enum, or clear any state until the decision tree points you there.
- Reproduce on a store-installed build, never a simulator. A simulator or dev build hides the real store-to-backend handshake; only a store-installed build surfaces platform-specific validation behaviour [277][278].
- Capture the store logs on device while you reproduce. On Android, attach logcat to the store-installed build so you can see the real store-to-backend handshake [278].
- Determine which shape you have: did the store return products to buy? If the product list came back empty, stop and treat it as a store-configuration precondition before debugging any client code [1127].
- If products exist and the purchase completed, read the backend validation response for the purchase token. Look for a 400 rejection and note the exact reason [278].
- If validation returned 400, inspect the platform value the store sent versus what the backend maps. A purchase-token validation failure traces to the backend not mapping the store's platform value onto its subscription-platform enum [278][279].
- Test the restore path read-only: attempt a re-buy on an account that already purchased and confirm whether it dead-ends at "already subscribed." That symptom means restore is broken alongside validation [278][279].
Read-only diagnostic checklist:
[ ] Repro is on a STORE-INSTALLED build, not a simulator or dev build
[ ] On-device store logs captured during repro (logcat on Android)
[ ] Did the store return products to sell? (empty list => config precondition)
[ ] Backend validation response read for the purchase token (200 vs 400?)
[ ] Platform value the store sent vs the backend enum mapping compared
[ ] Re-buy on an already-purchased account: does it show "already subscribed"?
Decision points
Branch on what the read-only pass showed. The first branch is the store-config precondition versus the backend-validation bug — resolve it before anything else.
- If the store returned NO products, it is a store-configuration precondition, not code. The store returns no products until a pending paid-app agreement is accepted on the account side; do not debug the client [1127].
- If products exist, the purchase completed, and backend validation returned 400, it is a validation bug — the backend is not mapping the store's platform value onto its subscription-platform enum [278][279].
- If a re-buy on an already-purchased account shows "already subscribed" and premium still is not active, restore is broken and must be fixed alongside validation, not treated separately [278][279].
- If the symptom only appears in a simulator or dev build and cannot be reproduced on a store-installed build, it is a test-harness artifact, not a production entitlement bug — re-scope to SEV-3 [277][278].
Mitigation menu
Apply the least invasive fix that resolves the confirmed root cause. Move up the list only when the cheaper fix does not fit the evidence.
- Accept the pending paid-app agreement on the account side. The store returns products only once the agreement is active; this is a console change, not a code deploy [1127].
- Correct the backend platform enum mapping so the incoming store platform value maps onto the subscription-platform enum and validation stops returning 400 [278][279].
- Repair the restore-purchases path so a re-buy no longer dead-ends at "already subscribed" and an existing purchaser recovers entitlement [279].
Escalate when
Escalate the moment a cheaper fix does not fit the evidence.
- Escalate to the store/account owner when the store returns no products — the fix lives in the store console (a pending paid-app agreement), and no client or backend change will help until it is accepted [1127].
- Escalate to the backend owner when purchases complete but validation returns 400, or when restore dead-ends at "already subscribed" — both need a backend change to the enum mapping and the restore path [278][279].
Rollback criteria: if a backend mapping or restore change makes validation worse or blocks a path that previously worked, revert it and drop back to the last read-only-confirmed state before trying the next item.
Verification & recovery
Verify from the buyer's side against a store-installed build, not from a simulator or a green log line.
-
1
Verify recovery
On a store-installed build, complete a purchase and confirm the backend validates cleanly with no 400 [278][279].
Confirm premium state actually updates after validation passes — the account flips to entitled [279].
Exercise restore: re-buy or restore on an already-purchased account and confirm it no longer dead-ends at "already subscribed" [279].
Confirm there is no crash entering the subscription flow [279].
For the config-precondition branch, confirm the store now returns products once the agreement is active before declaring recovery [1127]. -
2
Acceptance criteria
Acceptance criteria: purchase completes, backend validates cleanly, premium state updates, restore works, and there is no crash entering the subscription flow [279].
Evidence & comms
Capture evidence while it is fresh, from the real store context.
- The on-device store logs from the store-installed build during repro (logcat on Android), showing the store-to-backend handshake [278].
- The backend validation response for the purchase token, including the 400 and its reason [278].
- The store platform value sent versus the backend enum mapping, to document the mismatch [278][279].
- The store-console state for the paid-app agreement (pending vs active) for the no-products branch [1127].
Status-update template:
[SEV-x] In-app purchase / premium entitlement — <UPDATE>
Impact: <who/what: charged-but-locked, restore broken, or cannot-sell>
Branch: <store-config precondition (no products) | backend validation 400 | broken restore>
Root cause: <confirmed / suspected — pending agreement, enum mismatch, restore path>
Action: <read-only repro on store build done / mitigation applied>
Next update: <time>
Post-incident
Book a short retro and turn the root cause into a preventative action.
- Add a store-configuration precondition check to release readiness so an empty product list is diagnosed as a pending agreement, not chased in client code [1127].
- Keep IAP repro on a store-installed build with on-device logs in the standard test procedure, since only that surfaces the real store-to-backend validation behaviour a simulator hides [277][278].
- Cover the platform enum mapping with a test so an unmapped store platform value cannot slip through and 400 the validation again [278][279].
- Treat validation and restore as one path: any change to token validation must re-verify that restore does not dead-end at "already subscribed" [279].
Ready to Implement This Playbook?
Our team can implement these strategies for you, tailored to your specific business needs.
Schedule Consultation