Support

Lesson 8 of 11 ·12 min read

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 goodsPhysical 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 --|
Store console

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.

OS

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.

Your backend

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.

Store → Your backend

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:

StateWhat it meansAccess?
ActivePaid, current periodYes
Grace period / billing retryRenewal payment failed (expired card); store is retryingYes: cutting access here is the classic complaint-generator
On hold / pausedRetries exhausted, or user pausedNo, but the subscription can still recover
Cancelled (period remaining)User cancelled auto-renew but paid through the period endYes, until expiry
ExpiredPeriod over, no renewalNo
Refunded / revokedStore clawed the money backNo; 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:

  1. Your backend creates a PaymentIntent (Stripe) and returns its client secret to the app.
  2. 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.)
  3. 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.

RailFee (rough)Who handles global tax
Stripe (you as merchant of record)~3%You
Merchant-of-record processors (Paddle, Lemon Squeezy)~5% + fixed feeThey do (the web-world middle option)
Store billing15–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

SymptomCause
Cracked APKs get your premium features freeEntitlement checked client-side only; no server verification of purchase tokens
Refunded users keep lifetime accessRefund/revocation notifications not handled
Paying subscribers locked out for a dayGrace-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 purchaseNotification handlers not idempotent; stores redeliver, exactly like Stripe
Purchase succeeded but the app crashed before grantingGrant 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 paymentsDigital goods routed around store billing, or a non-compliant external-purchase link; the classification in §1 is enforced at review
Prices wrong in some countriesHardcoded 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?

Reviewed 4 August 2026.