View as Markdown

Onsite Script 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:

<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, a few things work differently — you register an auth adapter and signal sign-in changes yourself. See Visitor Authentication for the BYO-specific handling that pairs with 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.

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

    // 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. 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 first.

To sell the ad-free product to visitors who don't have it yet, open the paywall from your own "Remove ads" button or link with ezsubscriptions.showPaywall({ product: "remove-ads" }).

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.

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.

<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 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 the full showPaywall() options, callback hooks, and return values, see the JavaScript API Reference.

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 }) — we handle payments and managed visitor entitlements behind the scenes. A "buy" button only needs to know its price handle (set on your product in the Ezoic dashboard) — the widget then collects payment and establishes access, so you never touch card data or build a checkout form.

<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,
          onSuccess: function () {
            window.location.reload();
          },
        });
      });
    });
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>

openCheckout({ price }) launches checkout directly for that price — no product or price picker. If the visitor is not already signed in, they sign in or — when guest checkout is enabled — continue as a guest, then pay, and the widget establishes access; your onSuccess callback runs once access is confirmed.

The example reloads the page on success because it's the simplest option. A completed checkout also fires access:change, so you can instead re-reveal content or remove ads in place from your onSuccess handler or an access:change listener — no reload required.

Since your pricing UI replaces showPaywall(), the checkout openCheckout({ price }) opens always renders in the Classic/Light default — it does not pick up the product's Appearance settings. To keep it on-brand, set CSS variables on #ezoic-sm; those still apply.

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 JavaScript API Reference for all options.

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.

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

Or call the API from your own handler:

<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 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 for that setup. See login() for the full options.

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:

<button type="button" data-ezoic-logout>Log out</button>
await ezsubscriptions.logout();

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 behind the scenes. 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; for the method itself, see logout().

If you removed ads for the subscriber with disableAds(), signing out restores them. Because ad suppression is applied on Ezoic's servers, a page refresh may be needed before ads begin showing again.

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.

For casual paid access, a soft gate may be enough: include the full content in the HTML and hide it until hasAccess(...) returns allowed.

For stronger protection, use a hybrid approach:

  1. Show the headline and teaser to everyone.
  2. Check access with ezsubscriptions.hasAccess(...).
  3. Fetch or render the full body only after access is allowed.

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.

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 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 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:

<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.

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 and Products, Prices, and Paid Access for those setup details.