Payments in Apps
Store billing, your own processor, and the entitlements layer that unifies them.
After this lesson you can explain
- Why a pizza order pays the store nothing while a sticker pack pays 30%: the digital-versus-physical classification and everything that follows from it.
- What happens between the tap on Buy and the content unlocking: the purchase token, server-side verification, and the entitlements table.
- Why a subscriber is a state machine rather than a paid/unpaid boolean, and how the store's notification stream drives it.
1. Everything starts from one classification
The stores draw a hard line between two kinds of purchase, and every rule downstream follows from which side you are on:
| Digital goods | Physical goods & services | |
|---|---|---|
| Examples | Book access, premium features, subscriptions, coins, sticker packs | Food, rides, parcels, hotel rooms, event tickets |
| Payment rail | Must use store billing (Play Billing / StoreKit) | Must not use store billing; use your own processor |
| Cut | 15–30% to the store | ~3% to your processor (Stripe, Adyen…) |
| Merchant of record | The store (they handle VAT, refunds, currency) | You |
This is why Domino's pays Google nothing on a pizza while a €5 sticker pack pays €1.50, and why (from Web Apps vs Native Apps) takeaway chains happily live in the stores while content sellers push purchases to the web.
The wall is cracking, but is not gone. EU regulation (the Digital Markets Act) and the US Epic rulings have forced both stores to tolerate external payment links and alternative billing in various jurisdictions, on shifting, jurisdiction-specific, and often still-commissioned terms. Treat "the 15–30% is escapable" as in flux: check the current rules for your store, country, and app category before building a pricing strategy on them.
2. Store billing end to end
The flow has the same skeleton as login and push: a platform intermediary, an opaque token, and your backend verifying that token before trusting anything.
App Store (Play/App Store) Your backend
| | |
|--1 query products -------->| |
|<-- localized prices -------| |
|--2 user taps Buy | |
| OS purchase sheet ------>| (app never sees payment |
|<-- purchase token ---------| details) |
|--3 POST /purchases { token } --------------------------->
| |<-- verify token (server- |
| | to-server API call) ----|
| |--- valid + details ------->|
| | grant entitlement in |
|<-------- entitlement confirmed --------- your DB ------|
| | |
| |--4 server notifications -->| (renewal, cancel,
| | (webhooks) | refund, retry...)
| | entitlements updated --|
Step 1: Products live in the store, not your code
SKUs and price tiers are defined in the Play Console / App Store Connect. The app queries the store SDK at runtime and gets prices back pre-localized in the user's currency, tax-inclusive where required. Your code never hardcodes a price.
Step 2: The purchase sheet belongs to the OS
The confirmation sheet is drawn by the operating system; the user pays with their stored store payment method (card, carrier billing, gift balance). Your app never sees card numbers, which is also why the store, not you, carries the PCI compliance burden. What your app receives is a purchase token (Google) / signed transaction (Apple): an opaque proof-of-purchase.
Step 3: Verify server-side, then grant
The app sends the token to your backend, which calls the store's verification API server-to-server, checks the product ID and purchase state, and only then writes the entitlement to your database.
Never trust the client's word that a purchase happened. Tampered clients that report fake successful purchases are the oldest trick in mobile; apps that check entitlement client-side only are precisely the ones that circulate as cracked APKs. The rule is the same one from the login lesson (verify the ID token) and the sync lesson (the server owns truth): the client reports; the server verifies and decides.
Step 4: The store keeps talking: server notifications
Purchases are not one-shot events. Apple's App Store Server Notifications and Google's Real-time Developer Notifications deliver a webhook stream: renewals, cancellations, refunds, billing failures, grace periods. Handle them exactly like a Stripe webhook: verify the signature, process idempotently, update the entitlements table. Missing the refund notification is how apps end up giving lifetime access to refunded purchases.
3. Subscriptions are a state machine, not a boolean
The biggest engineering surprise in store payments: a subscriber is never simply "paid or not." They are in one of several states, driven by the notification stream:
| State | What it means | Access? |
|---|---|---|
| Active | Paid, current period | Yes |
| Grace period / billing retry | Renewal payment failed (expired card); store is retrying | Yes: cutting access here is the classic complaint-generator |
| On hold / paused | Retries exhausted, or user paused | No, but the subscription can still recover |
| Cancelled (period remaining) | User cancelled auto-renew but paid through the period end | Yes, until expiry |
| Expired | Period over, no renewal | No |
| Refunded / revoked | Store clawed the money back | No; requires acting on the notification |
Also in this layer: free trials, intro offers, upgrades/downgrades with proration, and win-back offers, all defined in the store console, all surfaced to you through the same notifications.
The rates worth knowing: the headline 30% drops to 15% for your first $1M/year under both stores' small-business programs, and Apple subscriptions drop to 15% after a subscriber's first year. For small developers, "the store takes 30%" is usually really "the store takes 15%."
4. Your own processor in an app (physical goods, services)
When store billing is not required, the in-app flow is your website's Stripe flow in native dress:
- Your backend creates a PaymentIntent (Stripe) and returns its client secret to the app.
- The app shows Stripe's native PaymentSheet, a drop-in sheet handling cards, Apple Pay, and Google Pay. (Apple Pay/Google Pay here are wallets over your processor at ~3%, not store billing; a common confusion.)
- Confirmation and fulfilment run through your existing webhook: verify, process idempotently, grant.
Same artefact shapes as section 2: a server-created intent instead of a store product, a confirmation instead of a purchase token, a webhook stream either way.
5. The pattern for digital content sellers: sell on the web, unlock in the app
Netflix, Spotify, and Kindle famously sell nothing inside their iOS apps. The purchase happens on the website at web economics; the app is a free viewer where the user simply logs in. This is the reader app pattern, and it is exactly how an Endpoint 51 companion app would work:
- The €35 purchase stays on Stripe at ~3%, on the website, with the existing checkout.
- The app is free to download, authenticates with the existing magic-link/token system, and asks the backend what the user is entitled to.
- No store billing is involved because no purchase happens in the app.
Historically the stores' anti-steering rules forbade even linking from the app to the website's checkout, or mentioning that purchasing elsewhere was possible. Those are the rules the DMA and the Epic rulings have been dismantling, so this pattern keeps getting easier, but the exact wording of what a link may say still varies by store and jurisdiction; check current policy at build time.
6. The unifying architecture: one entitlements table
Whatever the payment rail, the durable design is a single source of truth in your backend for "what has this user paid for," fed by every rail:
Stripe webhook ─────────────┐ Play token verification ─────┤──► entitlements table ◄──── queried by App Store notifications ─────┤ (user_id, product, website, app, Manual grants (support) ─────┘ status, expires_at) API middleware
- Clients (web or app) only ever ask your API "what am I entitled to?" They never reason about receipts themselves.
- Every rail's webhook/notification handler is a writer to the same table, processed idempotently.
- This is an extension of the existing Endpoint 51 architecture: the Stripe webhook already writes "this email has lifetime access"; store rails would simply be additional writers.
Cross-platform services like RevenueCat exist purely to outsource this layer: they verify receipts on both stores, run the subscription state machine, and expose one "entitlements" API. The trade is the usual one: convenience and cross-store neatness against a per-revenue fee and a third party in your payment path.
7. Merchant of record: the honest case for the store cut
The 15–30% buys something real: the store is the merchant of record. It charges the customer in 175 countries' currencies, collects and remits VAT/sales tax everywhere, fields chargebacks, and processes refunds. With Stripe, you are the merchant of record: tax registration thresholds, cross-border VAT on digital goods (an EU seller knows this pain), and chargeback handling are yours.
| Rail | Fee (rough) | Who handles global tax |
|---|---|---|
| Stripe (you as merchant of record) | ~3% | You |
| Merchant-of-record processors (Paddle, Lemon Squeezy) | ~5% + fixed fee | They do (the web-world middle option) |
| Store billing | 15–30% | The store |
(Engineering context, not tax advice; the actual obligations depend on jurisdiction and turnover, and that part belongs to an accountant.)
8. Common gotchas
| Symptom | Cause |
|---|---|
| Cracked APKs get your premium features free | Entitlement checked client-side only; no server verification of purchase tokens |
| Refunded users keep lifetime access | Refund/revocation notifications not handled |
| Paying subscribers locked out for a day | Grace-period state treated as expired |
| User reinstalls and their purchase is "gone" | No restore-purchases path (store review requires one); entitlements must be recoverable by account, not by device |
| Duplicate grants from one purchase | Notification handlers not idempotent; stores redeliver, exactly like Stripe |
| Purchase succeeded but the app crashed before granting | Grant must be driven by backend verification, not by the in-app success callback; unfinished transactions must be re-processed on next launch |
| App rejected at review over payments | Digital goods routed around store billing, or a non-compliant external-purchase link; the classification in §1 is enforced at review |
| Prices wrong in some countries | Hardcoded prices instead of the store SDK's localized ones |
The takeaway shape: payments repeat the course's pattern a third time. A platform intermediary, an opaque token (the receipt), server-side verification as the security boundary, and a webhook stream your backend consumes idempotently into a single source of truth. If you can run a Stripe webhook (and Endpoint 51 already does), you already know how store payments work; only the vocabulary and the commission change.
Check your understanding
- Which side of the digital/physical line must use store billing, and roughly what cut does each rail take?
- Why must the entitlement grant be driven by your backend verifying the purchase token, not by the app's success callback?
- A subscriber's renewal payment just failed. What state are they in, should they keep access, and how does your backend find out?
Sources and further reading
Reviewed 4 August 2026.