# JavaScript API Reference

Source: https://docs.ezoic.com/docs/subscriptions/api-reference/


The onsite script exposes `window.ezsubscriptions` after `https://sm.ezoic.com/min.js` loads. Because the script loads asynchronously, put API calls inside the `cmd` queue:

```html
<script>
  window.ezsubscriptions = window.ezsubscriptions || {};
  ezsubscriptions.cmd = ezsubscriptions.cmd || [];
  ezsubscriptions.cmd.push(function (api) {
    // api is the resolved ezsubscriptions API.
  });
</script>
<script src="https://sm.ezoic.com/min.js" async defer></script>
```

Methods that return promises can reject if a network request fails. Wrap access checks and checkout-launching calls in `try` / `catch` when your page needs a custom fallback.

Most calls take a **product handle** or a **price handle** — the stable keys you author on a product and its prices in your Ezoic dashboard. See [Products, Prices, and Paid Access](/docs/subscriptions/products/) for how those keys are created.


Examples here assume the default **Ezoic visitor accounts**. A few methods behave differently under [bring your own login](/docs/subscriptions/visitor-authentication/#bring-your-own-login) — notably `login()`, `logout()`, `initialize()`, and `authChanged()`. See [Visitor Authentication](/docs/subscriptions/visitor-authentication/) for the BYO handling.


## Readiness

### `ezsubscriptions.cmd`

Queues callbacks until the script is ready. Each callback receives the resolved API object.

```javascript
ezsubscriptions.cmd.push(function (api) {
  api.showPaywall({ product: "remove-ads" });
});
```

Callbacks pushed after the script is ready run immediately.

### `ezsubscriptions.ready`

Boolean value that is `true` after the API is ready.

```javascript
if (window.ezsubscriptions?.ready) {
  window.ezsubscriptions.showPaywall({ product: "remove-ads" });
}
```

## Access Methods

### `hasAccess(query)`

```typescript
hasAccess(query: string | { product: string }): Promise<AccessResponse>
```

Checks whether the current visitor holds an active entitlement for a product. Pass a **product handle** — the whole-product access key, not a price handle or an item key.

```javascript
const access = await ezsubscriptions.hasAccess("remove-ads");
```

You can pass the product handle directly or as an object:

```javascript
await ezsubscriptions.hasAccess("remove-ads");
await ezsubscriptions.hasAccess({ product: "remove-ads" });
```

Parameters:

- `query: string | { product: string }` — the product handle authored on your product.

Returns:

```typescript
{
  decision: "allowed" | "denied" | "login_required" | "expired" | "revoked" | "unknown_product",
  reasonCode: string
}
```

Show protected content only when `decision` is `allowed`. Treat every other decision as no current access.

Anonymous visitors return `login_required` without a network request.


Call `hasAccess` when your page or view renders — not on a timer. Every access change the widget can observe (login, logout, completed checkout) is pushed to you through [`access:change`](#onevent-handler--offevent-handler); polling with `setInterval` only adds requests without catching anything the event misses.


This promise can reject if the visitor is signed in and the access request fails.

### `hasPurchased(query)`

```typescript
hasPurchased(query: { item: string } | { page: true }): Promise<AccessResponse>
```

Checks whether the current visitor has bought a one-time per-item purchase. The `item` is a site-wide key matched on its own — no price needed. It is the read counterpart to `openCheckout({ price, item })` and `showPaywall({ product })`.

```javascript
// A single item you name:
const access = await ezsubscriptions.hasPurchased({ item: "article-12345" });

// The current article (a price set to unlock the current article automatically):
const access = await ezsubscriptions.hasPurchased({ page: true });

if (access.decision === "allowed") {
  revealArticle();
}
```

Parameters:

- `query: { item: string } | { page: true }`
- `query.item` — the item key you passed to `openCheckout({ price, item })` / `showPaywall({ product, item })`. It is matched exactly, across the visitor's one-time purchases on the site, independent of which price sold it.
- `query.page` — pass `{ page: true }` instead of an `item` to check the current page. Ezoic resolves the same page key it stamped at checkout for a price set to unlock *the current article automatically*, so you never compute it yourself.

Returns the same [`AccessResponse`](#hasaccessquery) shape as `hasAccess`.

Anonymous visitors return `login_required` without a network request. A missing `item` returns `denied` without a network request.

### `getProducts()`

```typescript
getProducts(): Promise<string[]>
```

Returns the product handles the visitor currently holds.

```javascript
const products = await ezsubscriptions.getProducts();
if (products.includes("pro")) {
  enableProFeatures();
}
```

Parameters: none.

Returns:

```typescript
string[]
```

Anonymous visitors return `[]` without a network request.

This promise can reject if the visitor is signed in and the request fails.

### `getPurchases()`

```typescript
getPurchases(): Promise<Purchase[]>
```

Returns the visitor's purchases — for building a "your purchases", downloads, or library page. Unlike `getProducts()`, the list **includes** one-time per-item purchases.

```javascript
const purchases = await ezsubscriptions.getPurchases();
purchases
  .filter((purchase) => purchase.status === "active")
  .forEach((purchase) => renderLibraryRow(purchase));
```

Parameters: none.

Returns:

```typescript
Array<{
  productKey?: string,
  item?: string,
  status: string,
  expiresAt?: string
}>
```

- `productKey` is the product the purchase belongs to. It is omitted if that product was removed.
- `item` is set only for a one-time per-item purchase.
- `status` lets you filter active access from expired or revoked entitlements.
- `expiresAt` is set only for time-limited access; lifetime access omits it.

Anonymous visitors return `[]` without a network request. This promise can reject if the visitor is signed in and the request fails.

## Ad Removal Methods

An ad-free experience is the most common paid benefit. After `hasAccess(...)` returns `allowed`, call `disableAds()`; when access is lost, call `allowAds()`. The widget owns the whole mechanism — you never touch ad placeholders or cookies. See [Removing Ads for Subscribers](/docs/subscriptions/site-integration/#removing-ads-for-subscribers) for the full pattern.

### `disableAds()`

```typescript
disableAds(): Promise<void>
```

Suppresses **every** ad format for an entitled visitor — display, floating video, and interstitials. The widget sets a signed cookie that Ezoic reads server-side, so ads never load rather than being torn down after they render.

```javascript
const access = await ezsubscriptions.hasAccess("remove-ads");
if (access.decision === "allowed") {
  ezsubscriptions.disableAds();
}
```

Parameters: none.

Returns:

```typescript
Promise<void>
```

On the page where access is first gained (typically right after checkout or login), `disableAds()` reloads once so the server can apply suppression; 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. If the visitor is not entitled or the cookie cannot be set, it fails open and leaves ads in place.

### `allowAds()`

```typescript
allowAds(): void
```

Removes the ad-suppression cookie when access is lost — logout, an expired subscription, or a revoked entitlement. It does not reload; ads return on the visitor's next page view.

```javascript
const access = await ezsubscriptions.hasAccess("remove-ads");
if (access.decision !== "allowed") {
  ezsubscriptions.allowAds();
}
```

Parameters: none.

Returns: `void`.

## Paywall and Checkout Methods


**`onSuccess` is deprecated on `showPaywall` and `openCheckout`.** The callback is a closure on the checkout instance that opened, so anything that takes the visitor away from the page — a full-page 3D Secure redirect, a bring-your-own-login detour — drops it: it never fires when the visitor returns. Anything that **must** happen when access is granted belongs in an [`access:change`](#onevent-handler--offevent-handler) listener with a fresh `hasAccess(...)` / `hasPurchased(...)` check — it fires on every completion path, including the page load after a redirect. `onCancel` and `onError` are unaffected: a dismissal or a failed attempt only ever happens on the page that is showing checkout.



With [expedited checkout](/docs/subscriptions/visitor-authentication/#expedited-checkout) on (the default), an anonymous guest pays on a single screen (wallet buttons where available, an email field, and the card form) instead of stepping through a separate sign-in flow first.


### `showPaywall(options)`

```typescript
showPaywall(options: {
  product: string;
  item?: string;
  dismissible?: boolean;
  email?: string;
  onSuccess?: (result: CheckoutResult) => void;
  onCancel?: () => void;
  onError?: (error: CheckoutError) => void;
}): Promise<void>
```

Opens Ezoic's pre-built paywall and checkout experience for a product and its active prices. It takes a **product handle** (what you sell), not a price handle; `item` scopes a one-time single-item purchase and is matched later by `hasPurchased({ item })`.

```javascript
await ezsubscriptions.showPaywall({
  product: "remove-ads",
  onError: function (error) {
    console.log(error.message);
  },
});
```

Parameters:

- `options?: object`
- `options.product: string` — the product handle to present. **Required**: without it the call does nothing.
- `options.item?: string` — your item key for a one-time *single-item* price that sells [an item your site provides](/docs/subscriptions/products/#a-single-item-your-site-provides): the purchase is scoped to that item and matched later by `hasPurchased({ item })`. These prices don't appear in the paywall unless you pass an `item` — without one there is nothing to scope the purchase to. Prices that unlock *the current article automatically* ignore `options.item` — they always appear and key the purchase to the page the paywall is shown on.
- `options.dismissible?: boolean` — when `false`, the paywall is blocking: no close button, no Escape, and `onCancel` cannot fire. Defaults to `true` (dismissible).
- `options.email?: string` — prefills the guest email field, for example with an address your page already collected. The visitor still confirms it before payment.
- `options.onSuccess?: (result: CheckoutResult) => void` — **deprecated**: fires after checkout completes and access is established, but does not survive a page redirect. Use `access:change` instead — see the note above.
- `options.onCancel?: () => void` — fires when a dismissible paywall is closed before checkout completes.
- `options.onError?: (error: CheckoutError) => void` — fires when a checkout attempt fails. It may fire more than once if the visitor retries.

Returns:

```typescript
Promise<void>
```

The widget loads the product's active prices, re-checks visitor access, and renders nothing if the visitor already holds the product or already bought the item in play, or if the product has no active prices to sell. If the product config cannot be loaded, the widget shows a dismissible error message and fires `onError`.

### `openCheckout(options)`

```typescript
openCheckout(options: {
  price: string;
  item?: string;
  dismissible?: boolean;
  email?: string;
  onSuccess?: (result: CheckoutResult) => void;
  onCancel?: () => void;
  onError?: (error: CheckoutError) => void;
}): Promise<void>
```

Launches checkout directly for a single **price handle**, skipping the paywall's product and price selection. Use it for custom "buy" buttons wired to a specific price. `item` scopes a one-time single-item purchase; it is only meaningful for an item-scoped one-time price.

```javascript
await ezsubscriptions.openCheckout({
  price: "remove-ads-monthly",
});
```

To react to a completed checkout — for example, reload the page — subscribe to [`access:change`](#onevent-handler--offevent-handler) and re-check access; see the note above for why `onSuccess` is deprecated.

Parameters:

- `options: object`
- `options.price: string` — the price handle to charge. **Required**: without it the call does nothing.
- `options.item?: string` — scopes a one-time per-item purchase; the value is stored verbatim as the entitlement key and matched exactly by `hasPurchased({ item })`. Optional for a product-scoped price, but **required for an item-scoped price** — checkout is rejected without it.
- `options.dismissible?: boolean` — when `false`, checkout is blocking: no close button, no Escape, and `onCancel` cannot fire. Defaults to `true` (dismissible).
- `options.email?: string` — prefills the guest email field, for example with an address your page already collected. The visitor still confirms it before payment.
- `options.onSuccess?: (result: CheckoutResult) => void` — **deprecated** — see the note above
- `options.onCancel?: () => void`
- `options.onError?: (error: CheckoutError) => void`

Returns:

```typescript
Promise<void>
```

## Login and Logout

### `login(options)`

```typescript
login(options?: { dismissible?: boolean }): Promise<void>
```

Opens a login screen so a returning subscriber can sign in **without first hitting a paywall** — wire it to a "Log in" link in your own navigation. On a successful login the widget establishes the access session and fires `access:change`, so gated content unlocks in place with no page reload.

```javascript
document.getElementById("log-in").addEventListener("click", function () {
  ezsubscriptions.login();
});
```

`login()` is **mode-aware**:

- **Ezoic visitor accounts** — opens Ezoic's login screen in the widget (a one-time email sign-in link, Continue with Google when available, and password sign-in for visitors who set one).
- **Bring your own login** — forwards to your adapter's `goToLogin()`; the widget renders no login UI of its own. See [Visitor Authentication](/docs/subscriptions/visitor-authentication/#bring-your-own-login) for the adapter setup and the `authChanged()` call that goes with it.

You can also trigger it declaratively, with no JavaScript, using the [`data-ezoic-login`](/docs/subscriptions/site-integration/#add-a-log-in-link) attribute.

Parameters:

- `options?: object`
- `options.dismissible?: boolean` — whether the visitor can close the login screen. Defaults to `true`.

Returns:

```typescript
Promise<void>
```

If the visitor already has an active session, `login()` does nothing (and re-fires `access:change`). On a bring-your-own-login domain with no registered adapter, it is a no-op.

### `logout()`

```typescript
logout(): Promise<void>
```

Ends the visitor's access session — the counterpart to `login()`. It clears the Subscriptions access session and fires `access:change` so gated content re-locks in place. Wire it to a "Log out" link, or use the declarative [`data-ezoic-logout`](/docs/subscriptions/site-integration/#add-a-log-out-link) attribute.

```javascript
document.getElementById("log-out").addEventListener("click", function () {
  ezsubscriptions.logout();
});
```

Call `logout()` only when the visitor explicitly asks to sign out — never in response to an access check. A `denied`, `expired`, or `revoked` decision from [`hasAccess(...)`](#hasaccessquery) means "no access to this product", not "should be signed out". A signed-in visitor without the product still needs their session to buy it: signing them out mid-flow revokes the session their in-progress checkout depends on, and the purchase fails.


If you granted an ad-free experience with `ezsubscriptions.disableAds()`, `logout()` restores ads too — see [Signing Out](/docs/subscriptions/visitor-authentication/#signing-out).


`logout()` is **mode-aware**:

- **Ezoic visitor accounts** — also signs the visitor out of their Ezoic visitor account behind the scenes, so they are not silently re-authenticated on the next page load.
- **Bring your own login** — the widget cannot end your own login session, so sign the visitor out of your system first, then call `logout()`. Calling `authChanged()` after your sign-out also clears the access session, but unlike `logout()` it does not restore ads.

Returns:

```typescript
Promise<void>
```

See [Signing Out](/docs/subscriptions/visitor-authentication/#signing-out) for the full lifecycle across both modes. The subscriber portal (`subscriber.ezoic.com`) has its own separate sign-out.

## Authentication Methods

These methods apply to **Bring Your Own Login** integrations. See [Visitor Authentication](/docs/subscriptions/visitor-authentication/) for the full setup, including the auth adapter contract. Sites on Ezoic visitor accounts do not need them.

### `initialize(config)`

```typescript
initialize(config: { auth: AuthAdapter }): void
```

Registers your auth adapter so the widget can resolve the logged-in visitor's identity from your own login system.

```javascript
ezsubscriptions.initialize({
  auth: {
    getUserEmail: () => currentUser?.email ?? null,
    goToLogin: () => location.assign("/login"),
    goToCreateAccount: () => location.assign("/signup"),
  },
});
```

Parameters:

- `config: { auth: AuthAdapter }` — the adapter object. See [Visitor Authentication](/docs/subscriptions/visitor-authentication/) for the adapter method contract.

Returns: `void`.

Safe to call repeatedly; a later valid call replaces the adapter.

### `authChanged()`

```typescript
authChanged(): Promise<void>
```

Signals that your auth state changed — a login, logout, account switch, or async session restore. The widget re-resolves identity through the adapter, refreshes or clears its session, and fires `access:change`.

```javascript
await ezsubscriptions.authChanged();
```

Parameters: none.

Returns:

```typescript
Promise<void>
```

Call this instead of re-running `initialize` for runtime auth changes.

### `on(event, handler)` / `off(event, handler)`

```typescript
on(event: "access:change", handler: () => void): () => void
off(event: "access:change", handler: () => void): void
```

Subscribe to `access:change` so gated UI re-renders when the visitor's access may have changed (login, logout, account switch, completed checkout).

```javascript
const unsubscribe = ezsubscriptions.on("access:change", async function () {
  const access = await ezsubscriptions.hasAccess("remove-ads");
  if (access.decision === "allowed") {
    ezsubscriptions.disableAds();
  } else {
    ezsubscriptions.allowAds();
  }
});

// Later, on cleanup:
unsubscribe();
```

Parameters:

- `event: "access:change"` — the only supported event.
- `handler: () => void` — runs when access may have changed. Takes no arguments; re-read access inside it.

`on` returns an unsubscribe function. `off(event, handler)` detaches a handler by reference and returns `void`.

## Analytics Methods

### `trackEvent(name, value)`

```typescript
trackEvent(name: string, value?: string): void
```

Emits a custom analytics event — for example `trackEvent("cta_click", "homepage-hero")`. Custom events appear in your dashboard's Subscriptions analytics. Fire-and-forget: safe to call at any time, and it never throws.

```javascript
ezsubscriptions.trackEvent("cta_click", "homepage-hero");
```

Parameters:

- `name: string` — the event name.
- `value?: string` — an optional value to attach.

Returns: `void`.

## Account Methods

### `openAccountPortal()`

```typescript
openAccountPortal(): void
```

Opens the subscriber portal at `https://subscriber.ezoic.com` in a new tab, where a logged-in member manages payment methods, receipts, and subscriptions. Wire this to a "Manage subscription" link for members.

```javascript
document.getElementById("manage-subscription").addEventListener("click", function () {
  ezsubscriptions.openAccountPortal();
});
```

Parameters: none.

Returns: `void`.

On **Ezoic visitor accounts**, a signed-in member opens the portal already signed in, with no additional login. 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); without it, and for guest or unrecognized visitors, this is plain navigation and the portal runs its own sign-in.

### `getSessionToken()`

```typescript
getSessionToken(): string | null
```

Returns the signed-in reader's session token (a domain-scoped JWT), or `null` when the reader is signed out or the session has expired.

This is the client half of the [Server-to-Server REST API](/docs/subscriptions/rest-api/). On **Ezoic-visitor-account** sites your origin server never sees the reader's email, so you relay this token instead: read it on the page, send it to your backend, and forward it to the REST API as the `X-Ezoic-Reader-Token` header.

```javascript
ezsubscriptions.cmd.push(function (api) {
  const token = api.getSessionToken();
  if (token) {
    fetch("/my-backend/unlock", { headers: { "X-Reader-Token": token } });
  }
});
```

Parameters: none.

Returns:

```typescript
string | null
```

The token is short-lived and scoped to the current domain, so read it per request rather than caching it. See the REST API's [Reader Identity](/docs/subscriptions/rest-api/#reader-identity) section for the full relay flow.

## Donation Methods

### `openDonation(options)`

```typescript
openDonation(options?: {
  amountCents?: number;
  productId?: string;
  onSuccess?: (result: CheckoutResult) => void;
  onCancel?: () => void;
  onError?: (error: CheckoutError) => void;
}): Promise<void>
```

Opens the donation checkout. This is the single donation entry point: it loads your donation settings the first time it's called, so no setup call is required and it works no matter when you call it. On a site with no donations configured it does nothing (and logs a warning). A `[data-ezoic-donate]` element opens the same dialog with no JavaScript, and accepts `data-ezoic-product-id` and `data-ezoic-amount-cents` attributes matching the `productId` and `amountCents` options:

```html
<button type="button" data-ezoic-donate data-ezoic-amount-cents="2500">Give $25</button>
```

```javascript
ezsubscriptions.openDonation({
  amountCents: 2500,
  onSuccess: function (result) {
    console.log("Donation complete", result.amountCents);
  },
});
```

Parameters:

- `options?: object`
- `options.amountCents?: number` — preselected donation amount in the smallest unit of the currency the visitor's picker presents (cents for USD/EUR). For example, `2500` means `$25.00` for a visitor seeing dollars. The configured minimum still applies.
- `options.productId?: string` — optional donation product ID. Most donation integrations should omit this because a site has one active donation in the current dashboard flow.
- `options.onSuccess?: (result: CheckoutResult) => void` — fires after donation checkout completes. Donations grant no entitlement, so this is the only completion signal — there is no `access:change` to key off. Treat it as best-effort: a full-page 3D Secure redirect drops it.
- `options.onCancel?: () => void` — fires when the visitor closes the donation dialog before checkout completes.
- `options.onError?: (error: CheckoutError) => void` — fires when a checkout attempt fails. It may fire more than once if the visitor retries.

Returns:

```typescript
Promise<void>
```

If `amountCents` is missing, invalid, or below the configured minimum, the widget falls back to the normal donation picker.

### `closeDonation()`

```typescript
closeDonation(): void
```

Closes the donation dialog.

```javascript
ezsubscriptions.closeDonation();
```

Parameters: none.

Returns: `void`.

## Hiding the Widget

### `hide()`

```typescript
hide(): void
```

Closes the paywall, checkout, donation, and login screens.

```javascript
ezsubscriptions.hide();
```

Parameters: none.

Returns: `void`.

## Callback Payloads

### `CheckoutResult`

Passed to `onSuccess`.

```typescript
{
  productType?: "access" | "donation",
  productExternalId?: string,
  priceExternalId?: string,
  amountCents?: number,
  product?: string,
  price?: string,
  item?: string
}
```

- `amountCents` is the requested base amount in cents. Taxes, discounts, or payment-provider adjustments can change the final charged total. Set for `showPaywall` checkouts (including per-item purchases); omitted for `openCheckout`, along with `productType`, `productExternalId`, and `priceExternalId`.
- `productType` is `access` for any subscription or one-time product (legacy type names are normalized to `access` server-side) and `donation` for donations.
- `product` is set when checkout was opened with `showPaywall({ product })`.
- `price` is set when checkout was opened with `openCheckout({ price })`.
- `item` is set for a one-time per-item purchase — `openCheckout({ price, item })`, `showPaywall({ product, item })`, or a paywall price that unlocks the current article automatically.
- A callback that throws is logged and does not break the widget.

### `CheckoutError`

Passed to `onError`.

```typescript
{
  message: string,
  code?: string
}
```

Checkout stays open for retry after an error, so `onError` can fire more than once. `code` is the payment provider's error code (for example `card_declined`).

