Fix: RevenueCat Entitlement Not Active After a Successful Purchase

When a purchase succeeds but the RevenueCat entitlement stays inactive, the cause is usually entitlement mapping or a stale cache. Here is how to diagnose and fix it on iOS.

The Symptom: Paid, But Still Locked

This problem shows up as a purchase that clearly succeeds, the payment sheet completes and Apple confirms the transaction, yet your check of customerInfo.entitlements["pro"]?.isActive returns false or nil, so premium features stay locked. It is one of the most frustrating states to debug because the money side worked and the code looks correct. The important insight is that a successful StoreKit transaction and an active RevenueCat entitlement are two separate things connected by configuration. The transaction proves Apple charged the user; the entitlement is RevenueCat's interpretation of what that transaction grants, which depends entirely on how you mapped products to entitlements in the dashboard and on your app reading fresh CustomerInfo. When these diverge, you get a paid-but-locked state. The good news is that the causes are a short, well-understood list: mapping mistakes, reading the wrong or stale data, entitlement identifier typos, observation timing, and testing-environment differences. Work through them in order rather than assuming the SDK is broken, because each has a concrete check and fix and the SDK itself is rarely the culprit.

Cause 1: Product Not Attached to the Entitlement

The most common cause is that the product the user purchased is not attached to the entitlement your app checks. In RevenueCat, entitlements are deliberately decoupled from products, and access is granted only when the purchased product is explicitly listed under the entitlement. If you created a pro entitlement and an offering with a monthly and an annual product, but only attached the monthly product to the entitlement, then a user who buys the annual product completes a valid purchase yet receives no active entitlement, because nothing maps that product to pro. Open the RevenueCat dashboard, go to Entitlements, select the entitlement your app checks, and confirm every product that should grant it, across all durations, is listed. Cross-reference against your offering's packages so no purchasable product is left unmapped. This single oversight accounts for a large share of paid-but-locked reports, especially right after adding a new product or a new subscription duration to an existing offering, when it is easy to update the offering but forget to attach the new product to the entitlement as well.

Cause 2: Entitlement Identifier Mismatch in Code

The second common cause is a mismatch between the entitlement identifier in your Swift code and the one defined in the dashboard. When you write customerInfo.entitlements["pro"], that string must match the entitlement identifier in RevenueCat exactly, including case. If the dashboard entitlement is named Pro or premium and your code asks for pro, the lookup returns nil and the entitlement appears inactive even though RevenueCat granted it correctly. This is easy to introduce when renaming entitlements or copying code between projects. To check, open the dashboard, note the exact entitlement identifier, and confirm your code uses the identical string everywhere it appears. A safer pattern is to define the identifier as a single constant referenced throughout the app, eliminating the chance of a typo in one place while other places are correct. Also confirm you are reading from customerInfo.entitlements, and specifically checking the active set or isActive, rather than accidentally inspecting all entitlements including inactive ones. A stable, centralized identifier prevents this whole category of bug and makes an eventual rename a one-line change instead of a scavenger hunt.

Cause 3: Reading Stale Cached CustomerInfo

RevenueCat caches CustomerInfo so your app stays responsive and works offline, but this means you can read a stale snapshot taken before the purchase completed. If you cached CustomerInfo at launch and check that old copy right after a purchase, it will not reflect the entitlement that just became active. The correct approach is to use the CustomerInfo the purchase call itself returns, since it is the freshest possible state: let result = try await Purchases.shared.purchase(package: package); let info = result.customerInfo; check info.entitlements["pro"]?.isActive on that object. Do not re-read an older stored value. When checking status elsewhere, call try await Purchases.shared.customerInfo(), which returns cached data but refreshes from the backend, and update your app's state from the result. If you maintain your own isPro flag, make sure you update it from the purchase result and from CustomerInfo updates, not from a value captured once at launch and never refreshed. Reading fresh data at the right moment resolves many apparent mapping failures that are really caching timing, so rule this out before you go hunting for a dashboard misconfiguration.

Cause 4: Not Observing CustomerInfo Updates

Entitlement state can change outside of an explicit purchase, for example when a purchase finishes processing slightly asynchronously, when a subscription renews, or when a restore on another device changes access. If your app reads entitlement status only once and never observes changes, it can miss the moment an entitlement becomes active and leave the UI locked. RevenueCat provides a way to observe CustomerInfo updates, through an async stream, Purchases.shared.customerInfoStream, or a delegate. Subscribe once, typically in a central entitlement manager that publishes an isActive flag your SwiftUI views observe, and update that flag whenever a new CustomerInfo arrives. This ensures the UI unlocks as soon as RevenueCat reports the entitlement active, without requiring the user to relaunch or manually refresh the screen. Observation also smooths edge cases where the entitlement activates a beat after the purchase call returns. Building your feature gating around observed, published state rather than a single point-in-time read makes the app both correct and responsive to every source of entitlement change, which is exactly the property you want when subscriptions renew or restore in the background.

Cause 5: Testing Environment Producing No Real Receipt

If you are testing with Xcode's local StoreKit configuration file, be aware that it simulates transactions on-device without generating the real App Store receipts RevenueCat's backend normally validates. Depending on your setup and SDK version, this can mean the server-side entitlement grant behaves differently than in sandbox or production, so a locally simulated purchase might not produce the entitlement you expect through RevenueCat's full pipeline. If your entitlement activates in sandbox but not in local StoreKit testing, this environment difference is the likely explanation, not a mapping bug, and chasing the dashboard will waste time. Confirm which environment you are in via the scheme's StoreKit Configuration setting. To verify the true entitlement path end to end, test in Apple's sandbox with a sandbox Apple ID, which produces genuine receipts that reach RevenueCat's servers and drive the real entitlement logic. Use local testing for fast iteration on UI and flow, but always validate that entitlements actually activate through RevenueCat in sandbox before shipping to real users, so you never discover the gap in production.

Cause 6: App User Identifier and Account Confusion

If your app uses its own accounts with RevenueCat app user identifiers, a mismatch there can leave a purchase attached to the wrong user, so the account you are checking shows no entitlement. For instance, if a purchase happened while the SDK was on an anonymous ID and you later called logIn with a different identifier without properly transferring the purchase, the entitlement may live on the anonymous user rather than the logged-in one. Confirm you call logIn before the purchase when using your own accounts, so the transaction attaches to the right user from the start, and understand how RevenueCat handles aliasing and transfers per their documentation. Check the RevenueCat dashboard's customer view for the identifier you expect and confirm the entitlement is present there. If it is active on a different user, you have an identity mismatch rather than a mapping problem, and the fix is in your login sequencing rather than your entitlement configuration. Getting the app user identifier right ensures the entitlement you granted and the account your app checks are one and the same, which is essential once purchases need to follow a user across devices and platforms.

A Diagnosis Checklist and Confirmation

To fix a paid-but-locked entitlement quickly, proceed in order. First, in the RevenueCat dashboard, confirm the exact product the user purchased is attached to the entitlement your app checks. Second, confirm the entitlement identifier string in your code matches the dashboard exactly, ideally via a shared constant. Third, read entitlement status from the CustomerInfo returned by the purchase call, not a stale cached copy, and elsewhere fetch fresh CustomerInfo. Fourth, observe the CustomerInfo stream so the UI reacts to updates rather than reading once. Fifth, verify you are testing in an environment that produces real receipts, using Apple sandbox to confirm the true path. Sixth, if you use your own accounts, confirm the app user identifier is correct and the entitlement lives on the expected customer. Enable Purchases.logLevel = .debug throughout to read exactly what the SDK reports at each step. Use the dashboard's customer view to confirm, from RevenueCat's side, that the entitlement is active, which cleanly separates a mapping problem from a client reading problem: if the dashboard shows the entitlement active but your app does not, the issue is in your code's reading; if the dashboard shows it inactive, the issue is in your mapping or environment.

Frequently Asked Questions

Why is my entitlement inactive when the purchase clearly succeeded?

A successful StoreKit transaction and an active entitlement are separate things linked by configuration. The usual causes are the purchased product not being attached to the entitlement in RevenueCat, an entitlement identifier typo in code, or reading stale cached CustomerInfo instead of the fresh result from the purchase call.

Where do I check that a product grants an entitlement?

In the RevenueCat dashboard under Entitlements. Select the entitlement your app checks and confirm every product that should grant it, including all subscription durations, is listed. A newly added product that was never attached will complete a valid purchase but grant no entitlement.

Should I read entitlements from the purchase result or a cached value?

From the purchase result. The CustomerInfo returned by Purchases.shared.purchase(package:) is the freshest state and reflects the entitlement that just activated. Elsewhere, call Purchases.shared.customerInfo() to get cached data that also refreshes from the backend, and update your app state from that.

Could my code's entitlement identifier be wrong?

Yes. The string in customerInfo.entitlements["pro"] must match the dashboard entitlement identifier exactly, including case. A mismatch returns nil and looks inactive even when RevenueCat granted access. Define the identifier once as a shared constant to avoid typos across the app.

Why does the entitlement work in sandbox but not in local StoreKit testing?

Xcode's local StoreKit configuration simulates transactions without producing the real receipts RevenueCat's backend validates, so the server-side entitlement path can behave differently. Verify the true entitlement flow in Apple's sandbox with a sandbox Apple ID, which generates genuine receipts that reach RevenueCat.