Skip to main content
useMeridian() is the hook every component uses to read the shop’s plan and act on it. It throws if there is no MeridianProvider above it.

State

Everything is null until loading turns false. Gate on loading before you gate on a feature.

Gates

Four synchronous lookups, safe to call in render:
Features and events are addressed by their key: the app-local identifier you set when you define the feature or the event. A key that does not exist reads as false / undefined, so a typo gates nothing and fails silently. See Feature gating. getUsage() also takes a view key: views and events share one key namespace per app, so you hold one string and don’t have to know which kind it is. A view’s value comes back as usedThisPeriod over that view’s own window; getView() is the version that also tells you what that window is. If the store’s plan prices that view, getUsage() carries includedQuantity and cappedAmount too, exactly as it does for a metered event, so one usage meter in your UI renders either kind without branching on which it got. Their absence is the signal that nothing is billed on the metric.

Actions

cancelSubscription refreshes the snapshot for you. subscribe returns a confirmationUrl the merchant has to be sent to in order to approve the charge. A free plan has no approval step, so it activates immediately, returns no URL, and refreshes the snapshot instead. See Pricing page. confirmSubscription is for the merchant’s return from that approval screen, if you build your own pricing page. Pass the charge_id Shopify appends to your returnUrl, or nothing to confirm the shop’s newest pending charge. Meridian asks Shopify for the charge’s status instead of waiting for Shopify’s subscription webhook, records it (your automations fire exactly as they would for the webhook), and returns it with the plan’s ID. status is ACTIVE once approved, PENDING while Shopify has not settled it, DECLINED or EXPIRED when it never became a subscription, and null when Meridian knows no such charge. It does not refresh the snapshot: call refresh({ silent: true }) once the status is settled. Meridian asks Shopify at most once every 5 seconds per store and otherwise answers what it already recorded, so calling it every few seconds is fine. MeridianPricingPage does all of this for you. A charge nobody confirms (the merchant never came back, and the webhook was lost) is not left pending: Meridian also asks Shopify about it on a schedule, a few minutes after it was created and again at growing intervals, and records whatever Shopify answers. A silent refresh leaves loading alone, keeps the current snapshot when the request fails (no error either), and keeps the same objects when nothing changed, so effects that depend on plans or entitlements do not re-run on every poll. A plain refresh() sets loading and replaces everything, as before.
returnUrl on subscribe is optional in the type only. Omitted, it falls back to window.location.href, which inside an embedded app is the iframe URL and is rejected. Always pass a shop-admin https URL built server-side.
validateDiscount previews a code, or the best automatic discount when you pass undefined. It never throws on a bad code. It resolves { valid: false } with a generic message, so the existence of your codes is never leaked to a merchant guessing at them.

Pure helpers

The same three gates exist as standalone functions over an Entitlements snapshot, exported from both entries. Use them when you already hold a snapshot and are not inside a component: a useMemo, a reducer, a server route:
A null snapshot reads as ungated rather than throwing.

useOptionalMeridian

useOptionalMeridian() returns the same context value, or null when there is no provider, for code that has to keep working in an app running without Meridian credentials. The SDK’s own page components use it to degrade to an empty state instead of crashing. Reach for it only in shared components that genuinely mount in both worlds. In an app that always mounts the provider, useMeridian()’s throw is the more useful behaviour.
Last modified on October 7, 2026