> ## Documentation Index
> Fetch the complete documentation index at: https://help.the-meridian.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Pricing page

`MeridianPricingPage` renders your whole plan grid from the plans you built in the [plan builder](/introduction-1): prices, trials, features, events, discounts, and the subscribe flow.

```tsx theme={null}
import { MeridianPricingPage } from "@the-meridian/sdk"

<s-page heading="Pricing">
  <s-section>
    <MeridianPricingPage returnUrl={returnUrl} />
  </s-section>
</s-page>
```

It requires a [`MeridianProvider`](/sdk-provider) ancestor, and it renders Polaris web components, so it must run inside an embedded Shopify app with App Bridge loaded, which every Shopify app template provides. Outside the embedded admin the elements are unregistered and render as unstyled text, with a console warning.

## The returnUrl

`returnUrl` is where Shopify sends the merchant after they approve or decline the charge. It **must** be an `https` URL on the shop's own admin:

```
https://<shop>.myshopify.com/admin/apps/<client_id>/<route>
https://admin.shopify.com/store/<store>/apps/<app>/<route>
```

Build it in a loader, from the session shop and your Shopify client ID:

```ts theme={null}
const returnUrl = `https://${session.shop}/admin/apps/${process.env.SHOPIFY_API_KEY}/app/pricing`
```

<Warning>
  Your app's own hosting origin, `request.url`, `window.location`, and any `http://` URL are all rejected. The SDK throws before the call, with the correct format in the message. The shop-admin URL is also what re-embeds your app after approval instead of dropping the merchant on a login screen.
</Warning>

## Props

| Prop | Notes |
| - | - |
| `returnUrl` | Required in practice. See above |
| `onPlanSelected` | Replaces the default subscribe flow. The page calls you with the plan ID and does nothing else |
| `onSubscribed` | Fires after a plan activates without a billing step |

## What a merchant sees

Only **active** plans appear; drafts stay hidden until you activate them. Prices are converted to the shop's currency before they reach the browser.

Each card carries the plan name, its price for the selected cycle, its trial badge, and one line per feature and per metered event included in it. The card for the shop's current plan is outlined, and its button is disabled.

A metered event's line says what usage costs, so a merchant knows it before reaching Shopify's approval screen:

| The plan's terms | The line reads |
| - | - |
| Included quantity only | 100 translations |
| Overage rate and a monthly cap | 100 translations included, then $0.10 per 10 translations, up to $5.00 per month |
| Overage rate, no cap | 100 translations included, then \$0.10 per 10 translations |
| Overage rate, nothing included | $0.10 per 10 translations, up to $5.00 per month |
| Overage rate under one cent (0.005 per translation) | 100 translations included, then $0.05 per 10 translations, up to $5.00 per month |

Amounts are in the shop's currency, and the rate is the one Meridian states in the usage terms Shopify asks the merchant to approve for the same plan (see [What the merchant approves](/create-a-plan#what-the-merchant-approves)). It is converted exactly and never rounded to the cent: 0.10 USD in a euro store at an FX rate of 0.92 reads 0,092 €, not 0,09 €. A rate under one cent is stated per ten times the units until it reaches a cent, so 0.005 per translation reads "$0.05 per 10 translations" and 0.0004 reads "$0.04 per 100 translations". The cap is in cents, the spending limit sent to Shopify.

The words come from the event's **Unit** in the [catalog](/introduction-6), pluralized for any count other than one ("per translation" for a rate on a single unit). An abbreviation such as `GB` and a unit that already ends in "s" are left as you typed them. An event with no unit uses its name instead, as written.

A priced [usage view](/usage-views) states its allowance and its cap but not its rate, because a view's rate can be a share of money (`0.02` is 2% of GMV) rather than a price per unit: "Gross merchandise value: 1,000 included, then billed on usage, up to \$200.00 per month".

The button label reads the shop's current plan rather than saying "Subscribe" to everyone:

| Situation | Label |
| - | - |
| No current plan | Subscribe |
| Priced above the current plan | Upgrade |
| Priced below the current plan | Downgrade |
| Same monthly price, different plan | Change to this plan |
| The plan the shop is on | Current plan |

The comparison uses the monthly price, so the label holds when the merchant toggles to yearly.

## Monthly and yearly

The **Monthly** / **Yearly** toggle appears only when at least one of your plans has a yearly price. A plan without one renders and bills monthly even while the toggle is on yearly; a plan with one shows a badge for the amount saved against twelve monthly payments.

A plan priced at zero renders as **Free**, with no billing period and no trial badge.

## Discounts

The page handles both kinds of [discount](/discounts) without any wiring from you.

**Automatic discounts** are previewed per plan for the selected cycle, so the price shown is the price charged. **Codes** are entered behind an **Add promo code** control: the page validates a code against every plan at once and shows the discounted price on the cards it applies to. A typed code takes precedence over an automatic one, and the backend re-validates at subscribe time.

An invalid code reports on the field alone, never revealing whether another code exists.

Shopify bakes a discount into a subscription when it is created, so one cannot be added to a subscription the shop is already on. On the current plan's card, a preview is labelled either **applied to your subscription** or **for new subscriptions**, so a merchant is never shown an offer they cannot take.

## Subscribing

Picking a plan calls `subscribe`, which returns the Shopify confirmation URL the merchant has to approve. The page opens it in the top frame.

A free plan has no approval step: it activates immediately, returns no confirmation URL, and the page refreshes so the new plan shows at once. `onSubscribed` fires then.

### Coming back from Shopify

After the merchant approves the charge, Shopify sends them back to your `returnUrl` with a `charge_id` in the URL. The page reads it and asks Meridian to confirm the charge with Shopify directly, rather than waiting for Shopify's subscription webhook, which can land a minute or more later. The merchant sees no new message at any point:

* The plan cards stay on screen. The button of the plan the merchant approved shows its loading state, and every other plan button is disabled, so a second subscribe cannot start. When the page cannot tell which plan was approved yet, all plan buttons are disabled and none shows the loading state.
* As soon as the charge is active, the page refreshes in place and the card reads **Current plan**. This usually takes a second or two.
* If the merchant declined the charge, or it expired, the buttons come back as they were.
* If nothing is settled after 60 seconds, the buttons are enabled again. The page keeps checking in the background every 15 seconds, still without a message, until 5 minutes after the return, and turns to **Current plan** by itself if the charge settles meanwhile.

The page then removes `charge_id` from the URL, so a reload does not start the check again. Confirming records the subscription exactly as Shopify's webhook would, so your [automations](/automations) fire once, whichever arrives first.

This only works when your `returnUrl` points at the route that renders `MeridianPricingPage`, as in the examples above. Before sending the merchant to Shopify, the page also remembers the chosen plan in the browser's session storage: that is how it knows which button to show as loading, and it lets the page run one silent check even if the return URL carries no `charge_id`.

Meridian records the subscription and holds the Shopify billing handshake, so your app never implements the recurring-charge or usage-charge APIs. Once the subscription is active, [feature gates](/features) and [usage allowances](/events) resolve against the new plan.

<Warning>
  Subscribing requires your app to be [hosted on Meridian](/sdk-hosting). Meridian reads the shop's Shopify offline token out of your app's own session store to create the charge. On an app hosted elsewhere the call fails with `PLAN_BUILDER_UNAVAILABLE`, and the plan builder is gated in the dashboard for the same reason.
</Warning>

## Building your own instead

Pass `onPlanSelected` to intercept the click, or skip the component: read `plans` from [`useMeridian`](/sdk-use-meridian) and call `subscribe` yourself. The plan objects carry everything the built-in page renders, including each plan's features and its events' included quantity, overage rate and cap.


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