App Store

    Does Apple App Review check for hardcoded Stripe secret keys in 2026?

    Apple App Review and hardcoded Stripe secret keys in iOS apps

    If you are about to submit an iOS app that takes payments through Stripe, and someone on your team is worried that the Stripe secret key ended up in the build, this is the page that tells you what Apple will and will not catch, and what you should do about it before you press Submit.

    Short answer

    Apple App Review in 2026 does not systematically scan IPA binaries for Stripe secret key patterns, and apps with hardcoded sk_live_ tokens are approved and shipped every week. According to Stripe's official API keys documentation, only the publishable key is "safe to expose outside your application's backend"; secret keys, restricted keys, and organisation keys are all server-side credentials. The check has to happen on your side before submission, because a leaked sk_live_ key lets the holder charge your customers and read every transaction on your account.

    What you should know

    • Stripe defines four key types. Publishable (pk_), secret (sk_), restricted (rk_), and organisation (sk_org_). Only the first one belongs in a mobile app.
    • Apple's review does not check for any of these. Apple's automated checks focus on private APIs and entitlements; the human reviewer tests user flows.
    • A leaked sk_live_ key has unrestricted Stripe API access. No IP allow-list, no time limit, no scope. The first attacker to find it owns the Stripe account.
    • Stripe is deprecating secret keys for new use cases. The official guidance is to use restricted keys with explicit endpoint permissions.
    • The architectural fix is Payment Intents server-side plus Stripe.js Elements or the iOS SDK client-side. No secret ever reaches the device.

    What does Apple's review actually look for?

    Apple's App Review checks fall into five published categories: safety, performance, business, design, and legal. Per the App Review section of developer.apple.com, submissions pass through automated checks first, then a human reviewer.

    The automated layer reportedly catches private API usage, prohibited frameworks, and missing entitlements. The human reviewer opens the build on a device, walks through the main user flows, checks privacy disclosures, confirms metadata accuracy. Neither layer involves decompiling the binary or running pattern matching against the strings inside it.

    There is no published Apple guideline that lists "hardcoded third-party credentials" as a rejection reason. The closest the guidelines come is section 5.1.1 of the App Review Guidelines, which says developers are responsible for protecting any user data they collect. The wording assigns responsibility; it does not commit Apple to enforcing it via binary scans.

    What are Stripe's four key types and why do prefixes matter?

    Stripe distinguishes four key types by prefix, each with a distinct purpose:

    PrefixKey typeSafe in client?What it grants
    pk_test_ / pk_live_PublishableYesCreates Payment Intents and confirms cards via Stripe.js or the iOS SDK. Cannot charge or read account data on its own.
    sk_test_ / sk_live_SecretNoUnrestricted access to every Stripe API: charge, refund, read customers, read transactions, manage subscriptions.
    rk_test_ / rk_live_RestrictedNoScoped permissions across endpoints (for example, read-only on Charges). Still server-only.
    sk_org_OrganisationNoAccount-level access for multi-account ownership. Server-only.

    The prefix is part of the key string, not just a label, which makes the diagnostic check trivial. strings MyApp \| grep -E 'sk_(live\|test\|org)_\|rk_(live\|test)_' finds every secret-class Stripe key in the binary. Anything that matches is a finding.

    Why does Stripe push developers toward restricted keys?

    Stripe's own documentation now says: "Because you can't limit their permissions, we don't recommend using secret keys for new use cases, and for existing integrations, we recommend migrating secret key usage to RAKs." The reasoning is the same one that makes them dangerous in a client binary: a leaked secret key has unrestricted scope, and even a careful incident response leaves a window where every Stripe operation is at risk.

    Restricted keys constrain the damage. A key scoped to Charges: read cannot issue refunds, even if it leaks. A key scoped to a single endpoint cannot pivot. The cost is configuration overhead; the benefit is bounded blast radius.

    None of this changes the rule for client-side code. Restricted keys are still server-side. The architectural pattern Stripe recommends for mobile apps is the same in 2026 as it was in 2020: publishable key in the client, Payment Intents created server-side, the client confirming the intent via Stripe.js Elements or the iOS SDK.

    What is the correct architecture for taking Stripe payments from an iOS app?

    The pattern Stripe documents end-to-end:

    1. iOS client calls your backend with the order details (amount, currency, customer reference).
    2. Your backend authenticates the request, then calls PaymentIntents.create against Stripe using your secret key (or, preferably, a restricted key scoped to PaymentIntents).
    3. Backend returns the Payment Intent's client_secret to the iOS client.
    4. iOS client uses the Stripe iOS SDK and the client_secret to collect card details and confirm the payment. The publishable key authenticates the client side; the client_secret authorises the specific intent.
    5. Stripe sends a webhook to your backend on payment success or failure. Backend updates the order status.

    At no point does the iOS app see your secret key, restricted key, or any other credential that grants account-level access. The publishable key plus a single-use client_secret is enough to take the payment.

    For apps built with no-code or AI-coded backends (Lovable, Bolt, Bubble, Cursor), the same architecture applies: the Payment Intent creation must happen in a server function, not in the client component. AI agents sometimes suggest pasting the secret key into the client to simplify the demo; that suggestion is wrong and produces the exact failure mode this article is about.

    How do you scan your own IPA before submission?

    Four checks, runnable on any Mac in under a minute:

    unzip MyApp.ipa
    cd Payload/MyApp.app
    strings MyApp | grep -E 'sk_(live|test|org)_|rk_(live|test)_'
    find . -name '*.plist' -exec plutil -p {} \; | grep -E 'sk_(live|test)_|rk_(live|test)_'
    find Frameworks -name '*.dylib' -exec strings {} \; | grep -E 'sk_(live|test)_'
    

    The first three lines cover the main executable and Info.plist. The fourth covers embedded frameworks, which is the failure mode that catches developers who think they removed the key when they only removed it from the top-level binary.

    For a more thorough scan that maps findings to OWASP MASVS controls, PTKD.com (https://ptkd.com) accepts an IPA upload and runs the same pattern matching plus several dozen other credential patterns (AWS, Twilio, SendGrid, OpenAI, Anthropic, Supabase, Firebase). The report calls out Stripe findings under MASVS-STORAGE-1 ("the app does not store any sensitive data in publicly accessible locations") and includes the architectural rewrite for each finding.

    What to watch out for

    Three details that recur in audits of payment-handling iOS apps.

    First, test-mode keys (sk_test_) are not safe to leave in a shipped binary. They authenticate to your Stripe test environment, which often contains real customer data copied over for testing. The strings check has to grep for both sk_live_ and sk_test_.

    Second, the Stripe publishable key in test mode (pk_test_) is not the same as production (pk_live_). Apps that ship with pk_test_ in the binary are pointing at the test environment, which means real customer transactions silently fail or go to the wrong place. Confirm the publishable key is pk_live_ before submission.

    Third, Stripe's webhook signing secret is also server-side. Some apps store the webhook secret in the iOS bundle as part of a generic environment-variable copy operation. The webhook secret cannot charge cards, but it can let an attacker forge webhooks that mark orders as paid. Treat it the same as a secret key.

    Key takeaways

    • Apple App Review does not check for hardcoded Stripe keys. The strings command an attacker would run takes you ten seconds.
    • Stripe defines four key types; only the publishable key is safe in mobile apps. Everything else stays server-side.
    • The architectural pattern is Payment Intents server-side, Stripe iOS SDK client-side, client_secret as the per-payment authorisation. No secret reaches the device.
    • For mobile apps that bundle payment SDKs, PTKD.com (https://ptkd.com) scans IPAs for Stripe, AWS, and dozens of other credential patterns and maps each finding to OWASP MASVS.
    • Document the credential audit in your CHANGELOG so the next AI-generated build does not silently reintroduce the key.
    • #app-store
    • #stripe
    • #hardcoded-keys
    • #ios
    • #ipa
    • #payments

    Frequently asked questions

    What is the difference between a publishable key and a secret key in Stripe?
    The publishable key (pk_live_) authenticates client-side calls that create payment intents but cannot, by itself, charge a card or read account data. The secret key (sk_live_) authenticates server-to-Stripe calls and has unrestricted access to your Stripe account: refunds, charges, customer data, payouts. The publishable key is safe in iOS apps; the secret key is never safe outside a trusted backend.
    What about Stripe's restricted API keys (rk_live_)?
    Restricted keys let you scope a key to specific endpoints (for instance, read-only access to charges). They are still server-only. Stripe's documentation describes them as 'API key with permissions you control', and explicitly notes that 'you can't limit their permissions' for secret keys, which is why Stripe now recommends migrating secret key usage to restricted keys. Neither restricted nor secret keys belong in a mobile app.
    Can an attacker actually use a Stripe sk_live_ key from a stolen IPA?
    Yes, instantly, from any internet-connected machine. A POST to api.stripe.com with the secret key in the Authorization header returns customer lists, charge histories, and lets the holder create refunds or new charges against your saved payment methods. There is no 'mobile-only' restriction on Stripe secret keys; they work from any HTTP client.
    Will Apple reject an app if it crashes because the secret key was revoked mid-review?
    Apple will reject for crashes or broken core functionality, which is unrelated to the credential leak itself. The crash happens because revoking the secret key breaks any client call that used it. If you discover the key in your build during a live review, replace it with the publishable key plus a Payment Intent created server-side before resubmitting.
    How do I know if my app currently ships a Stripe secret key?
    Run 'strings' against the compiled binary in your IPA and grep for 'sk_live_' or 'sk_test_'. Both prefixes are diagnostic; legitimate uses of those prefixes inside an iOS binary do not exist. The same check finds restricted keys (rk_live_, rk_test_) and organisation keys (sk_org_). Any hit is a finding.

    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