How to Get Base44 In-App Purchases Approved
Get your Base44 app approved with real in-app purchases. StoreKit and Play Billing through RevenueCat, and how to keep your web subscribers.

Search for a command to run...
Get your Base44 app approved with real in-app purchases. StoreKit and Play Billing through RevenueCat, and how to keep your web subscribers.

No comments yet. Be the first to comment.
window.despia is undefined in your app because it is not a public API. Here is what the Despia SDK exposes and how to call native features correctly.

Apple is auto-rejecting Base44 apps over placeholder purpose strings. What the Guideline 5.1.1 check looks at, what to clear, and how to resubmit.

Apple is auto-rejecting Lovable apps over purpose strings. Where Info.plist lives on each shipping route, what to clear, and how to resubmit.

Apple now auto-rejects apps over placeholder purpose strings. What triggers the Guideline 5.1.1 check, why WebView apps get caught, and how to fix it.

Despia Blog: Build Native iOS & Android Apps from Web Code
110 posts
To get approved selling digital content, your Base44 app needs three things: purchases running through StoreKit on iOS and Google Play Billing on Android, a paywall the reviewer can actually reach, and a working restore. Base44 Payments is a web checkout processed through Wix, so it has no native billing path. RevenueCat supplies one, compiled into the app binary, and your existing web subscribers keep their access under Guideline 3.1.3(b) at the same time.
This is the whole path: what approval requires, how to build it, how to keep the customers you already have, and what to check before you submit.
https://www.youtube.com/watch?v=C9WQ2VV6rB4
| Requirement | Why it is on the list |
|---|---|
| Digital goods sold through StoreKit and Play Billing | Guideline 3.1.1, the core rule |
| A restore purchases option that works | Its own rejection when missing |
| A paywall the reviewer can reach on a clean install | Correct billing still fails if it is unreachable |
| Demo credentials in App Review Information | The usual reason a reviewer cannot get there |
| Products submitted with the app version | Configured is not the same as submitted |
| Price and billing period shown on the paywall | Required on any subscription screen |
| Terms and privacy links on the paywall | Same |
| Discounts as Apple offer codes, not your own promo codes | Your own codes are an external unlock |
| Existing web subscribers honoured, with in-app purchase also offered | The 3.1.3(b) condition |
| No pricing that steers iOS users to your website | Prohibited outside the US storefront |
Everything below is how to satisfy each line.
Apple splits payments by what is being sold, not by who processes them.
| What you sell | Payment method required on iOS |
|---|---|
| Subscriptions to digital content | In-App Purchase (StoreKit) |
| Premium features or a full version unlock | In-App Purchase (StoreKit) |
| Credits, tokens, in-game currency | In-App Purchase (StoreKit) |
| Courses, lessons, digital downloads | In-App Purchase (StoreKit) |
| Physical products | Any processor |
| Real-world services (delivery, booking, transport) | Any processor |
| Sales direct to organisations | Narrow carve-out, see 3.1.3(c) |
Guideline 3.1.1 is explicit that if money unlocks features or functionality inside your app, you must use In-App Purchase, and that apps may not use their own mechanisms to unlock content. Google applies the equivalent rule through Play Billing.
Base44 Payments is Base44's built-in checkout, processed through Wix and subject to the Wix Payments terms, with Stripe available alongside. Fees land around 3%, well below store commission, and on the web it works exactly as designed. What it does not have is a StoreKit path or a Play Billing path, because those are native frameworks that compile into an app binary and a hosted web checkout has nowhere to put them. Base44's own store submission documentation says a built-in integration is in progress, which is the company confirming the gap itself.
So the trade is roughly 3% against 15% on your first million dollars of annual proceeds and 30% above it. The cheaper route is the one that does not ship.
The two rejection notices this prevents are worth recognising, because they mean opposite things. Selling in-app through a non-Apple processor gets you the standard message about accessing paid digital content by means other than in-app purchase. Removing the purchase flow and leaving only login gets you a different one, saying your app accesses digital content purchased outside the app and that content is not available to purchase using in-app purchase. The second is not asking you to remove more. It is asking you to add the in-app option alongside what you have.
This is permitted, and it is written into the guidelines rather than being a workaround.
Guideline 3.1.3(b), Multiplatform Services, permits apps operating across multiple platforms to let users access content, subscriptions, or features they acquired in your app on other platforms or on your website, provided those same items are also available as in-app purchases within the app.
The condition is the whole thing. Honour the web subscription, and also sell the same tier through StoreKit.
| Setup | Review outcome |
|---|---|
| Web subscription honoured, StoreKit paywall also offered | Approved |
| Web subscription honoured, no in-app purchase option | Rejected under 3.1.1 |
| Login-only companion app, no purchase UI at all | Usually rejected |
| Digital goods sold in-app through Base44 Payments | Rejected under 3.1.1 |
The third row surprises people. Teams shipping companion apps with no sign-up, no pricing screens, and no purchase UI still report repeated rejections, because access to paid digital content triggers the rule rather than the presence of a buy button. Guideline 3.1.3(c) carves out services sold directly to organisations for their employees or students, but consumer, single user, and family sales must use in-app purchase.
Four stages. The code is the fastest of them.
At app.revenuecat.com, create a project, then add your apps.
For iOS you need your bundle ID, an App Store Connect API key with App Manager access, and an in-app purchase key. For Android you need the same package name as your build plus a Google Play service account JSON with permissions granted in Play Console. Permissions can take time to propagate, so do this before you need it.
Then create your products in App Store Connect and Play Console, import them into RevenueCat, attach each to an entitlement, group them into packages, and put those packages in an offering. Mark one offering as default. An offering with no packages returns an empty list rather than an error, which is a confusing hour if you skip it.
AI builders invent Base44 native APIs constantly. The most common invention is window.despia as an environment check, which does not exist, so the condition is false on every device and every native call inside it is skipped in silence. Nothing errors. The feature is just dead, and you spend four builds debugging a runtime that is fine.
The Despia MCP server is a documentation server that hands your builder the real reference for the despia-native package. In Base44: profile icon, Settings, MCP connections under Account, Add custom MCP, name it Despia, URL https://setup.despia.com/mcp, authentication not required. It needs a Builder plan or higher.
Do not describe the integration to your agent from memory, and do not let it write one from its own idea of how RevenueCat works. Point it at the guide and have it implement what is actually there.
Read this page and implement it in this app exactly as written:
https://blog.despia.com/base44-in-app-purchases-without-webhooks-revenuecat.md
Rules while you implement it:
- This app ships to iOS and Android with Despia. Native features come from the
despia-native npm package.
- Detect the native runtime with the user agent. Never use window.despia, it
does not exist and the check will be false on every device.
- Assign global callbacks at module scope, outside the environment check, so
they exist before the native side fires them.
- Do not add Capacitor, Cordova, or any second native project.
- Follow the code in the guide. Do not substitute your own RevenueCat pattern,
and do not add webhooks or a subscriptions table.
One addition that is not in the guide:
- Before granting access, check the user's existing subscription record first,
and only ask the store if that returns nothing. A customer who already
subscribed on our website must never be shown a paywall.
When you are done, list every value I have to supply myself, and tell me which
files you changed.
The .md on the end of that URL matters. It serves the guide as plain markdown, which is what you want an agent reading instead of a rendered page full of navigation. If your agent cannot fetch URLs, open that link yourself and paste the page into the chat before the instructions above.
Asking which files changed gives you something to check, because an agent reporting success and an agent having written the call are not the same event.
In the Despia Editor open App, Integrations, RevenueCat, paste the iOS and Android public SDK keys, and trigger a new build.
The RevenueCat SDK compiles into the binary, so it cannot arrive over the air the way your Base44 web code does. Until you rebuild and install that build, the integration stays dormant even with the keys saved. Purchases fail quietly and entitlement checks return nothing while every dashboard screen looks correct. This is the single largest source of support tickets on this integration.
Keep the entitlement concept singular. A user is premium or not. Where the money came from belongs in one function, not scattered through your UI.
This function decides what the interface shows. It is not security, and the next section is.
import despia from 'despia-native'
const isDespia = navigator.userAgent.toLowerCase().includes('despia')
// display only: what screens to show, never what work to run
async function resolveAccess(user) {
// web subscribers keep their access on every platform
if (user.subscription_status === 'active') return true
// in the app, ask the store what this user actually owns
if (isDespia) {
const { restoredData } = await despia('getpurchasehistory://', ['restoredData'])
return (restoredData ?? []).some(p => p.entitlementId === 'premium' && p.isActive)
}
return false
}
// outside the gate, so it exists before native fires it
window.onRevenueCatPurchase = () => resolveAccess(currentUser).then(setPremium)
Two things about this matter more than the code.
It runs in both environments. On the web the store branch never executes and your Base44 record decides. In the app both are consulted. One function, one answer.
Order matters for the user, not the logic. Checking the existing subscription first means a paying web customer opening the app never sees a paywall flash before access resolves. That flash is what produces the review saying they were charged twice.
One detail that quietly creates support work: pass your Base44 user id to RevenueCat as the app user id. If RevenueCat generates anonymous ids instead, a user who subscribes in the app and later signs in on the web becomes two customers, and you merge them by hand.
The function above reflects what the client claims. That is fine for hiding a screen and useless for protecting anything that costs you money, because a client can be modified. Anyone who opens the developer tools can flip a boolean and start calling your paid endpoints.
So every premium action with a real cost behind it, an AI generation, a paid export, a third-party API you are billed for, gets checked when the request arrives. In a Base44 backend function, ask RevenueCat directly.
// runs server-side, secret key never ships to the client
async function hasActiveEntitlement(appUserId, entitlementId) {
const id = encodeURIComponent(appUserId) // anon ids carry a $RCAnonymousID: prefix
const res = await fetch(
`https://api.revenuecat.com/v2/projects/${PROJECT_ID}/customers/${id}/active_entitlements`,
{ headers: { Authorization: `Bearer ${RC_SECRET_KEY}` } }
)
if (res.status === 404) return false // customer RevenueCat has never seen
if (!res.ok) throw new Error(`RevenueCat ${res.status}`)
const { items = [] } = await res.json()
return items.some(e => e.entitlement_id === entitlementId)
}
async function runExpensiveThing(req) {
// web subscriber, or a real store entitlement, and nothing else
const entitled =
(await getSubscriptionRecord(req.userId))?.status === 'active' ||
(await hasActiveEntitlement(req.userId, 'premium'))
if (!entitled) return { status: 403 }
return doTheWork(req)
}
Four practicals worth getting right. The secret key stays server-side and never reaches your web code. The appUserId here must be the same external_id you passed when launching the paywall, or RevenueCat resolves a different customer and correctly reports no entitlement. URL-encode the id, because anonymous ids carry a prefix with a colon in it. And in sandbox this endpoint can read empty while the entitlement is genuinely live, so cross-check against RevenueCat's older subscriber lookup while testing rather than assuming your integration is broken.
Verify the active-entitlements route against RevenueCat's current documentation before you ship, since the API differs across endpoints and versions.
This is where teams who did everything else right still get bounced, with a notice saying the subscription is not discoverable in review.
The reviewer has to reach a working purchase screen from a fresh install, with no context and no account. Four things stop them.
Sign-in blocks the paywall. If your paywall lives behind login, supply working credentials in the App Review Information section, and check them on a device that has never opened your app.
Onboarding breaks before the paywall. A quiz or setup flow that stalls on a slow network or an unexpected input leaves the reviewer stuck upstream of the thing they are meant to test. Test the whole path on cellular from a clean install.
The paywall is region-gated. Review commonly runs from outside your main market. If your offering only returns products in some regions, they see an empty screen.
Offerings fail to load for the reviewer specifically. This happens, which is the reason to render a real error state rather than an empty view. A blank screen reads as a broken app.
Say where the paywall is in your review notes. One sentence naming the screen and how to reach it removes the entire category.
A common Base44 pattern that fails review: a field where users enter a code to unlock premium.
Apple's notice names it directly, saying the app unlocks or enables additional functionality with mechanisms other than in-app purchase, specifically that the app uses promo codes to unlock digital content. The instruction is to use an Apple-supported offer code instead.
So implement discounts as offer codes and promotional offers inside App Store Connect, attached to your subscription. Your own code redemption stays on the web where it always worked.
| In-app element | Allowed? |
|---|---|
| Sign-in for existing web subscribers | Yes, and it should be easy to find |
| Restore purchases | Yes, and a missing one is its own rejection |
| StoreKit paywall at your chosen price | Yes |
| Managing an existing subscription | Yes, without pricing or purchase calls to action |
| "Subscribe on our website and save" | No, outside the US storefront |
| Instructions to contact support to upgrade | No, same violation with extra steps |
| A link to your web checkout | US storefront only, and gated |
Apple does not require price parity across platforms. If your web tier is 9.99 and the store takes 15%, pricing the in-app tier at 12.99 is permitted, and many large apps do exactly that.
What you cannot do is explain why inside the app. The price is the price on each platform, and users who care will find the cheaper one without your help.
Partly, in one country, and it does not remove the need for native billing.
Since the 2025 Epic v. Apple injunction, apps on the United States storefront may include buttons and external links pointing at outside checkout, with no entitlement required. The 27% external-purchase commission was struck down and no replacement rate has been set, so US external purchases currently carry no Apple commission. Google's US policy moved the same way after Epic v. Google, with a revised settlement still working through the courts.
Four things keep this from being the approval route:
It applies to the United States storefront only. Everywhere else the old restrictions stand, so a global build needs storefront gating and two purchase paths regardless.
You still cannot process the payment inside the app. The allowance covers linking out to a browser.
A browser handoff converts worse than a native purchase sheet with Face ID and a stored card. That gap is measurable and often larger than the commission.
The fee is zero because a court has not set a rate, not because a rate was decided.
Rules current as of July 2026 and actively moving. Check Apple's and Google's own policy pages before building around them.
You cannot migrate them automatically. There is no mechanism to move a web subscription into StoreKit on a user's behalf. A subscription is a contract with a processor, and switching means the user cancels one and starts the other themselves.
You should not push them to switch. Web subscribers at roughly 3% are your best-margin customers. Moving them to a store tier costs you money and gains nothing, because 3.1.3(b) already lets them use the app.
Cancel-and-rebuy creates refund work. If you prompt someone to move, you own the overlap, the refund request, and the confusion while both are briefly active.
No, and this is worth answering directly because the community threads on it mostly end in someone giving up. The recurring conclusion is that Base44 cannot do store subscriptions, therefore the app must be rebuilt elsewhere, and more than one team has paid an agency to port everything to Flutter or React Native over exactly this.
That is an expensive answer to a solvable problem.
The gap is real and narrow. Base44 does not currently ship StoreKit or Play Billing, and RevenueCat's own support has stated that Base44 is not a supported no-code builder on their side, so no first-party instructions exist. Both facts are accurate. Neither means what people take them to mean. What is missing is not your app. It is the native layer beneath it.
A rebuild replaces your entire product to acquire one capability. A native runtime adds the capability and leaves the product alone: your Base44 app, database, auth, screens, and existing subscribers all stay, and the RevenueCat SDK compiles into the binary underneath.
Run these on a physical device, from a clean install, on cellular.
Buy the subscription in sandbox and confirm premium unlocks.
Delete the app, reinstall, sign in, and confirm restore returns your entitlement.
Sign in as an existing web subscriber and confirm no paywall appears.
Open the paywall with the demo account you are giving Apple.
Confirm your in-app purchase products are attached to the version you are submitting, not merely created in App Store Connect.
Check the paywall shows price, billing period, terms, privacy, and restore.
Write one line in the review notes saying where the paywall is.
Products have to be submitted together with the app version. Creating them is not enough, and this is the most common reason a technically correct billing setup still gets bounced.
| What you see | Most likely cause |
|---|---|
| Entitlement check empty, keys are saved | No rebuild since the keys were added |
| Paywall button does nothing, no error | Environment check false, so the call never ran |
window.despia is not a function |
That API does not exist, use the user agent check |
Error fetching offerings, code 23 |
Store cannot return the products yet |
| Offerings load but contain no packages | Products not attached to an offering |
| Purchase completes, access does not unlock | Callback assigned inside the gate instead of at module scope |
| Existing web subscriber sees a paywall | Access check only reads the store |
Error code 23, CONFIGURATION_ERROR, says that none of the products registered in the RevenueCat dashboard could be fetched from App Store Connect or the Play Store. RevenueCat is relaying a refusal rather than failing. Work these in order: the Paid Applications Agreement must be signed and active in App Store Connect, products must not be sitting in READY_TO_SUBMIT, tax and banking must be complete, the bundle ID must match across Despia, App Store Connect and RevenueCat, and product identifiers must match exactly including case. On Android, the app must have had an active release on a testing track, which is the most common Android-only cause.
Debug on a real device rather than a simulator, and remember there is no console in TestFlight. Render the user agent, whether the environment check passed, and the raw entitlement response into a debug panel you can toggle.
Yes, on the web, and for physical goods or real-world services anywhere. What you cannot do is sell digital content through it inside the iOS or Android app. Most approved apps run both: Base44 Payments on the web, StoreKit and Play Billing in the app.
Not if you build the two-source access check. Guideline 3.1.3(b) exists so they do not, on condition that the same subscription is also purchasable in-app.
Not natively on either platform today. Base44's documentation states a StoreKit and Play Billing integration is in progress. Android has the same gap as iOS, so one native runtime solves both.
No. RevenueCat support has said the same from their side, that Base44 is not a supported no-code builder and they have no instructions for it. It works regardless, because the integration point is the native binary rather than either platform.
No. That conclusion is common in community threads and it is the most expensive available fix. The missing piece is native billing, not your application.
Yes, for auto-renewable subscriptions. A subscription group holds tiers and durations of the same service, a user can hold only one active subscription per group, and an introductory offer or free trial can be redeemed only once within a group. Putting monthly and annual in separate groups lets one person subscribe to both and claim two free trials.
Yes, through an introductory offer on the subscription in App Store Connect. It is configured on the store side rather than in your code, and RevenueCat reports the trial state back through the entitlement. Play has an equivalent.
Yes, as a non-consumable in-app purchase. The same rules apply: sold through StoreKit, with a working restore path, because a user reinstalling on a new device has to get it back.
No. Restoring entitlements on the device covers what the app displays, and one direct API call covers anything the server has to protect. A webhook plus a subscription table is a second source of truth you then maintain forever. Add one later if you need real-time cancellation handling.
Your Base44 app runs as a real iOS and Android binary through a native runtime, with no separate native project and no second codebase. That is the same layer that carries StoreKit and Play Billing, which is why the purchase question and the conversion question have one answer.
The code is an afternoon. The slow parts are the RevenueCat project, store product configuration, and the build. Budget a day, and expect the store side to take longer than the integration.
Shipping your first app is stressful, and an AI builder makes it worse in a specific way: it does not tell you when it is unsure. It tells you what you asked to hear, confidently, and you find out four builds later.
If you are stuck on something with Despia and Base44 and you want a human to look at it, email humans@despia.com. A developer with five or more years of experience reads it and writes back about your actual setup, not a template.
Worth knowing what you are asking for. A person will sometimes tell you your approach is wrong, that the roadblock is upstream of the thing you are debugging, or that the feature you want is not the one that gets you approved. That is the point of asking a human.
Take the Base44 app you already built and ship it to iOS and Android without a CLI or a Mac. StoreKit and Play Billing arrive through RevenueCat with one JavaScript call, your web subscribers keep the access they paid for, and your Base44 app stays the single source of truth.