In-App Purchases Advanced

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.

45 min Octacer Engineering July 8, 2026
A mobile developer at night studying a store-installed phone tethered to a workstation, where a single completed purchase step glows green while the entitlement step stays dark.

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.

  1. 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].
  2. 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].
  3. 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].
  4. 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].
  5. 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].
  6. 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.

  1. 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].
  2. 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].
  3. 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. 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. 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