> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tryhelium.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe Onboarding Guide

export const preset_0 = "Stripe A2W Eligible"

export const processor_0 = "Stripe"

This guide walks you through integrating a Stripe "app2web" flow with Helium.

## What You'll Accomplish

By the end of this guide, you'll have:

* A Stripe account connected to Helium, with products synced and ready to add to paywalls.
* Paywalls that offer Stripe products.
* A hosted web checkout flow that lets users purchase in their browser and deep-links them back into your app.
* Entitlement tracking across Stripe, the Helium SDK, and (optionally) RevenueCat.

## Prerequisites

Before you start, make sure you have:

* The Helium SDK already integrated in your app.
* A deep link scheme or universal link configured in your app — users need a way back into your app after purchase.
* Admin access to your Helium dashboard.

## Implementation Steps

### 1. Connect Stripe to Helium

Go to the Helium [Integrations](https://app.tryhelium.com/integrations/products) page, click the Stripe tab, and **Connect Stripe**.

Create a new account or connect an existing Stripe account.

Sync your products and ensure the APPLE PAY DOMAIN status is Verified.

<Info>
  Helium does not yet support Stripe sandbox.
</Info>

### 2. Adjust Stripe Products

Assign Stripe products to appropriate buckets. Add free/paid trials as needed.

### 3. Build your paywalls

For a short walkthrough, see [Build an App2Web paywall](/guides/build-app2web-paywall).

Stripe checkout requires **two paywalls**:

1. **The original paywall**, shown in-app, containing your Stripe products (we recommend adding iOS products too as a backup if user decides to not use Stripe).
2. **A hosted web paywall**, which runs the actual Stripe checkout in the user's browser on `clickthrough.to`.

When the user taps a CTA on the original paywall, they're kicked out to the hosted web paywall, where they complete the purchase.

To set this up:

1. Create the original paywall and add Stripe products to it.
2. Create a hosted web paywall (the editor will prompt you).
3. Link the two. The original paywall can't be published without a linked hosted web paywall.

The hosted web paywall doesn't have to show the same products as the original — you can use it to customize the checkout experience (e.g., upsell a different plan at purchase time). Hosted web paywalls can't be added to workflows, experiments, or targeting rules; they exist only to back a Stripe paywall.

### 4. SDK integration

<Tabs>
  <Tab title="iOS (native, Helium 4.6.0+)">
    **Enable external web checkout before initializing Helium:**

    ```swift theme={null}
    Helium.config.enableExternalWebCheckout(
      redirectURL: "yourapp://openapp",
      paymentProcessors: .stripe
    )
    ```

    The `redirectURL` is set once, globally. Checkout redirects back to it whether the purchase succeeds, is canceled, or fails. Make sure it's registered as a deep link in your app (via scheme or universal link).

    <Note>
      On older SDK versions that don't have `redirectURL`, use the equivalent two-URL form with the same URL for both: `enableExternalWebCheckout(successURL:cancelURL:paymentProcessors:)`. That form still works, but is deprecated.
    </Note>

    **Set your user ID on Helium** before the user reaches a paywall. If you must set user ID after showing a paywall, set the `allowWebCheckoutWithoutUserId` option to true.

    **Handle the checkout return.** Whether checkout succeeds, is canceled, or fails, the user is deep-linked back to your app via the redirect URL. For a smoother handoff, call `Helium.shared.handleURL(url)` from your `onOpenURL` / `application(_:open:)` handler.

    **Add a "Manage Subscription" button** so users can cancel or update payment methods via Stripe's customer portal:

    ```swift theme={null}
    if await Helium.entitlements.hasActiveStripeEntitlement() {
        // show the manage subscription button
    }

    // when the button is tapped:
    do {
        let url = try await Helium.shared.createStripePortalSession()
        UIApplication.shared.open(url)
    } catch {
        print("Failed to open customer portal: \\(error)")
    }
    ```
  </Tab>

  <Tab title="Expo (Helium 3.4.13+)">
    **Enable external web checkout before initializing Helium:**

    ```tsx theme={null}
    enableExternalWebCheckout({
      redirectURL: "yourapp://openapp",
      paymentProcessors: ["stripe"],
    });
    ```

    The `redirectURL` is set once, globally. Checkout redirects back to it whether the purchase succeeds, is canceled, or fails. Make sure it's registered as a deep link in your app (via scheme or universal link).

    <Note>
      On older SDK versions that don't have `redirectURL`, pass the equivalent `successURL` and `cancelURL` parameters with the same URL for both. That form still works, but is deprecated.
    </Note>

    **Set your user ID on Helium** before the user reaches a paywall. If you must set user ID after showing a paywall, make sure to call `setAllowWebCheckoutWithoutUserId(true)`.

    **Handle the checkout return.** Whether checkout succeeds, is canceled, or fails, the user is deep-linked back to your app via the redirect URL. For a smoother handoff, call `heliumHandleURL(url)` from your deep link handling:

    ```tsx theme={null}
    useEffect(() => {
      const sub = Linking.addEventListener('url', (event) => heliumHandleURL(event.url));
      void Linking.getInitialURL().then(heliumHandleURL);
      return () => sub.remove();
    }, []);
    ```

    <Note>
      You do not need to do anything with the links other than forward them to `heliumHandleURL` like shown above. If you are using **Expo Router** you may need to [override the default behavior](https://docs.expo.dev/router/advanced/native-intent/#sending-navigation-events-to-third-party-services).
    </Note>

    **Add a "Manage Subscription" button** so users can cancel or update payment methods via Stripe's customer portal:

    ```tsx theme={null}
    if (await hasActiveStripeEntitlement()) {
        // show the manage subscription button
    }

    // when the button is tapped:
    const url = await createStripePortalSession();
    if (url) {
      await Linking.openURL(url);
    }
    ```
  </Tab>

  <Tab title="Flutter (Helium 3.3.8+)">
    **Enable external web checkout before initializing Helium:**

    ```dart theme={null}
    await HeliumFlutter().enableExternalWebCheckout(
      redirectURL: "yourapp://openapp",
      paymentProcessors: {HeliumWebCheckoutProcessor.stripe},
    );
    ```

    The `redirectURL` is set once, globally. Checkout redirects back to it whether the purchase succeeds, is canceled, or fails. Make sure it's registered as a deep link in your app (via scheme or universal link).

    <Note>
      On older SDK versions that don't have `redirectURL`, pass the equivalent `successURL` and `cancelURL` parameters with the same URL for both. That form still works, but is deprecated.
    </Note>

    **Set your user ID on Helium** before the user reaches a paywall. If you must set user ID after showing a paywall, make sure to call `setAllowWebCheckoutWithoutUserId(true)`.

    **Handle the checkout return.** Whether checkout succeeds, is canceled, or fails, the user is deep-linked back to your app via the redirect URL. For a smoother handoff, call `handleURL(url)` from your deep link handling:

    ```dart theme={null}
    _linkSubscription = AppLinks().uriLinkStream.listen((uri) {
      HeliumFlutter().handleURL(uri.toString());
    });
    ```

    **Add a "Manage Subscription" button** so users can cancel or update payment methods via Stripe's customer portal:

    ```dart theme={null}
    final hasStripe = await HeliumFlutter().hasActiveStripeEntitlement();
    if (hasStripe) {
        // show the manage subscription button
    }

    // when the button is tapped — returnUrl is where Stripe sends the user
    // back after they're done managing their subscription:
    final urlString = await HeliumFlutter().createStripePortalSession(
      returnUrl: "yourapp://openapp",
    );
    if (urlString != null) {
      await launchUrl(Uri.parse(urlString));
    }
    ```
  </Tab>
</Tabs>

\*\*The Helium SDK will return an unauthenticated link as Stripe customer portal, so your users will have to provide email and log in. For a smoother experience, we recommend creating a [pre-authenticated link](https://docs.stripe.com/customer-management/integrate-customer-portal).

### 5. (Optional) Connect RevenueCat

If you use [RevenueCat alongside Stripe](https://www.revenuecat.com/docs/web/integrations/stripe), follow these additional steps so that RevenueCat will be able to track entitlements for Stripe purchases:

<Steps>
  <Step title="Visit your RevenueCat Account Settings and find the Connect Stripe button" />

  <Step title="Install the Revenue Cat app to your Stripe account" />

  <Step title="Open RevenueCat Dashboard">
    * Open the dashboard: [https://app.revenuecat.com/](https://app.revenuecat.com/)
    * Select your Project > Apps & Providers > Add Web Config
    * Find your Stripe account and complete the flow
  </Step>

  <Step title="Select the Stripe web configuration you just added and configure Stripe webhooks to send to RevenueCat">
    * Open the dashboard: [https://www.revenuecat.com/docs/platform-resources/server-notifications/stripe-server-notifications](https://www.revenuecat.com/docs/platform-resources/server-notifications/stripe-server-notifications)
    * Make sure to include these events:
      1. customer.subscription.updated
      2. customer.subscription.deleted
      3. charge.refunded
      4. invoice.updated
      5. customer.subscription.created
      6. checkout.session.completed
  </Step>

  <Step title="Enable Track new purchases from server-to-server notifications">
    Make sure to enter **rc\_user\_id** as the Metadata field key:

    <Frame>
      <img src="https://mintcdn.com/helium/xncQKV6C2-wx-EJ0/images/download(4).png?fit=max&auto=format&n=xncQKV6C2-wx-EJ0&q=85&s=2dbf0053a50f3cf8c4b9d7df707d632d" alt="Download (4)" width="1682" height="760" data-path="images/download(4).png" />
    </Frame>
  </Step>

  <Step title="Configure your Stripe products with RevenueCat">
    1. Visit **Product catalog** for your desired project in RevenueCat dashboard.
    2. In the Products tab, find the Stripe section and add Stripe products
    3. Go to Entitlements tab, select desired entitlement/s, click Attach and add the Stripe products you just created
    4. If you rely on Offerings in your RevenueCat integration, you can also add the Stripe products there
  </Step>
</Steps>

## Testing

To test without affecting production traffic, create a test targeting group that includes:

* Stripe eligible users
* `sandbox` or `debug` environment

Target the App2Web paywall to that group, then run through these scenarios before going live:

1. **Happy path purchase.** Open an App2Web paywall in-app, tap a web product, and confirm the app correctly opens the browser. Complete the standard purchase flow, then confirm you're deep-linked back with an active entitlement. Dismiss the paywall before purchasing as well, and confirm the app returns to the expected screen.
2. **Entitlement sync.** After purchase, confirm `hasActiveStripeEntitlement()` returns `true` immediately, and that RevenueCat (if connected) updates within a few seconds.
3. **Cancel flow.** Open the customer portal from your app, cancel a subscription, and confirm the entitlement expires at period end.
4. **Canceled checkout.** Back out of the checkout page without paying and confirm you're deep-linked back to the app with no entitlement granted and no success state shown.
5. **Pre-login purchase (if applicable).** If your App2Web paywall can be shown before you set a durable `user_id`, make a purchase before setting it. Set the durable user ID afterwards and confirm the entitlement attaches correctly.

## Appendix

### How entitlements work

Testing sign-in, access, or RevenueCat delivery? See [How to test App2Web entitlements](/guides/app2web-faq-stripe#test-entitlements).

Helium maps your `user_id` to Stripe's `customer_id`. If the user isn't logged in at purchase time, we use the `helium_persistent_id` as a stand-in and reconcile once a real user ID is set.

On successful purchase, the SDK's entitlement cache updates immediately. The Helium backend processes Stripe's webhook to update the customer mapping and forward events to your analytics/revenue pipeline. Entitlements are also checked server-side (which ultimately reflects Stripe's transaction state), so they stay consistent across devices.

### Why two paywalls?

The original in-app paywall presents your Stripe offerings. The hosted web paywall — served from `clickthrough.to` — runs the actual Stripe checkout, which has to happen in a browser. The two can be customized independently and don't need to show the same products.

### Apple Pay readiness

Web checkout on iOS pays with Apple Pay. A small share of users open it from a browser that cannot complete an Apple Pay payment, and they come back to the app without buying. Apple Pay readiness keeps those users out of web checkout so you can route them to an in-app purchase paywall instead.

**How it works.** On launch, the SDK checks whether Apple Pay can pay on the checkout domain and reports the result to Helium as the `webApplePayReadiness` targeting variable. The **{preset_0}** targeting preset excludes users whose result is `notReady`. Those users no longer match your App2Web audience, so give them an IAP-only rule later in the same workflow, or they get no paywall. Everyone else keeps seeing web checkout, including users on older SDKs and users whose result is `unknown`.

The result is an optimization signal, not a guarantee, since the user's Wallet can change between the check and checkout. Presenting a paywall does not wait for it, apart from the first-launch wait described under Launch timing.

**Versions.** iOS only. Measurement is on by default from Helium iOS 4.13.0, `expo-helium` 3.6.3, `@tryheliumai/paywall-sdk-react-native` 3.1.13, and `helium_flutter` 3.3.14. Earlier versions report `ready` without measuring, unless you opted in during the beta.

Measurement runs only when external web checkout is already configured when the launch request is sent. Call `enableExternalWebCheckout` before you initialize Helium, as shown above. If you configure it later, users report `unknown:notMeasured` and the readiness rule excludes no one.

**Turning it off.** Set the flag before you initialize Helium. Users then report `unknown:notMeasured` and keep seeing web checkout.

<Tabs>
  <Tab title="iOS (native)">
    ```swift theme={null}
    Helium.config.enableWebApplePayReadiness = false
    Helium.initialize(...)
    ```
  </Tab>

  <Tab title="Expo">
    ```tsx theme={null}
    setEnableWebApplePayReadiness(false);
    ```

    The setter is a no-op on Android.
  </Tab>

  <Tab title="React Native">
    ```tsx theme={null}
    setEnableWebApplePayReadiness(false);
    ```

    Available from `@tryheliumai/paywall-sdk-react-native` 3.1.14. The setter is a no-op on Android. On 3.1.13, set `Helium.config.enableWebApplePayReadiness = false` in your iOS `AppDelegate` before your JavaScript code initializes Helium.
  </Tab>

  <Tab title="Flutter">
    ```dart theme={null}
    await HeliumFlutter().setEnableWebApplePayReadiness(false);
    ```

    Available from `helium_flutter` 3.3.15. The setter is a no-op on Android. On 3.3.14, set `Helium.config.enableWebApplePayReadiness = false` in your iOS `AppDelegate` before your Dart code initializes Helium.
  </Tab>
</Tabs>

If you drive the flag from remote config, read the value cached from the previous session and default to `true`. A value fetched after initialization only takes effect on the next launch.

**Launch timing.** On the first launch with measurement on, after install or after updating to a version that measures by default, the SDK waits up to two seconds for the measurement before it requests your paywall configuration. Later launches send the stored result immediately and re-measure in the background. A launch into the background skips the measurement and the wait, and the SDK measures once the app becomes active.

**Apps that declare `WKAppBoundDomains`.** Add `bundles.clickthrough.to` to that list in your `Info.plist`. Without it, iOS blocks the check: users report `unknown:probeFailed` on that first launch and `unknown:notMeasured` afterwards. Checkout still works and users still see web checkout, but users who are not ready are no longer excluded. Apps that don't declare `WKAppBoundDomains` need no change.

**Targeting.** `webApplePayReadiness` is `ready`, `notReady`, or `unknown:<reason>` when the measurement could not answer. The {preset_0} preset already handles it. If you built a {processor_0} audience without the preset, add a rule that `webApplePayReadiness` is not `notReady`. If an audience uses the standalone **Browser Apple Pay ready** preset from the beta, keep it unless you also add the {preset_0} preset to that audience, since it may be the only readiness rule there.

### Known limitations and gotchas

* **Availability:** Stripe App2Web is currently supported for US storefronts only.
* **Publish states:** both the original paywall and its linked hosted web paywall must be published before the flow works end-to-end.
* **Localizations:** hosted web paywalls currently support one language.
* **Testing:** [App2Web previews and SDK requirements](/guides/paywall-previews#app2web-previews) cover simulated purchases and real purchases on [eligible US devices](/guides/app2web-previews#purchase-eligibility). Also test through a real trigger to verify targeting and your post-purchase handlers.

## Before you go live

Run through the [App2Web go-live checklist](/guides/app2web-go-live-checklist) before sending traffic to this flow.

See the [App2Web FAQ](/guides/app2web-faq-stripe) for attribution, checkout behavior, and evaluating experiment results.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.