Do it yourself
$109 per platform, one-time
The WebViewGold license: a finished Xcode or Android Studio project. Paste your URL, set name and icon, switch on features, build and submit. Free updates forever.
See plans on the main pageClaude Code + payments
Inside the iOS and Android apps, digital subscriptions go through the stores; on your website, Stripe can stay. WebViewGold starts the store purchase through RevenueCat with one URL scheme, so the work in your codebase is a clean platform branch, a webhook and an entitlement check. This guide shows how to let Claude Code build exactly that and how to prove it works before review.
From $109 one-time per platform, or let our team do it all from $698.
Apple guideline 3.1.1 says digital content, features and subscriptions unlocked inside an iOS app must use in-app purchase; Google Play's Payments policy says the same for Play Billing. Physical goods and services used outside the app are exempt. The regional exceptions have moved a lot since 2025: Apple's current guidelines allow buttons and links to other payment methods in apps on the United States storefront, and Google changed its US rules after the Epic v. Google ruling. Both are worth reading in their current form before you rely on them; the simplest approach that passes everywhere is still in-app purchase inside the app and Stripe on the web. The Stripe vs in-app purchase guide walks through the decision.
The WebViewGold side is small. RevenueCat is the default integration: your button navigates to revenuecat://purchase?external_id=USER_ID&product=PRODUCT_ID, the native store sheet appears, RevenueCat records the transaction and sends a webhook to your server, and your server writes the entitlement your web app checks. restoreinapppurchases:// restores purchases and cancelinapppurchase:// opens the store's subscription settings. On iOS 15 and higher, revenuecat://launchPaywall?external_id=USER_ID&offering=OFFERING_ID shows the RevenueCat paywall you designed in their dashboard; the docs list that scheme for iOS only, so Android uses revenuecat://purchase per product. Note that using in-app purchases in an end product requires the WebViewGold Extended License (see the in-app purchase docs).
| Area | Claude Code | You |
|---|---|---|
| Store consoles | Writes the product list and paywall copy for you to paste | Paid Apps agreement, banking and tax, subscription products in App Store Connect and the Play Console |
| RevenueCat | Explains which fields to fill, nothing more | Project with both apps, products, entitlement, offering, webhook URL and its Authorization header value |
| WebViewGold | Can set the RevenueCat keys in Config.swift / Config.java when asked | Bundle ID and package name that match RevenueCat, builds, sandbox testers |
| Web app | Paywall branch, restore and manage links, webhook route, entitlement table, polling after purchase, a guard command against Stripe inside the app | Approve the plan, set environment variables, test the purchase on devices |
Store agreements and product review take the longest; start them first.
In App Store Connect, sign the Paid Apps agreement, then create a subscription group with products such as pro_monthly and pro_yearly. In the Play Console, create the same product IDs as subscriptions with a base plan; Play needs the app in at least one testing track. The RevenueCat setup guide covers every field.
Add the App Store app and the Play Store app, import the products, attach them to an entitlement called pro and put them in an offering. Under Integrations → Webhooks, enter https://app.example.com/api/webhooks/revenuecat and an Authorization header value you generate yourself.
iOS: in Config.swift set kRevenueCatEnable to true and fill revenueCatAPIKey and revenueCatProjectID; leave kRevenueCatShowDefaultOfferingPaywall false unless you really want a paywall on every launch. Android: fill REVENUECAT_API_KEY and REVENUECAT_PROJECT_ID in Config.java. Use the public SDK key RevenueCat shows for each platform's app.
The plan is the valuable part here: it should list every place your app sells or mentions a price. Approve it, let the agent implement, then add the environment variables REVENUECAT_WEBHOOK_AUTH and your database credentials on the host and deploy.
TestFlight on iOS, internal testing on Android, with the checks below. If you prefer to hand the native builds and store setup to someone else, our Full Service team configures and builds both apps.

My iOS WebView is finally approved. Thanks!!
Replace the product IDs and route names. The last item creates a reusable slash command that you can run before every release.
Inside our iOS and Android apps (built with WebViewGold, detected by the token WebViewGoldApp in the user agent), digital purchases must go through the App Store and Google Play. WebViewGold integrates RevenueCat: navigating to revenuecat://purchase?external_id=USER_ID&product=PRODUCT_ID starts a native purchase, restoreinapppurchases:// restores purchases and cancelinapppurchase:// opens the store subscription settings. On iOS 15 or higher, revenuecat://launchPaywall?external_id=USER_ID&offering=OFFERING_ID shows the RevenueCat paywall; the docs list it for iOS only, so Android uses revenuecat://purchase per product. Read the In-App Purchases sections in https://www.webviewgold.com/llms-full.txt and use nothing that is not documented there. Our product IDs are pro_monthly and pro_yearly, the entitlement is pro. Plan first and wait for my approval. The plan must list every place where we sell, price or advertise something digital (Stripe checkout, pricing page, upgrade banners, emails with payment links) and how each one behaves inside the app. Then implement: 1. src/lib/native.ts: reuse isNativeApp() and add isIOSApp() based on the user agent. 2. Paywall actions: buyPro(productId) navigates to revenuecat://purchase with the signed-in user id (URL-encoded) as external_id. Never start a purchase for a signed-out user; send them to login first. In browsers keep the existing Stripe flow unchanged. 3. Inside the app, hide every Stripe button, web price and external payment link. Do not mention cheaper web prices. 4. The paywall shows plan name, duration, price per period, that the subscription renews automatically until cancelled, and links to /terms and /privacy. On iOS prefer launchPaywall, which shows the store prices. Where we render our own paywall, read the storefront with getstorelocation:// (window.storeLocation) and take prices from a per-storefront table in src/config/prices.ts. 5. A Restore purchases button (restoreinapppurchases://) and a Manage subscription link (cancelinapppurchase://) in settings and on the paywall, only inside the app. 6. POST /api/webhooks/revenuecat: compare the Authorization header with REVENUECAT_WEBHOOK_AUTH, read event.type, event.app_user_id, event.product_id, event.expiration_at_ms, event.environment and event.store, and upsert an entitlements row (user_id, entitlement, product_id, store, expires_at, will_renew, sandbox). CANCELLATION only means auto-renew is off; access lasts until expires_at. Add the migration. 7. GET /api/me/entitlement returns whether the user has pro right now. After a purchase starts, poll it every 4 seconds for up to a minute and again on visibilitychange, then unlock the UI. The webhook is the only source of truth; never unlock from client-side state. 8. Create .claude/commands/iap-check.md: a slash command that searches components rendered inside the app for Stripe calls, checkout links and hard-coded prices, and reports anything that would conflict with Apple guideline 3.1.1. Finish with the environment variables, the RevenueCat dashboard settings I must enter and a test plan.
RevenueCat sends the Authorization header value you entered in its dashboard with every event. app_user_id is the external_id your button passed, which is why it must be your stable user ID.
// app/api/webhooks/revenuecat/route.ts
import { NextResponse } from "next/server";
import { db } from "@/lib/server";
const GRANTS = new Set(["INITIAL_PURCHASE", "RENEWAL", "PRODUCT_CHANGE", "UNCANCELLATION", "NON_RENEWING_PURCHASE"]);
export async function POST(req: Request) {
// Same value you typed into RevenueCat -> Integrations -> Webhooks -> Authorization header
if (req.headers.get("authorization") !== process.env.REVENUECAT_WEBHOOK_AUTH) {
return NextResponse.json({ error: "unauthorized" }, { status: 401 });
}
const { event } = await req.json();
const userId: string | undefined = event?.app_user_id;
if (!userId || userId.startsWith("$RCAnonymousID")) {
return NextResponse.json({ ok: true, skipped: "no user id" }); // purchase without external_id
}
const base = {
userId,
entitlement: "pro",
productId: event.product_id,
store: event.store, // APP_STORE or PLAY_STORE
sandbox: event.environment === "SANDBOX",
};
const expiresAt = event.expiration_at_ms ? new Date(event.expiration_at_ms) : null;
if (GRANTS.has(event.type)) {
await db.entitlements.upsert({ ...base, expiresAt, willRenew: true });
} else if (event.type === "CANCELLATION" || event.type === "BILLING_ISSUE") {
// Auto-renew is off or payment failed: keep access until the store says it expired
await db.entitlements.upsert({ ...base, expiresAt, willRenew: false });
} else if (event.type === "EXPIRATION") {
await db.entitlements.upsert({ ...base, expiresAt: new Date(), willRenew: false });
}
// TRANSFER events carry transferred_from / transferred_to instead; handle them if you allow account switching
return NextResponse.json({ ok: true });
}external_id was empty.entitlements table has a row with sandbox = true, and the app unlocks pro within a few seconds without a manual reload./iap-check in Claude Code: no Stripe button, checkout link or web price is reachable inside the app.expires_at has passed.deletecache (iOS) or CLEAR_CACHE_ON_STARTUP (Android) set to true, users are signed out on every launch and your button has no user ID to send.No. WebViewGold also has a manual API (inapppurchase:// and inappsubscription:// with success and expiry URLs, plus injected planID, transactionIdentifier and subreceipts variables). RevenueCat is the default because it gives you one entitlement and one webhook for both stores.
Yes. Browser visitors keep the Stripe checkout. Both sources write the same entitlements table, so a user who paid on the web is also pro in the app, as long as the app does not advertise the web purchase.
Apple's current guidelines allow buttons and links to other payment methods for apps on the United States storefront, and Google changed its US policy after the Epic ruling. Rules differ by region and keep changing, so read the current guideline and policy text before you build on them.
No. The public SDK keys in Config.swift and Config.java are designed to ship in the app. The webhook Authorization value and database keys live in your host's environment variables; give the agent only their names.
The docs do not define a success callback for revenuecat://purchase, so treat the webhook as the source of truth: the app polls your entitlement endpoint briefly after starting a purchase and unlocks as soon as the row appears.
Same result, different amount of work on your side. All three come with the approval guarantee and 24/7 human support.
$109 per platform, one-time
The WebViewGold license: a finished Xcode or Android Studio project. Paste your URL, set name and icon, switch on features, build and submit. Free updates forever.
See plans on the main page$349 per license, one-time add-on
No Mac, no Xcode, no Android Studio: configure in the browser, we build both apps in the cloud and hand them to App Store Connect and the Play Console.
How the Cloud Builder works Try the free demo$698 per platform $798
Send us your URL and icon. Our team configures, builds and delivers your app within 72 hours, white-label, with source projects. Both platforms for $1,158.
Full Service details Powered byWinter Sale prices valid until December 31, 2026.
App Store & Google Play approval guarantee – or your money back within 14 days
Rather hand it over? Full Service: we configure, build and deliver within 72 hours, from $698 per platform. Questions? Chat with us 24/7.
Refunds are granted under the conditions listed in our FAQ: a non-content rejection by Apple and/or Google, claimed within 14 days after purchase.