# Products, Prices, and Paid Access

Source: https://docs.ezoic.com/docs/subscriptions/products/


A product is the paid thing visitors can buy — a subscription, or a one-time purchase. Each product has a **product handle** and one or more **prices**, and each price has a **price handle**. Those two handles drive the onsite integration: you check the product handle with `ezsubscriptions.hasAccess(...)`, and you open checkout with either `ezsubscriptions.showPaywall({ product })` or `ezsubscriptions.openCheckout({ price })`.

## Products

You create products in the Ezoic dashboard. A product has:

- A public name, such as `Remove Ads` or `Premium`.
- An optional description shown on the paywall.
- A **product handle** your site code uses.
- One or more prices.

Suppose you create a product named `Remove Ads` with the product handle `remove-ads`. On your site, your template checks access with `ezsubscriptions.hasAccess("remove-ads")`. If the visitor has access, you show the subscriber-only experience. If they do not, you call `ezsubscriptions.showPaywall({ product: "remove-ads" })`, which opens Ezoic's pre-built paywall and checkout experience for that product's prices.

You can create more than one product on a site — for example, a `remove-ads` subscription site-wide and a separate `poll-access` product on results pages — and open whichever one fits the page.

## Product Handles

The product handle is the stable identifier your site passes to `hasAccess(...)` and `showPaywall({ product })`. Pick a handle when you create the product in your Ezoic dashboard.

Product handles are:

- Domain-scoped and unique per site.
- Case-insensitive and stored lowercase.
- Limited to letters, numbers, hyphens, and underscores.

Examples: `remove-ads`, `premium`, `poll-access`.

Use handles that describe the access level, not the current price or promotion. Avoid changing a product handle after your site is using it unless you also update the site code that references it.

## Prices

A price is a way to buy a product. Each price has:

- A label, such as `Monthly`, `Annual`, or `Lifetime`.
- An amount and billing interval — weekly, monthly, or yearly, including every-N intervals such as every 3 months.
- A **price handle** your site code can use for custom checkout buttons.

`showPaywall({ product })` presents all of a product's active prices and lets the visitor choose. When you want a button that charges one specific price directly — skipping price selection — pass its price handle to `ezsubscriptions.openCheckout({ price })`.

Price handles follow the same rules as product handles: domain-scoped, unique per site, case-insensitive, and limited to letters, numbers, hyphens, and underscores. Examples: `remove-ads-monthly`, `remove-ads-annual`.

## Additional Currencies

Every price has a default currency (USD unless you change it). Without any further setup, checkout automatically presents a local-currency equivalent to international visitors, converted at the payment provider's exchange rate. Two things to know about the automatic conversion: the amounts follow the rate, so they are rarely round numbers (a `$9.99` price might present as `€9.21`), and the buyer pays a small conversion fee on top. Visitors can always switch back to your listed currency at checkout if they prefer it. Automatic currency conversion is on by default; you can turn it off in the Ezoic dashboard's Subscriptions area under **Settings → Ways buyers pay → Adaptive Pricing**.

**Recommended:** set your own amounts per market with **additional currencies** on the price — for example `$9.99` for US visitors and `₹499` for visitors in India. A visitor whose region matches one of your currencies sees and pays that exact amount in that currency, with no conversion fee. This works for recurring and one-time prices, and for donations (where each currency sets its own suggested amounts and minimum — see [Donation Integration](/docs/subscriptions/donations/#currencies)).

No onsite code changes are needed either way — the paywall and checkout present the visitor's currency automatically.

## One-Time Purchases

A one-time price is a single payment rather than a recurring subscription. When you create one in the dashboard, two extra fields decide exactly what the payment buys:

- **What this unlocks** — one of three modes (see below).
- **Access duration (days, optional)** — leave it blank for lifetime access, or enter a number of days for a time-limited pass (for example a 1-day article rental or a 7-day pass). When the window lapses, access stops and the check returns `expired`, so always treat a fresh check as the source of truth.

### What this unlocks

| Mode | What it grants | Gate the content with |
|---|---|---|
| **The whole product** | The entire product, exactly like a subscription. | `hasAccess({ product })` |
| **A single item your site provides** | One item you name at checkout (an article id, a download slug). | `hasAccess({ product }) \|\| hasPurchased({ item })` |
| **The current article automatically** | The article the paywall is shown on. One price sells every article with no per-article setup; each purchase unlocks only the page it was bought on. | `hasPurchased({ page: true })` |

Either item mode grants access to that one item only: it never satisfies `hasAccess({ product })` and never appears in `getProducts()`.

### A single item your site provides

Pass a publisher-chosen `item` key alongside the price at checkout:

```javascript
await ezsubscriptions.openCheckout({
  price: "article-unlock",
  item: "article-12345",
});
```

You can also let Ezoic's paywall sell it: `ezsubscriptions.showPaywall({ product: "premium", item: "article-12345" })`. Check it later with `ezsubscriptions.hasPurchased({ item: "article-12345" })`. The `item` is a site-wide key — stored verbatim and matched exactly on its own, no price needed — so keep it stable per item.

### The current article automatically

Pick this mode when every article sells the same way and you don't want to maintain a per-article id. Put `ezsubscriptions.showPaywall({ product: "premium" })` on the article — the paywall sells access to that page — and reveal an already-bought article with `hasPurchased({ page: true })`:

```javascript
const access = await ezsubscriptions.hasPurchased({ page: true });
if (access.decision === "allowed") {
  revealArticle();
}
```

`{ page: true }` tells Ezoic to check the current page using the same normalized page key it stamps at checkout, so your check always matches what was sold — you never compute or pass the item yourself.

## Free Trials

A recurring price can start with a free trial. Set **Free trial (days)** on the price in the dashboard (1–365 days) to give new subscribers a no-charge trial before their first payment.

With a trial configured:

- The paywall shows a **Start {N}-day free trial** call to action (for example, "Start 7-day free trial") and a trial badge. The badge label is customizable and substitutes the trial length — the default is `{days}-day free trial`.
- The visitor enters a payment method but is not charged during the trial. They get full access immediately, so `hasAccess(...)` returns `allowed` while the trial is active.
- The subscription converts to paid automatically when the trial ends, unless the subscriber cancels first from the [subscriber portal](/docs/subscriptions/subscriber-experience/#subscriber-portal).

Trials are limited to one per product per subscriber. A visitor who has already used the trial (or whose card was used for one) cannot start it again: when they submit the trial signup, checkout shows an "already redeemed a free trial" notice with the option to subscribe at the regular price instead. Until then the paywall can still show the trial call to action, so a returning visitor may see the trial offer briefly before checkout flags it at signup.

No onsite code changes are needed for trials — you check and sell the product exactly as you would without one.

## Example Product Structures

### Ad-Free Subscription

- Product name: `Remove Ads`
- Product handle: `remove-ads`
- Price: `Monthly` — `$4.99 / month` — price handle `remove-ads-monthly`

The most common setup: one product that removes ads for subscribers.

### Premium Content Subscription

- Product name: `Premium`
- Product handle: `premium`
- Price: `Monthly` — `$9.99 / month` — price handle `premium-monthly`

Use this when one paid product unlocks all subscriber-only content.

### Subscription With Multiple Prices

- Product handle: `premium`
- Price: `Monthly` — `$9.99 / month` — price handle `premium-monthly`
- Price: `Annual` — `$99.00 / year` — price handle `premium-annual`

Use this when you want to offer the same product at more than one billing interval. `showPaywall({ product: "premium" })` lets the visitor choose; a custom button can call `openCheckout({ price: "premium-annual" })` to sell a specific price.

## Appearance and Styling

Each product has its own Appearance settings — template, color mode, and accent color — plus CSS variables for finer brand matching. See [Paywall Appearance and Styling](/docs/subscriptions/paywall-appearance/).

## Discounts

To offer a percentage or fixed-amount discount at checkout, create a [promo code](/docs/subscriptions/promo-codes/). When a domain has an active promo code, the checkout automatically shows a code entry field — no onsite code required.

## Donations Are Separate

Donations are a separate domain-level support option. See [Donations](/docs/subscriptions/donations/).

## After Creating a Product

After your product is live:

1. Add the onsite script to your site — see [Onsite Script Integration](/docs/subscriptions/site-integration/).
2. Use the product handle in `ezsubscriptions.hasAccess(...)` — see [Publisher-Managed Access API](/docs/subscriptions/publisher-managed-access/).
3. Use the product handle in `ezsubscriptions.showPaywall({ product })`, or a price handle in `ezsubscriptions.openCheckout({ price })`.
4. If you gate article content, add [paywalled-content SEO markup](/docs/subscriptions/seo-paywalling-best-practices/).
5. Verify checkout as a new visitor.

