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

# Build a paywall

> Create a paywall flow, add Offers blocks bound to real packages, preview every state, and publish.

A paywall is a flow with a purchase attached, so you build it in the normal editor. This page covers the parts that are specific to paywalls: the Offers blocks, binding plans to packages, dynamic pricing, the preview modes, and the extra publish checks.

<Info>
  **Before you start**

  Paywalls build on the standard FlowPilot setup, plus RevenueCat:

  * A FlowPilot [workspace](/dashboard/workspaces-and-apps), an [app](/dashboard/workspaces-and-apps) inside it, and an [SDK API key](/dashboard/api-keys) for that app.
  * A [RevenueCat](https://www.revenuecat.com) account with your products, entitlements, and an offering configured for your app.
  * Paywalls enabled for your workspace. If you do not see a **Paywalls** item in the dashboard sidebar, reach out and we will turn it on.
</Info>

## Create a paywall

<Steps>
  <Step title="Start a new paywall">
    Open the **Paywalls** section in the sidebar and choose **New paywall**. This creates a flow of type paywall, seeded with a starter layout and its monetization settings already turned on.
  </Step>

  <Step title="Set the offering">
    In the flow settings, under **Monetization**, pick which RevenueCat offering this paywall shows. The default is your **current** offering, so changing your featured offering in RevenueCat updates the paywall without a republish. You can also pin a specific offering by identifier.
  </Step>
</Steps>

## Monetization settings

The paywall inspector holds the settings that make a flow behave like a paywall. You reach it from the flow settings panel.

| Setting                  | What it does                                                                                                                                           |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Offering**             | Current, or a pinned offering by identifier. Determines which packages are available.                                                                  |
| **Required entitlement** | The entitlement this paywall unlocks (for example `pro`). Used for gating and to skip already-subscribed users.                                        |
| **Skip if entitled**     | When on, the paywall is not shown to a user who already holds the required entitlement. On by default.                                                 |
| **Gating mode**          | `soft` (dismissible, continues on close), `hard` (closing leaves the feature locked, your app decides), or `none` (informational). Defaults to `soft`. |
| **Close behavior**       | Whether to show the close control, and an optional delay before it appears.                                                                            |
| **Default plan**         | Which package is pre-selected when the paywall opens.                                                                                                  |
| **Plan order**           | The order your bound packages appear in.                                                                                                               |
| **Legal links**          | Your Terms and Privacy URLs, surfaced by the legal footer.                                                                                             |
| **Restore copy**         | The message shown if a restore finds no purchase.                                                                                                      |

## Add Offers blocks

Paywall-specific building blocks live in the editor's **Offers** category. They are compositions of standard components, so they render on every SDK with no extra work, but they carry the purchase wiring for you.

<CardGroup cols={2}>
  <Card title="Plan picker" icon="table-cells">
    A group of selectable plan cards that behaves like a single-select question. Add or remove plans, bind each to a package, set the order, and mark a best-value badge.
  </Card>

  <Card title="Plan option" icon="square-check">
    A single selectable plan card: name, price, period, a trial line that shows only when the user is eligible, and a savings badge slot. Tapping it selects that package.
  </Card>

  <Card title="Purchase CTA" icon="bolt">
    The primary button. It runs the purchase, shows a busy state while it is in flight, and closes the flow on success by default.
  </Card>

  <Card title="Restore" icon="rotate-left">
    A text button that restores a previous purchase. App Review requires one somewhere in the flow.
  </Card>
</CardGroup>

There is also a **Legal footer** block that renders your Terms and Privacy links plus the auto-renewal disclosure, bound to the URLs in your monetization settings. Everything else on the screen, the headline, benefit list, hero image, animations, is normal content.

## Bind plans to packages

Each plan card points at one package from your synced catalog. Select a plan card, open its controller, and pick the package. When you rebind a plan, FlowPilot rewrites its price bindings and its selection wiring in one step, and keeps the plan order in sync, so you do not chase references by hand.

<Note>TODO: screenshot of a plan option controller showing the package picker fed by the synced catalog.</Note>

## Dynamic, store-truth pricing

Plan cards never contain a typed-in price. They read live values from the store through variables in the `paywall` namespace, so what a user sees is exactly what they will be charged, in their currency, after any eligible trial.

The most useful values:

| Variable                         | Meaning                                                                     |
| -------------------------------- | --------------------------------------------------------------------------- |
| `paywall.selected.price`         | Localized price of the currently selected plan.                             |
| `paywall.selected.periodText`    | Localized billing period of the selected plan, for example `month`.         |
| `paywall.package.<id>.price`     | Localized price of a specific package.                                      |
| `paywall.package.<id>.trialText` | Trial text, for example `7 days free`, empty when the user is not eligible. |
| `paywall.productsState`          | `loading`, `loaded`, or `failed`, for skeleton and retry states.            |
| `paywall.purchaseState`          | Drives the CTA's busy and disabled state during a purchase.                 |

Use them in copy the same way you use any variable, wrapped in interpolation. For example, a CTA label of `Continue for {{paywall.selected.price}}` updates as the user switches plans.

<Warning>
  Never type a currency symbol and a number into paywall copy. Apple requires the billed price to come from the store, and the publish validator blocks a paywall that hardcodes one. Bind to a `paywall` price variable instead.
</Warning>

## Preview every state

Prices, trials, and purchase outcomes only exist at runtime, so the editor gives you a paywall preview to exercise them. Open the paywall preview control in the phone toolbar and switch between:

* **Products:** loading, loaded, or failed to load.
* **Trial:** eligible or not eligible.
* **Purchase result on tap:** success, cancelled, pending, or failure.
* **Restore result:** found or not found.
* **Entitlement:** subscribed or not, to preview the skip-if-entitled path.
* **Locale:** sample currencies including a right-to-left layout, to catch overflow and truncation.

Preview prices are drawn from your synced catalog and are clearly watermarked as simulated. They are never published and never shown to a user.

## Publish

Publishing a paywall runs the normal publish flow plus a set of paywall checks. If any fail, publishing is blocked until you fix them. The main ones:

* A **purchase CTA** is present.
* A **restore** action exists somewhere in the flow.
* **Legal links** (Terms and Privacy) are set.
* **No hardcoded prices** anywhere in the paywall's text.
* Every bound package exists in your catalog and is not removed.

These checks exist so the paywall you ship is compliant by construction. See [App Store review](/paywalls/app-review) for the full compliance story.

## Common mistakes

* **Typing a price into a headline or button.** Bind to `paywall.selected.price` or a `paywall.package.<id>.price` variable so the store's real, localized price shows.
* **Forgetting the restore button.** App Review will reject a subscription paywall with no way to restore. Add the restore block.
* **Leaving trial copy unconditional.** A "7 days free" line must be bound to the trial variable so it only appears for users who are actually eligible.
* **Not previewing the failed-products state.** If products fail to load on device, your paywall still has to look right. Preview the loading and failed states.

## Related pages

* [Connect RevenueCat](/paywalls/connect-revenuecat)
* [iOS integration](/paywalls/ios)
* [Expo integration](/paywalls/expo)
* [App Store review](/paywalls/app-review)
* [Interactions and actions](/editor/interactions-and-actions)
