Skip to content
WebViewGold Vibe Coding to App Store
English
Get WebViewGold

ChatGPT & Codex

In-app purchases for a Codex-built web app: RevenueCat, WebViewGold and a pull request you can trust

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.

  • Approval guarantee – or money back within 14 days
  • No coding needed
  • One-time payment, free updates
  • 24/7 human support
  • Made in Germany since 2014 · ISO 27001

Goal and reviewer angle

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.

Every money path, mapped

SituationWhat the web app doesNotes
Browser user buysExisting Stripe Checkout, unchangedStripe webhook writes the entitlement
App user buys a productrevenuecat://purchase?external_id=UID&product=PRODUCT_IDiOS and Android; UID is your stable user ID, PRODUCT_ID as shown in RevenueCat
App user opens a paywalliOS: revenuecat://launchPaywall?external_id=UID&offering=OFFERING (iOS 15 or newer). Android: your own paywall page that calls purchaseThe docs describe launchPaywall for iOS only
Restorerestoreinapppurchases://A visible control, expected for subscriptions and non-consumables
Manage or cancelcancelinapppurchase://Opens the store's subscription screen
Show past purchasesgetpurchasehistory:// fills window.purchaseHistoryPer device; field names differ between iOS and Android
Grant accessNothing on the clientThe 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!

Antonio Davila · CodeCanyon

A nested AGENTS.md for the billing code

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.

markdown
# 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.

The cloud task

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.

Paste into a Codex cloud task (or the CLI)
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.

The webhook you should find in the pull request

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.

javascript
// 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();
});

From pull request to a sandbox purchase

  1. Review the pull request like an auditor

    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.

  2. Configure the webhook

    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.

  3. Set the native keys

    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).

  4. Test in the sandbox

    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.

  5. Submit with the products attached

    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).

Test checklist

  • Desktop browser: Stripe works exactly as before, and no revenuecat:// link is reachable.
  • App: no screen shows a Stripe button, card form or web pricing link for digital goods.
  • A purchase on iOS and on Android unlocks premium within seconds, and only after the webhook wrote the entitlement.
  • Killing the app while the purchase sheet is open does not leave a half state; the webhook still grants access.
  • Restore on a second device (same store account, same login) brings premium back.
  • Manage subscription opens the store screen; a cancelled sandbox subscription stays active until it expires.
  • The paywall shows price, period, renewal terms and links to Terms and Privacy.
  • A webhook request with a wrong Authorization header gets a 401.

Pitfalls

  • Using the email address as external_id. Emails change, and the RevenueCat customer splits. Use the uid.
  • Selling before login. Without a stable ID, the purchase lands on a customer you cannot match later. Ask for login first, or use a device ID from get-uuid:// and merge deliberately.
  • Granting premium in the client when the purchase sheet closes. That is a free unlock for anyone who edits a request; poll your server instead.
  • Trusting event order. Webhooks can arrive late or twice. Dedupe by event ID; for more robust state, have the handler re-read the customer from RevenueCat's REST API on each event instead of interpreting every event type.
  • Clearing cookies at every start (deletecache on iOS, CLEAR_CACHE_ON_STARTUP on Android). The WebViewGold docs recommend keeping these off when the purchase flow depends on the session.
  • Assuming window.purchaseHistory looks the same everywhere: iOS returns purchaseDateMs as a string, Android purchaseTime and orderId. Let Codex write one normalizer.

Frequently asked questions

Does the paywall need to be native?

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).

Should the cloud task get my RevenueCat or Stripe keys?

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.

Can web subscribers use premium in the app?

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.

How much do Apple and Google keep?

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.

Do I need the WebViewGold Extended License?

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.

Three ways to get your app into the stores

Same result, different amount of work on your side. All three come with the approval guarantee and 24/7 human support.

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 page

Cloud Builder

$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

Full Service

$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 by WeSetupYourWebViewApp

Winter Sale prices valid until December 31, 2026.

Ready to ship your app?

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.