Resources / Field guide
Taking iOS payments outside Apple's IAP
Apple's in-app purchase takes a 15–30% cut. For many apps you can charge through Stripe instead: send the user to Safari for checkout, then deep-link them back into the app to unlock access. Here's the architecture, the App Store rules that govern it, and the Swift wiring to receive the return.
Why bother
Apple's commission on in-app purchases is 15–30%. On a subscription business that's the difference between healthy margins and thin ones. If your product is also sold on the web, charging through your existing Stripe setup keeps payments in one place, gives you full control of the billing experience, and keeps that commission. The mechanism is a clean round trip: the app hands off to Safari for the actual payment, and Safari hands back to the app when it's done.
Rules first — and they're a moving target. What's allowed depends on app category and country, and it changed materially in May 2025: after the Epic v. Apple ruling, the US storefront now permits external purchase links, buttons, and calls to action with no special entitlement and no Apple commission. Outside the US, the older anti-steering restrictions still apply in many regions. This is an engineering guide, not legal advice — always check the current App Store Review Guidelines (3.1.1 / 3.1.3) for the storefronts you ship to before relying on any of this.
The safe, globally-compatible UI rules
If you want one build that passes review everywhere — including regions with the stricter rules — design the in-app UI so it never sells anything; the website does the selling. The conservative line that keeps review smooth:
- Don't show prices or plan names in the app, don't label a button "Subscribe / Buy / Purchase / Upgrade", and don't show a plan comparison in-app.
- Do use neutral wording — "Get Full Access", "Manage Account", "Continue on the web" — open Safari to an external URL, and receive a deep link back.
App review specifically looks for in-app purchase flows. If the app shows no prices and no "buy" button, the purchase happens entirely on your site, which is exactly where you want it. (On the US storefront post-2025 you can be more direct, but the neutral approach is the safe default for a single global build.)
The flow
- User taps "Get Full Access". The app opens Safari to your payment page.
- The website runs Stripe Checkout. The user pays on the web.
- On success, your site redirects to your app's custom URL scheme — e.g.
myapp://payment-complete?token=…. - iOS reopens the app with that URL; the app confirms the payment with your backend and moves the user into the paid experience.
Step 1 — Register a URL scheme
If you already do password-reset or magic-link deep links you likely have one; reuse it. In Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>com.yourcompany.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
Define one path per outcome so the app knows what just happened:
myapp://payment-complete?token={accountCreationToken} # new user paid
myapp://subscription-updated # existing user upgraded
myapp://reset-password?token={token} # (likely already exists)
Step 2 — Handle the incoming deep link
SwiftUI's app lifecycle exposes onOpenURL:
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
DeepLinkHandler.shared.handle(url)
}
}
}
}
On UIKit, the same arrives in the scene delegate:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
DeepLinkHandler.shared.handle(url)
}
Step 3 — Confirm, don't trust
Treat the deep link as a signal, not proof of payment — a URL is trivially forged. When payment-complete arrives, call your backend to verify the session/subscription (your server already knows the truth from Stripe, ideally confirmed by a Stripe webhook), and only then flip the user to paid:
func handle(_ url: URL) {
guard url.scheme == "myapp" else { return }
switch url.host {
case "payment-complete":
let token = url.queryItem("token")
Task { await AccountService.confirmPayment(token: token) } // verify server-side
case "subscription-updated":
Task { await AccountService.refreshEntitlements() }
default:
break
}
}
Have the website show a clear "return to the app" affordance too — auto-redirects to a custom scheme can be blocked, so a tappable "Open MyApp" link is a reliable fallback.
When this fits
This pattern suits apps that already sell on the web and have a Stripe account — the app rides the billing system you maintain rather than duplicating it in StoreKit. If iOS is your only surface and you have no web presence, native in-app purchase may genuinely be less work. And wherever you ship, let the current App Store guidelines for that storefront decide how direct your in-app wording can be.
Building payments into an app?
Stripe, deep links, web-to-app round trips, subscription logic — done properly and tested end to end. I take this on directly. Tell me what you're building and let's talk.
Work with me