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 pageChatGPT & Codex
Payments are where an agent can cost you a rejection or real money, so treat this one as a cloud task with a strict review. Codex finds every place you sell digital goods, routes them through RevenueCat when the page runs inside the app, and writes the webhook that grants access on your server. You keep the products, the keys and the merge button.
From $109 one-time per platform, or let our team do it all from $698.
The goal: inside the iOS and Android apps, every digital product (subscriptions, credits, premium features, unlockable content) is bought through the store; in browsers, Stripe keeps working; and both paths write the same entitlement record on your server.
The reviewer checks three things. A buy button that opens a card form or an external checkout for digital goods is a Guideline 3.1.1 rejection. A subscription paywall without price, billing period, renewal terms and links to terms and privacy is a 3.1.2 rejection. A missing Restore Purchases control gets flagged as well. Physical goods and real-world services are a different category and may keep Stripe inside the app (how to tell them apart).
Rules for linking out to external purchases have shifted in some regions, for example on the US storefront after the 2025 Epic ruling and in the EU under the DMA. If you want to use those options, read the current guideline text in App Store Connect first and keep them out of your first submission. The rest of this page assumes the standard path, which is also what the Codex release workflow recommends. Every purchase option WebViewGold offers is summarized on the in-app purchases hub.
| Situation | What the web app does | Notes |
|---|---|---|
| Browser user buys | Existing Stripe Checkout, unchanged | Stripe webhook writes the entitlement |
| App user buys a product | revenuecat://purchase?external_id=UID&product=PRODUCT_ID | iOS and Android; UID is your stable user ID, PRODUCT_ID as shown in RevenueCat |
| App user opens a paywall | iOS: revenuecat://launchPaywall?external_id=UID&offering=OFFERING (iOS 15 or newer). Android: your own paywall page that calls purchase | The docs describe launchPaywall for iOS only |
| Restore | restoreinapppurchases:// | A visible control, expected for subscriptions and non-consumables |
| Manage or cancel | cancelinapppurchase:// | Opens the store's subscription screen |
| Show past purchases | getpurchasehistory:// fills window.purchaseHistory | Per device; field names differ between iOS and Android |
| Grant access | Nothing on the client | The RevenueCat webhook to Express writes the entitlement |

WebViewGold has allowed us the shortest and most efficient time-to-market ever possible from among all the other app platforms we have ever tried during the past 5 years. I don’t know why I hadn’t discovered you sooner, but I am really, really glad I did. Currently nearing 6000 app downloads on both iOS App Store & Google Play Store without any problem!
Codex combines AGENTS.md files from the repository root down to the folder it is working in, and the file closest to the code wins. A small server/billing/AGENTS.md keeps the payment rules next to the payment code, on top of your root AGENTS.md, and its Review guidelines section tells Codex code review what to flag on every pull request that touches billing. Codex review on GitHub reports P0 and P1 findings, so the rules use those levels.
# server/billing/AGENTS.md
## Invariants
- Entitlements live in Firestore at entitlements/{uid} and are written ONLY by
webhooks: /webhooks/stripe (source "stripe") and /webhooks/revenuecat
(source "revenuecat"). No client code, callable function or admin page sets premium.
- Every access check reads entitlements/{uid} on the server. Never trust a flag,
cookie or localStorage value from the device.
- The RevenueCat app user ID is the Firebase uid, passed as external_id in
revenuecat://purchase?external_id=...&product=... (never the email address).
- Webhooks: compare the Authorization header with REVENUECAT_WEBHOOK_AUTH, dedupe by
event.id in billingEvents/{eventId}, answer 200 quickly. Tests use fixtures in
server/billing/__fixtures__, never live RevenueCat or Stripe APIs.
- Inside the native app (isNativeApp()), digital goods use the bridge only:
revenuecat://purchase, revenuecat://launchPaywall (iOS only),
restoreinapppurchases://, cancelinapppurchase://.
No Stripe links, no card forms, no "cheaper on the web" copy.
- Product IDs come from src/config/products.ts, never hardcoded in components.
## Review guidelines
- P0: any code path that grants or extends premium outside the two webhooks.
- P0: a webhook handler that does not check the Authorization header.
- P1: a Stripe checkout, price link or external payment URL for a digital product
that is reachable when isNativeApp() is true.
- P1: a paywall without price, billing period, renewal terms, Terms and Privacy
links, or without a Restore Purchases control.Run this as a cloud task so the result arrives as a diff you can turn into a pull request, not as edits in your working copy. Start it from the Codex page in ChatGPT, from the IDE extension, or with codex cloud in the terminal. The environment needs the repository and a setup script that installs dependencies (for example npm ci). It does not need network access to RevenueCat or Stripe, and it should never hold production keys.
Read AGENTS.md and server/billing/AGENTS.md. Goal: inside the native iOS and Android apps (isNativeApp() is true), every digital product is sold through in-app purchases via the WebViewGold RevenueCat bridge. Browsers keep Stripe. Stack: React front end, Express API, Firebase Auth, Firestore.
1. Inventory: list every place where a subscription, credit pack or premium feature is sold or advertised with a price (components, routes, emails or banners that link to checkout). Put the list in the pull request description, with the file for each.
2. Client: create src/lib/purchase.ts with buy(productKey), openPaywall(), restorePurchases() and manageSubscription(). In the app, buy navigates to revenuecat://purchase?external_id=UID&product=PRODUCT_ID, with UID = the Firebase uid (URL-encoded) and PRODUCT_ID from src/config/products.ts. restorePurchases navigates to restoreinapppurchases://, manageSubscription to cancelinapppurchase://. In browsers, buy keeps the existing Stripe Checkout.
3. Paywall: on iOS, openPaywall may use revenuecat://launchPaywall?external_id=UID&offering=default (documented for iOS 15 or newer only). Keep our own paywall component for Android and as the fallback, behind a flag in src/config/products.ts. Our paywall shows price, billing period, renewal terms, and links to /terms and /privacy.
4. In the app, remove Stripe buttons, web pricing links and "cheaper on the web" copy for digital goods. Physical goods and services stay on Stripe.
5. Server: POST /webhooks/revenuecat in server/billing/. Return 401 unless the Authorization header equals REVENUECAT_WEBHOOK_AUTH. Dedupe by event.id in billingEvents. Write entitlements/{uid} with premium, source, store, productId, expiresAt and sandbox. The Stripe webhook writes the same document with source "stripe". Every premium check on the server reads only this document.
6. After the native purchase sheet closes, the client grants nothing. It polls GET /api/entitlements for up to 20 seconds and shows "Processing your purchase" meanwhile.
7. Settings: Restore purchases and Manage subscription rows, native app only.
8. Tests with fixture events (INITIAL_PURCHASE, RENEWAL, CANCELLATION, EXPIRATION, TEST, a duplicate event ID) and one request with a wrong Authorization header. No calls to live RevenueCat or Stripe APIs.
9. Pull request description: the inventory, the environment variables I must set, the RevenueCat dashboard steps that remain, and a sandbox test plan for iOS and Android.Simplified, but with the parts that matter in review: authorization, idempotency, the user ID taken from app_user_id (which is your external_id), and no client involvement. Refunds, transfers and lifetime products need extra handling in a real app.
// server/billing/revenuecat.js (Express + firebase-admin)
import express from "express";
import { getFirestore, FieldValue } from "firebase-admin/firestore";
const db = getFirestore();
export const revenuecatRouter = express.Router();
const GRANT = new Set(["INITIAL_PURCHASE", "RENEWAL", "UNCANCELLATION",
"PRODUCT_CHANGE", "NON_RENEWING_PURCHASE", "SUBSCRIPTION_EXTENDED"]);
revenuecatRouter.post("/webhooks/revenuecat", express.json(), async (req, res) => {
if (req.get("Authorization") !== process.env.REVENUECAT_WEBHOOK_AUTH) {
return res.status(401).end();
}
const event = req.body && req.body.event;
if (!event || !event.id) return res.status(400).end();
if (event.type === "TEST") return res.status(200).end();
const seen = db.collection("billingEvents").doc(event.id);
if ((await seen.get()).exists) return res.status(200).end(); // duplicate delivery
const uid = event.app_user_id; // = external_id from revenuecat://purchase
if (uid) {
const expiresAt = event.expiration_at_ms ? new Date(event.expiration_at_ms) : null;
const ref = db.collection("entitlements").doc(uid);
if (GRANT.has(event.type)) {
await ref.set({ premium: true, source: "revenuecat", store: event.store,
productId: event.product_id, expiresAt, sandbox: event.environment === "SANDBOX",
updatedAt: FieldValue.serverTimestamp() }, { merge: true });
} else if (event.type === "EXPIRATION") {
await ref.set({ premium: false, expiresAt, updatedAt: FieldValue.serverTimestamp() }, { merge: true });
}
// CANCELLATION and BILLING_ISSUE: access usually runs until EXPIRATION.
}
await seen.set({ type: event.type, uid: uid || null, at: FieldValue.serverTimestamp() });
res.status(200).end();
});Read the inventory first: every sell point should be listed, and each one should end either in Stripe (browser) or in the bridge (app). Then comment @codex review for payment bypasses on the pull request for a focused second opinion that applies the nested review guidelines. Merge only when you can explain every path.
In RevenueCat, add a webhook pointing to https://api.example.com/webhooks/revenuecat and set an authorization header value; put the same value into REVENUECAT_WEBHOOK_AUTH on your server. Send RevenueCat's test event and confirm a 200 response.
iOS, Config.swift: kRevenueCatEnable true, revenueCatAPIKey, revenueCatProjectID; leave kRevenueCatShowDefaultOfferingPaywall false unless the paywall should open on every launch. Android, Config.java: REVENUECAT_API_KEY and REVENUECAT_PROJECT_ID. The bundle ID and package name must match the RevenueCat apps exactly (iOS IAP docs, Android IAP docs).
iOS: a TestFlight build and a sandbox Apple account. Android: license testers and an internal testing track. Buy, restore on a second device, let a sandbox subscription expire, and watch the entitlement change only when the webhook arrives.
Attach the first in-app purchases to the app version in App Store Connect, and tell the reviewer where the paywall is and which demo account to use (review notes template).
revenuecat:// link is reachable.external_id. Emails change, and the RevenueCat customer splits. Use the uid.get-uuid:// and merge deliberately.deletecache on iOS, CLEAR_CACHE_ON_STARTUP on Android). The WebViewGold docs recommend keeping these off when the purchase flow depends on the session.window.purchaseHistory looks the same everywhere: iOS returns purchaseDateMs as a string, Android purchaseTime and orderId. Let Codex write one normalizer.No. A paywall page in your web app that calls revenuecat://purchase works on both platforms, as long as it shows price, period, renewal terms and the legal links. On iOS you can also open a paywall designed in RevenueCat with revenuecat://launchPaywall (iOS 15 or newer).
No. The task writes code and tests against fixtures. Keys belong in your server's environment, set by you after the merge. Keeping production secrets out of agent environments is the cheapest risk reduction you have.
Usually yes. Apple's multiplatform rule (Guideline 3.1.3(b)) lets users access what they bought on your website, provided the same items are also available as in-app purchases in the app. Because both webhooks write the same entitlement, a Stripe subscriber who logs in is premium in the app too.
The standard commission is 30 percent. Apple's Small Business Program and Google's reduced tier for the first one million dollars of yearly earnings lower it to 15 percent, and RevenueCat charges a share of revenue above its free threshold.
Yes. The WebViewGold in-app purchase documentation requires the Extended License for using in-app purchases in an end product. Upgrade before you submit the build that contains purchases.
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.