GenerateSaaS

Mobile store billing

The server side of in-app purchases - the RevenueCat webhook, how a plan's source is decided, what the web checkout does about it, and the reconcile jobs.

Store purchases are an additive lane, never a replacement: a user may be billed by your web provider or by a store, and the same account and the same entitlements serve both. The lane ships only with --mobile-purchases revenuecat; the app's own side is on mobile billing.

The flag

config.payment.mobile is declared beside config.payment.enabled, not inside it, so it reads without narrowing on the web provider first.

KeyTypeDefaultDescription
enabledbooleanfalseWhether the store lane runs, and it needs payment.enabled. The CLI writes true for --mobile-purchases revenuecat
provider"revenuecat""revenuecat"The store aggregator
allowSandboxEventsbooleanfalseWhether a sandbox purchase takes effect on a deployment running as production. A non-production deployment accepts sandbox events already
grantCreditsToFamilySharebooleanfalseWhether a family-shared purchase grants credits
externalPurchaseLinks"off" | "us-only""off"Whether the app may link out to your own checkout. "us-only" needs the US external-purchase entitlement and its store declarations
customerCenterbooleantrueShow RevenueCat's Customer Center for manage and cancel

Environment

VarPurpose
REVENUECAT_WEBHOOK_SECRETVerifies the webhook signature
REVENUECAT_AUTH_HEADERThe authorization header value RevenueCat is configured to send
REVENUECAT_API_KEYServer key for reconcile reads
EXPO_PUBLIC_REVENUECAT_IOS_KEY, EXPO_PUBLIC_REVENUECAT_ANDROID_KEYThe app's public SDK keys, in apps/mobile/.env

An empty REVENUECAT_WEBHOOK_SECRET never fails open: the webhook refuses every event rather than trusting an unverified one.

Who funds the plan

Every plan carries a source. It decides which surface may change it.

SourceBought throughManaged in
stripe / polarYour web checkoutYour billing settings
revenuecatApple or Google, in the appThe store, or the Customer Center

When the source is a store, the web billing page says the subscription is managed through the app store, hides subscribe and upgrade, and the API refuses a subscribe call for that user. That is a store requirement, not a preference: a web checkout offered over a store subscription double-bills the user.

Reconcile

Store state is authoritative and webhooks can be missed, so two background passes re-read it: a daily sweep over the lane, and a per-entity pass the webhook enqueues when one event implies another. Both are idempotent - see Background jobs.

  • POST /billing/mobile/sync is the third path: the app calls it after a purchase and after a restore, so a late webhook never leaves a paying customer at the paywall. It answers 503 when the lane is off or REVENUECAT_API_KEY is blank.

On this page