# Onsite Script Integration

Source: https://docs.ezoic.com/docs/subscriptions/site-integration/


The onsite script is the main integration point for Ezoic Subscriptions. It gives your site access to subscriber access checks, Ezoic's pre-built paywall and checkout experience, donation dialogs, and subscriber login, including guest checkout when enabled.

## Add the Script

Add the Ezoic Subscriptions script on pages where subscriptions or donations should work:

```html
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

Because the script loads asynchronously, wrap your integration code in the `ezsubscriptions.cmd` queue. The callback runs after the widget is ready.


The examples on this page assume the default **Ezoic visitor accounts**, where Ezoic handles sign-in for you. If you [bring your own login](/docs/subscriptions/visitor-authentication/#bring-your-own-login), a few things work differently — you register an auth adapter and signal sign-in changes yourself. See [Visitor Authentication](/docs/subscriptions/visitor-authentication/) for the BYO-specific handling that pairs with these snippets.



**WordPress:** some SEO plugins (Rank Math, Yoast) build a post's meta description from its content when none is set — and inline integration scripts count as content, so the snippet can end up as your search or social description. Worth setting an explicit excerpt or meta description on any post that embeds these snippets.


## Removing Ads for Subscribers

The most common paid benefit is an ad-free experience, so it's the best place to start. Check whether the visitor has access, then call `ezsubscriptions.disableAds()` when the decision is `allowed` and `ezsubscriptions.allowAds()` otherwise. The widget handles the mechanics — you don't manage individual ad placeholders.

```html
<button id="go-ad-free" hidden>Go ad-free</button>

<script>
  window.ezsubscriptions = window.ezsubscriptions || {};
  ezsubscriptions.cmd = ezsubscriptions.cmd || [];
  ezsubscriptions.cmd.push(async function () {
    const cta = document.getElementById("go-ad-free");
    async function reconcileAds() {
      const access = await ezsubscriptions.hasAccess("remove-ads");
      const entitled = access.decision === "allowed";
      if (entitled) {
        ezsubscriptions.disableAds();
      } else {
        ezsubscriptions.allowAds();
      }
      if (cta) {
        cta.hidden = entitled;
      }
    }

    // Guarded so dropping the button can't take the ad suppression down with it.
    if (cta) {
      cta.addEventListener("click", function () {
        // dismissible: ad-free is an upgrade offer, not a hard wall — free
        // visitors must be able to close this and keep browsing with ads.
        ezsubscriptions.showPaywall({ product: "remove-ads", dismissible: true });
      });
    }

    // Re-run when the visitor logs in, logs out, or completes checkout.
    ezsubscriptions.on("access:change", reconcileAds);
    await reconcileAds();
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

Here `remove-ads` is the product handle of an ad-free product you create in your [Ezoic dashboard](/docs/subscriptions/products/). `disableAds()` sets a cookie that Ezoic reads on the server to suppress **every** ad format for that visitor — display, floating video, and interstitials — so ads never load rather than being torn down after they render. `allowAds()` removes that cookie when access is lost (logout, expired subscription); ads return on the visitor's next page view.

Because the server needs that cookie to suppress ads, `disableAds()` reloads the page **once** on the page where access is first gained — typically right after checkout or login. Every page view after that is ad-free with no reload. When access is gained inside checkout, the reload waits until the visitor closes the confirmation screen; for a mid-article login, their scroll position is preserved across the reload.

Ad removal applies to ads Ezoic serves on your site — including partner demand such as AdSense delivered through the Ezoic platform. Ads served outside Ezoic — another ad network, a header-bidding setup you run yourself, or ad tags you place directly on the page — are outside Ezoic's control, so you (or that provider) should remove those for subscribers as well.

Ad removal is per subscriber, not a site-wide switch. It applies to the individual visitor who is signed in and holds the entitlement, on the browser where they're signed in — so anonymous visitors, and signed-in visitors without the product, keep seeing ads. When a subscriber signs out or their access ends, `allowAds()` restores ads for them.

This assumes the Ezoic ads script is already configured and present on the page. If you have not set that up yet, complete [Ezoic Ads Integration](/docs/ezoicads/integration/) first.

The `Go ad-free` button is the sales path. It starts `hidden` so existing subscribers never see the upsell, `reconcileAds()` reveals it for visitors without access, and the same `access:change` re-check hides it again the moment checkout completes — no extra wiring. The paywall is `dismissible` on purpose: ad-free is an upgrade offer over the ad-supported free tier, not a hard wall, so free visitors can close it and keep browsing with ads. Place the button wherever you pitch the upgrade — your nav, an article footer, a pricing section — or wire any existing button or link to `ezsubscriptions.showPaywall({ product: "remove-ads" })` the same way.

This is the core access pattern. For the full access model — every access decision, checking multiple features with `getProducts()`, one-time item purchases, and reacting to access changes without a page reload (`ezsubscriptions.on("access:change", …)`) — see the [Publisher-Managed Access API](/docs/subscriptions/publisher-managed-access/).

## Gate Premium Content

Gating premium content uses the same pattern: check whether the visitor has access, reveal your subscriber-only content if they do, and show the paywall if they do not. Replace `premium` with the product handle your site checks and sells — it comes from a product you create in your [Ezoic dashboard](/docs/subscriptions/products/).

```html
<div class="paywalled-content" data-premium-content hidden>
  <!-- Subscriber-only content goes here. -->
</div>

<script>
  window.ezsubscriptions = window.ezsubscriptions || {};
  ezsubscriptions.cmd = ezsubscriptions.cmd || [];
  ezsubscriptions.cmd.push(async function () {
    const access = await ezsubscriptions.hasAccess("premium");
    if (access.decision === "allowed") {
      document.querySelector("[data-premium-content]").hidden = false;
      return;
    }

    ezsubscriptions.showPaywall({ product: "premium" });
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

Both `hasAccess` and `showPaywall({ product })` take the **product handle** from your Ezoic dashboard.

## Showing the Paywall

`ezsubscriptions.showPaywall({ product: "premium" })` opens Ezoic's pre-built paywall and checkout experience. For this example, `premium` is a product you create in your [Ezoic dashboard](/docs/subscriptions/products/) with one or more prices; the `product` value is its **product handle**, not the public display name.

Call it whenever a visitor does not have access. It is safe to call on every page load: if the visitor already holds the product — or the handle is unknown or has no active prices — the widget simply renders nothing.

To sell one specific price directly from your own button — skipping the paywall's price selection — use `ezsubscriptions.openCheckout({ price: "premium-monthly" })`.

For all `showPaywall()` options and callbacks, see the [JavaScript API Reference](/docs/subscriptions/api-reference/#showpaywalloptions).

## Build Your Own Pricing UI

If you want full control over how plans and prices look, build your own pricing cards and hand checkout off to Ezoic with `openCheckout({ price })`. A "buy" button only needs to know its **price handle** (set on your product in the [Ezoic dashboard](/docs/subscriptions/products/)) — the widget then collects payment and establishes access, so you never touch card data or build a checkout form.

```html
<div class="plans">
  <button class="plan" data-price="premium-monthly">Monthly — $4.99</button>
  <button class="plan" data-price="premium-annual">Annual — $49.99</button>
</div>

<script>
  window.ezsubscriptions = window.ezsubscriptions || {};
  ezsubscriptions.cmd = ezsubscriptions.cmd || [];
  ezsubscriptions.cmd.push(function (api) {
    document.querySelectorAll(".plan").forEach(function (button) {
      button.addEventListener("click", function () {
        api.openCheckout({
          price: button.dataset.price,
        });
      });
    });
    // Reload once checkout completes and access is established. access:change
    // fires on every completion path, including the load after a 3DS redirect.
    api.on("access:change", async function () {
      const access = await api.hasAccess("premium");
      if (access.decision === "allowed") {
        window.location.reload();
      }
    });
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

`openCheckout({ price })` launches checkout directly for that price, skipping the product and price pickers. A signed-in visitor goes straight to payment. So does an anonymous visitor when [expedited checkout](/docs/subscriptions/visitor-authentication/#expedited-checkout) is on (the default): they enter their email right on the payment screen. With expedited checkout off, they first sign in, create an account, or continue as a guest when guest checkout is enabled. Once payment completes, the widget establishes access.

The example reloads the page on success because it's the simplest option. A completed checkout fires [`access:change`](/docs/subscriptions/publisher-managed-access/#reacting-to-access-changes), so you can instead re-reveal content or remove ads in place — no reload required. Use the `access:change` listener rather than the deprecated `onSuccess` callback, which does not survive a login detour or 3DS redirect (see [Paywall and Checkout Methods](/docs/subscriptions/api-reference/#paywall-and-checkout-methods)).


The checkout `openCheckout({ price })` opens inherits the price's parent product's Appearance settings, so it matches your paywall by default. To adjust it further, set [CSS variables](/docs/subscriptions/paywall-appearance/#custom-styling-with-css-variables) on `#ezoic-sm`.


Use `showPaywall({ product })` when you want Ezoic to render the plan and price options for you, and `openCheckout({ price })` when you render them yourself. To sell a one-time purchase scoped to a single item — such as unlocking one article, a downloadable file, or a single tool or feature — pass an `item` key with `openCheckout({ price, item })`, then check it later with `hasPurchased({ item })`. See [Protecting Full Content](#protecting-full-content) before delivering the item — a hidden element alone does not protect it. See [JavaScript API Reference](/docs/subscriptions/api-reference/#opencheckoutoptions) for all options.

## Add a Log In Link

Returning subscribers should be able to sign in from your own navigation, not only by hitting a paywall. Add a login entry point and the widget opens the right sign-in experience for your authentication mode — Ezoic's login screen on Ezoic visitor accounts, or your own login on bring your own login.

The simplest option is declarative: add `data-ezoic-login` to any element, and the widget opens login when it is clicked.

```html
<button type="button" data-ezoic-login>Log in</button>
```

Or call the API from your own handler:

```html
<button id="log-in" type="button">Log in</button>

<script>
  window.ezsubscriptions = window.ezsubscriptions || {};
  ezsubscriptions.cmd = ezsubscriptions.cmd || [];
  ezsubscriptions.cmd.push(function (api) {
    document.getElementById("log-in").addEventListener("click", function () {
      api.login();
    });
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

On Ezoic visitor accounts, a successful login fires `access:change` and your gated content unlocks in place — no page reload (pair it with an [`access:change`](/docs/subscriptions/publisher-managed-access/) listener to re-reveal content). On bring your own login, `login()` calls your adapter's `goToLogin()`; after the visitor returns signed in, call `ezsubscriptions.authChanged()` as usual — see [Visitor Authentication](/docs/subscriptions/visitor-authentication/#bring-your-own-login) for that setup. See [`login()`](/docs/subscriptions/api-reference/#loginoptions) for the full options.

## Add a Log Out Link

Give signed-in subscribers a way to sign out from your navigation too. Add a log-out entry point the same way — declaratively with `data-ezoic-logout`, or by calling the API:

```html
<button type="button" data-ezoic-logout>Log out</button>
```

```html
<button id="log-out" type="button">Log out</button>

<script>
  window.ezsubscriptions = window.ezsubscriptions || {};
  ezsubscriptions.cmd = ezsubscriptions.cmd || [];
  ezsubscriptions.cmd.push(function (api) {
    document.getElementById("log-out").addEventListener("click", function () {
      api.logout();
    });
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

`logout()` ends the visitor's access session and re-locks gated content in place. On Ezoic visitor accounts it also signs the visitor out of their Ezoic visitor account. On bring your own login, sign the visitor out of your own system first, then call `logout()`. For the full picture across both modes, see [Signing Out](/docs/subscriptions/visitor-authentication/#signing-out); for the method itself, see [`logout()`](/docs/subscriptions/api-reference/#logout).


If you removed ads for the subscriber with [`disableAds()`](#removing-ads-for-subscribers), signing out restores them — see [Signing Out](/docs/subscriptions/visitor-authentication/#signing-out).


## Add a Manage Subscription Link

Give signed-in subscribers a way to manage their billing — update cards, view receipts, cancel — from your own navigation. Wire a link to `ezsubscriptions.openAccountPortal()`, which opens the [subscriber portal](/docs/subscriptions/subscriber-experience/#subscriber-portal) in a new tab.

```html
<button id="manage-subscription" type="button">Manage subscription</button>

<script>
  window.ezsubscriptions = window.ezsubscriptions || {};
  ezsubscriptions.cmd = ezsubscriptions.cmd || [];
  ezsubscriptions.cmd.push(function (api) {
    document.getElementById("manage-subscription").addEventListener("click", function () {
      api.openAccountPortal();
    });
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

On Ezoic visitor accounts, a signed-in member opens the portal already signed in. On bring your own login, a signed-in member also opens it already signed in when your adapter implements [`getIdentityToken()`](/docs/subscriptions/visitor-authentication/#getidentitytoken); otherwise, and for guests, the portal runs its own sign-in. Show this link only to members — gate it behind an [`hasAccess(...)`](/docs/subscriptions/publisher-managed-access/) check or your own signed-in state. See [`openAccountPortal()`](/docs/subscriptions/api-reference/#openaccountportal).

## Protecting Full Content

The widget decides whether a visitor has access and provides the paywall and checkout experience. Your site still controls what content is sent to the browser and when full content is revealed — the same principles apply whether you're gating an article body, a downloadable file such as a PDF, or a premium tool or feature.

For casual paid access, a soft gate may be enough: include the full content in the HTML and hide it until `hasAccess(...)` — or, for a one-time item purchase, `hasPurchased({ item })` — returns `allowed`.


Selling a file download? Hiding an element doesn't hide its `href`, so a link straight to the file's real URL can be read from the page source. For a low-stakes download that's often fine. If the file is worth protecting, point `href` at your own endpoint and verify the purchase server-side with the [Server-to-Server REST API](/docs/subscriptions/rest-api/) before releasing it, as described below.


For stronger protection, use a hybrid approach: send only the teaser in the initial HTML, then fetch the protected body from your own endpoint once access is allowed.

```html
<article>
  <div class="article-teaser"><!-- headline + teaser, sent to everyone --></div>
  <div data-premium-content hidden></div>
</article>

<script>
  window.ezsubscriptions = window.ezsubscriptions || {};
  ezsubscriptions.cmd = ezsubscriptions.cmd || [];
  ezsubscriptions.cmd.push(async function () {
    const access = await ezsubscriptions.hasAccess("premium");
    if (access.decision !== "allowed") {
      ezsubscriptions.showPaywall({ product: "premium" });
      return;
    }

    // Full body was never in the page source — fetch it now that access is allowed.
    const body = await fetch("/api/articles/12345/body").then((r) => r.text());
    const container = document.querySelector("[data-premium-content]");
    container.innerHTML = body;
    container.hidden = false;
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

Do not rely on a visual overlay alone if the full article is already visible in the page source and you need stronger content protection.

For the strongest protection, make the access decision on your server before it sends the protected body, so the content never leaves your backend for an unentitled reader. Your endpoint verifies the reader with the [Server-to-Server REST API](/docs/subscriptions/rest-api/) — `product=` for a subscription, or `item=` for a one-time purchase — rather than trusting the browser. Keep any response that returns the protected body private to the visitor session — see [Caching](/docs/subscriptions/seo-paywalling-best-practices/#caching).

## SEO Markup For Gated Articles

If you gate article or page content for readers, those pages should include paywalled-content structured data so search engines understand that subscriber-only content is intentionally paywalled. Follow [SEO and Paywalling Best Practices](/docs/subscriptions/seo-paywalling-best-practices/) before going live.

This applies only when you gate readable article or page content. Ad removal, tools, and other non-article products do not need paywalled-content markup.

## Single-Page Apps and Frameworks (React, Vue, Next.js)

Load the script once. In a single-page app or a framework like React, Vue, or Next.js, add `https://sm.ezoic.com/min.js` a single time — for example in your root layout, `app` shell, or Next.js `_document` — not on every route. The widget bootstraps once: on that first load it also completes a returning subscriber's magic-link sign-in and resumes any checkout that was interrupted by a redirect.

Because the widget has no client-side router hook, re-run your own access logic when the view changes:

- On a client-side route change, call `hasAccess(...)` again for the new view, then reveal content, remove ads, or open the paywall as needed.
- Subscribe to [`access:change`](/docs/subscriptions/publisher-managed-access/#reacting-to-access-changes) so gated UI updates in place when the visitor logs in or completes checkout.
- Keep the work you queue on `ezsubscriptions.cmd` idempotent — running it again for the same view should be safe.

Magic-link returns and checkout resumption run during a full page load, so let those links land on a real URL rather than intercepting them in your client router. The widget strips the one-time token from the address bar after it runs.

## Handling Script or Access Failures

The onsite script and its access checks depend on the network. Decide how your page should behave if either fails, based on your use case:

- **Removing ads — fail open.** If the script does not load or an access check rejects, leave the visitor's experience unchanged; they simply keep seeing ads. Don't hide content or block the page waiting on the widget.
- **Gating content — fail closed.** If you're protecting paid content, treat a failed or missing access check as *no access* and keep the content hidden rather than revealing it on error.

`hasAccess(...)` and `hasPurchased(...)` return immediately for anonymous visitors with no network request, but can reject for a signed-in visitor when the request fails. Wrap them in `try` / `catch` and apply the posture above:

```html
<script>
  window.ezsubscriptions = window.ezsubscriptions || {};
  ezsubscriptions.cmd = ezsubscriptions.cmd || [];
  ezsubscriptions.cmd.push(async function () {
    try {
      const access = await ezsubscriptions.hasAccess("remove-ads");
      if (access.decision === "allowed") {
        ezsubscriptions.disableAds();
      }
    } catch (err) {
      // Fail open for ad removal: leave ads in place on error.
      console.error("access check failed", err);
    }
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

If the script itself never loads, callbacks queued on `ezsubscriptions.cmd` never run — so for gated content, start hidden and reveal only on an `allowed` decision, never the other way around.

## Stripe Content Security Policy

If your site uses a strict Content Security Policy, allow Stripe so checkout can load. Stripe's current guidance can change, so use Stripe's official CSP documentation as the source of truth: [Content Security Policy](https://docs.stripe.com/security/guide#content-security-policy).

Common allowlist entries include:

- `script-src https://js.stripe.com https://*.js.stripe.com`
- `frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com`
- `connect-src https://api.stripe.com https://*.stripe.com`
- `img-src https://*.stripe.com`

## Dashboard Prerequisites

Before checkout can sell access, the site needs an active payment setup and a product with at least one price. See [Payment Setup](/docs/subscriptions/payment-setup/) and [Products, Prices, and Paid Access](/docs/subscriptions/products/) for those setup details.

