App Store

    Why In-App Purchases Need Server-Side Validation

    A purchase flow where the app sends a transaction identifier to a backend, which queries the App Store Server API before granting entitlement.

    The purchase succeeded on the device. That sentence is where most in-app purchase security goes wrong, because the device is the one participant in the transaction that the user controls. A client that releases content because a local call returned success has built an entitlement system that can be told what to believe.

    Short answer

    Validate purchases on a server you control, using Apple's App Store Server API to query transaction and subscription status directly rather than trusting what the client reports. StoreKit handles the purchase flow on the device; it does not make the device a trustworthy source of entitlement. The equivalent applies on the other platform, where Play billing subscriptions are verified server-side against Google rather than from the app.

    Why client-side validation fails

    The failure is structural rather than a matter of implementation quality.

    Everything the client does happens on hardware the user owns. A method returning whether a purchase is valid is a method that can be patched to return true. A network call to Apple can be intercepted and answered by something that is not Apple. A receipt stored locally can be replaced with one obtained elsewhere.

    None of this requires sophistication. Tooling to modify a running app and change what a function returns is widely available and does not need a rooted device in every case. The economics favour the attacker, because the work is done once and then packaged for people who could not have done it themselves.

    The second problem with client-side validation is that it produces no record. When entitlement lives only on the device, you cannot answer basic questions afterwards: which accounts have access, when it started, whether a refund was issued. A support request becomes guesswork, and a revenue discrepancy becomes unresolvable.

    The third is that it does not survive device changes. A user who reinstalls, switches phones, or uses your app on a second device expects their purchase to follow them. That expectation can only be met by a server that knows what they bought, which means you need the server anyway. Teams that build client-side entitlement almost always end up building the server component later, under worse conditions, with existing users in an inconsistent state.

    What server-side validation looks like

    The shape is straightforward once the trust boundary is drawn correctly.

    The app completes the purchase through the platform's normal flow and receives a transaction identifier or a signed representation of the purchase.

    The app sends that to your server. Your server does not trust it as an assertion; it treats it as a lookup key.

    Your server asks the platform directly whether that transaction exists, what product it was for, whether it is still valid, and whether it has been refunded or revoked. Apple's server API is the mechanism for this, and the answer comes from Apple rather than from the device.

    Your server records the entitlement against the user account, with the product, the start, the expiry if it is a subscription, and the source transaction.

    The app asks your server what the user is entitled to. Not the platform, and not its own local state.

    That last step is the one teams skip. An app that validates correctly on purchase and then caches entitlement locally forever has moved the problem rather than solved it.

    Handling the states that are not success

    Purchases have more states than bought and not bought, and each needs a defined behaviour.

    Pending. Some purchases require an action outside your app, such as a parental approval or a payment method that settles later. The correct handling grants nothing yet and tells the user what is happening, rather than failing silently.

    Refunded and revoked. A purchase can be reversed after the fact, sometimes long after. Server-side notifications are how you learn about this; polling on next launch is how you learn about it late. Entitlement systems that never process revocations accumulate users with access they no longer paid for.

    Expired. Subscriptions end. The server, not the client clock, decides when, because a device clock is adjustable and a subscription that checks the local date is a subscription that ends whenever the user says.

    Restored. A user reinstalling expects their purchase back. With server-side entitlement this is a lookup rather than a special flow, which is one of the practical reasons the architecture is worth the effort.

    Family or shared purchases. Where the platform supports sharing, the entitlement belongs to more than one account, and a model assuming one purchase equals one user will produce support tickets.

    The mistakes that reach production

    Trusting a client-supplied boolean. Any request where the app tells the server that the user is a subscriber is a request the user can make themselves.

    Validating once and never again. Entitlement is a state that changes, and an app checking only at purchase time will serve content to users whose subscriptions lapsed months ago.

    Using the local clock for expiry. Adjustable, and adjusted more often than teams expect.

    Treating a receipt as a secret. Receipts and transaction identifiers travel; treat them as claims to be verified, not as credentials.

    Ignoring the review requirement. Apple's App Review Guidelines are specific about using the platform's purchase mechanisms for digital content, and an entitlement design routing around them is a rejection risk independent of the security question.

    Building the entire thing in the client because a server felt like scope creep, which is the decision underneath most of the others.

    Where an artifact check helps

    Server-side validation is a backend design question, and the client half still has failure modes visible in a build.

    The common ones are an API key for a validation service compiled into the app, a debug flag that grants entitlement for testing left enabled, a hardcoded list of accounts with free access, and entitlement state written to unprotected storage where it can be edited directly.

    Each of those defeats a correct server design from the client side, and each is visible in a shipped artifact rather than in a code review of the backend. An automated pass such as PTKD.com covers that class of problem: secrets in the bundle, debuggable builds, and sensitive state stored without protection.

    Then confirm the server half by testing it directly. Call your entitlement endpoint with a modified request and see whether it disagrees with the client. If it does not, the validation is decorative.

    Getting the caching right

    Server-side entitlement raises an obvious objection: the app now needs a network call to know what the user paid for, and networks fail.

    The answer is a cache with a defined lifetime rather than no cache or a permanent one.

    Cache the entitlement response for a bounded period, long enough that a user in a tunnel keeps their subscription and short enough that a lapse or revocation takes effect within a reasonable window. Hours rather than days is the usual shape for a monthly subscription.

    Store the cached entitlement somewhere the user cannot trivially edit, and treat it as a convenience rather than as the source of truth. A cached value that has been tampered with should fail closed at the next successful refresh.

    Sign the entitlement response if the value being protected is high. A signature your app verifies means an intercepted or modified response fails verification, which closes the gap between the server deciding and the client believing.

    And define offline behaviour explicitly. Some products should keep working offline indefinitely once purchased; others should not. Deciding is better than discovering, because the default behaviour of a cache is whichever the implementation happened to produce.

    Testing the entitlement path

    Four tests cover most of the failure surface, and none of them are difficult.

    Buy, then modify the client response to claim a higher tier and confirm the server disagrees.

    Buy, then request a refund through the platform's normal process and confirm entitlement is revoked within your expected window.

    Buy on one device, install on a second, and confirm the purchase follows the account rather than the hardware.

    Set the device clock forward past a subscription expiry and confirm nothing changes until the server says so.

    That last one catches the local-clock assumption faster than any code review, and it is the mistake most likely to still be present in an otherwise correct implementation.

    What this costs to build

    Being honest about effort helps, since the reason teams skip this is that it looks like a large project.

    The minimum viable version is one endpoint that accepts a transaction identifier, calls the platform's server API, writes a row, and returns the entitlement. That is a day of work for someone familiar with the backend, not a quarter.

    The parts that take longer are the ones people underestimate: handling server notifications for refunds and renewals, reconciling users who purchased before the server existed, and the support tooling to answer questions about a specific account.

    Building it before you have users is much cheaper than building it after, which is the practical argument for doing it at launch even when the revenue does not yet justify the engineering. Reconciliation is the part that turns a one-day job into a two-week one, and it only exists because purchases were made before the records did.

    Why review cares about this too

    Entitlement design is usually framed as a revenue-protection question, and it has a store-policy dimension that is worth understanding before a rejection makes it urgent.

    Both stores require that digital content and services consumed inside the app are sold through the platform's purchase mechanism, with a set of documented exceptions. An entitlement system that grants access based on a purchase made elsewhere, in a way the guidelines do not permit, is a policy issue rather than a technical one, and it is the kind that surfaces at review rather than in testing.

    The adjacent problem is a reviewer being unable to test the paid experience. An app whose premium features are gated behind a server your reviewer cannot exercise will be rejected for being incomplete, and the fix is a documented test account provided in the review notes rather than a special build.

    Neither of these argues against server-side validation. They argue for deciding your entitlement model deliberately and writing down how a reviewer reaches the paid state, which takes ten minutes and prevents a rejection cycle measured in days.

    A closing note on trust boundaries

    The specific advice here generalises, and the general version is worth carrying to other features.

    Any decision the client makes that the server then honours is a decision the user makes. Entitlement is the clearest case because money is involved, and the same reasoning applies to feature flags that gate access, role checks performed in the app, and content filters implemented client-side.

    The question to ask of any check is where it would still hold if the app were replaced by a script sending the same requests. If the answer is that it would not, the check is a user interface behaviour rather than a control, and it should be documented as such so that nobody later builds something load-bearing on top of it.

    What to take away

    The device is not a trustworthy source of entitlement, because the user controls it. Draw the trust boundary at your server and keep it there.

    Validate against the platform's server API rather than against what the app reports, and record the result against the user account.

    Ask your own server what a user is entitled to, rather than caching the answer on the device indefinitely.

    Handle pending, refunded, revoked, expired, restored and shared states explicitly, because each of them produces a distinct support problem when it is not handled.

    And check the client artifact for the shortcuts that undo a good server design, since a test flag or a hardcoded exemption in the shipped build defeats the architecture regardless of how correct the backend is. The backend and the binary have to agree, and only one of them is visible to the person attacking it.

    • #in-app purchase
    • #storekit
    • #receipt validation
    • #subscriptions
    • #entitlement
    • #app store

    Frequently asked questions

    Why is client-side purchase validation insecure?
    Because every part of it runs on hardware the user controls. A method that returns whether a purchase is valid can be patched to return true, a network call to the platform can be answered by something that is not the platform, and a locally stored receipt can be replaced. The tooling for this is widely available and packaged for people who could not build it themselves.
    How does server-side receipt validation work?
    The app completes the purchase normally and receives a transaction identifier, then sends it to your server as a lookup key rather than as an assertion. Your server queries the platform's server API directly to confirm the transaction exists, which product it covers, whether it is still valid and whether it was refunded. It then records the entitlement against the user account.
    Do I still need a server if I only sell one-time purchases?
    In practice yes, because users expect purchases to follow them across reinstalls and devices, and only a server that knows what they bought can deliver that. Teams that build client-side entitlement for simplicity usually end up adding the server component later under worse conditions, with existing users in an inconsistent state that has to be reconciled.
    What happens when a purchase is refunded?
    Entitlement should be revoked, and the way you find out matters. Server-side notifications from the platform tell you promptly; discovering it when the app next launches tells you late, and never checking means accumulating users who retain access they no longer paid for. Refunds can arrive long after the purchase, so this is an ongoing process rather than a purchase-time check.
    Can subscription expiry be checked on the device?
    Not reliably, because the device clock is adjustable and a subscription evaluated against the local date ends whenever the user decides. Expiry belongs on the server, evaluated against the status the platform reports. The client should ask your server what the user is currently entitled to rather than computing that answer from a stored expiry date.

    Keep reading

    Scan your app in minutes

    Upload an APK, AAB, or IPA. PTKD returns an OWASP-aligned report with copy-paste fixes.

    Try PTKD free