# Troubleshooting

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


Use this guide when the onsite script, paywall, checkout, ad removal, donation dialog, SEO markup, or subscriber portal does not behave as expected. To reproduce an issue without affecting live subscribers or revenue, use [Testing with Test Mode](/docs/subscriptions/testing-sandbox/).

## Common Causes

Review these configuration details before investigating a specific symptom:

- **Payment setup can take live payments.** Until it can, the purchase fails when the visitor tries to pay — see [Checkout Does Not Open or Has No Products to Sell](#checkout-does-not-open-or-has-no-products-to-sell).
- **The product is live with an active price.** A draft, disabled, or archived product — or one with no active price — cannot sell access.
- **Your code uses the product handle, not the display name.** `hasAccess(...)` and `showPaywall({ product })` take the **product handle** from your dashboard, not the public product name.
- **The script is present on the page.** `https://sm.ezoic.com/min.js` needs to load on the pages where subscriptions or donations run.
- **Your integration runs inside `ezsubscriptions.cmd`.** The script loads asynchronously, so API calls made outside the [command queue](/docs/subscriptions/api-reference/#ezsubscriptionscmd) can run before the widget is ready.

## Setup and Dashboard

### Checkout Does Not Open or Has No Products to Sell

Checkout requires a payment setup that can take live payments. Without one, `showPaywall({ product })` can still list your prices, but the purchase fails when the visitor tries to pay.

Review your payment status in the dashboard (**Subscriptions → Overview → Current status**, or the **Payments** panel):

- **Your own Stripe account (Stripe Connect):** the account must have **charges enabled**. If Stripe still shows *Requirements due* or *Details submitted* pending review, complete the requested steps in Stripe and refresh the status in Ezoic. See [Stripe Account Status](/docs/subscriptions/payment-setup/#stripe-account-status).
- **Ezoic-managed payments:** the application can be **Reviewing**, **Not approved**, or **Revoked**. While a site is under review, you can finish building your product, add the script, and [test the flow in test mode](/docs/subscriptions/testing-sandbox/), but **live payments become available only once your site is approved**. If it was not approved, connect your own Stripe account instead. See [Ezoic-Managed Payments](/docs/subscriptions/payment-setup/#ezoic-managed-payments).

You must also have at least one **active product with an active price**. See [Payment Setup](/docs/subscriptions/payment-setup/) and [Products, Prices, and Paid Access](/docs/subscriptions/products/).

### Checkout Fails on a Bring-Your-Own-Login Site

If your domain is set to **bring your own login** and you have not connected your login system to the widget, checkout cannot run, even though the paywall may still appear. This is a one-time configuration step, not something a visitor triggers.

On a bring-your-own-login domain, you must register an auth adapter with [`ezsubscriptions.initialize({ auth })`](/docs/subscriptions/visitor-authentication/#register-the-adapter), supplying `goToLogin`, `goToCreateAccount`, and an identity method (`getUserEmail` or `getIdentityToken`). Until you do:

- `showPaywall({ product })` still renders your prices, but selecting one has no effect and logs an error to the browser console.
- `openCheckout({ price })` does not render.

This is **not** related to whether a visitor is signed in. Once your adapter is registered, anonymous visitors are not blocked — with [expedited checkout](/docs/subscriptions/visitor-authentication/#expedited-checkout) on (the default) they check out as a guest on the single payment screen; with it off they see the **Log in**, **Create account**, and (when enabled) **Continue as guest** options first. Register and test your adapter before switching the domain to this mode. See [Bring Your Own Login](/docs/subscriptions/visitor-authentication/#bring-your-own-login).

### A Visitor Doesn't See the Free Trial Option

If the paywall shows the regular price instead of a free trial:

- **The visitor already used their trial.** Trials are limited to one per product per subscriber. A visitor (or their card) that already trialed the product sees an "already redeemed a free trial" notice at checkout and can subscribe at the regular price. This is expected.
- **The price has no trial configured.** Confirm the recurring price has **Free trial (days)** set on its [Products](/docs/subscriptions/products/#free-trials) page. Trials apply only to recurring prices, not one-time purchases.

### A Promo Code Is Rejected at Checkout

If a visitor enters a code and it is not accepted:

- The code is **disabled, expired, or has reached its max redemptions**. Check its status on the [Products](/docs/subscriptions/products/) page.
- The code is **restricted to specific products** and the item in checkout isn't one of them.
- The order is **below the code's minimum order amount**.
- The code is **first-time customers only** and the visitor has bought before.
- The checkout is for a **free trial** and the code's duration is **Once** — once-duration codes can't be used with trials. Use a repeating or forever code instead.
- Only **one code** applies per checkout, and codes do **not** apply to [donations](/docs/subscriptions/promo-codes/#trials-and-one-time-purchases).

See [Promo Codes](/docs/subscriptions/promo-codes/).

## Onsite Script and Access

### The Script Does Not Load

Confirm the script is present on the page:

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

Then check:

- Browser console errors.
- Network errors for `https://sm.ezoic.com/min.js`.
- Ad blockers or privacy extensions.
- Content Security Policy restrictions.
- Whether the script is included on the page where subscriptions should work.

See [Onsite Script Integration](/docs/subscriptions/site-integration/) for placement guidance.

### `ezsubscriptions` Is Undefined or API Calls Have No Effect

Because `min.js` loads asynchronously, `window.ezsubscriptions` and its methods are not available the instant your page runs. If you call the API directly on page load, it may throw `ezsubscriptions is undefined` or have no effect.

Queue your integration on `ezsubscriptions.cmd` instead — the callback runs once the widget is ready:

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

If you need a synchronous check, `ezsubscriptions.ready` is `true` once the API is available. See [Readiness](/docs/subscriptions/api-reference/#readiness).

### The Paywall Does Not Appear

`showPaywall({ product })` is safe to call on every page load — it deliberately renders nothing in several expected cases. The usual explanations:

- **The visitor already has access.** For a signed-in visitor who already holds the product (or the in-play item), the paywall re-checks access and self-suppresses — this is correct behavior, not a defect. If the paywall appears blank during testing, the account is likely still signed in from a previous purchase. Test in a fresh private or incognito window as an anonymous visitor, who always sees the paywall.
- **The product handle is incorrect.** The `product` value must be the **product handle** from your dashboard, not the public display name. An unknown or inactive handle renders nothing.
- **The product is not sellable.** A **draft, disabled, or archived** product, or one with **no active price**, renders nothing. Confirm the product is live with at least one active price on your [Products](/docs/subscriptions/products/) page.
- **`product` is missing.** `showPaywall(...)` requires a `product`; without it, the call has no effect.
- **The call ran before the widget was ready.** Place it inside `ezsubscriptions.cmd`.

Note that `openCheckout({ price })` does not check access before opening — a subscriber who clicks your buy button sees checkout like anyone else, so gate custom buy buttons behind your own `hasAccess(...)` check. The one safeguard: for a one-time item purchase, a visitor who logs in at the checkout gate and already owns the item is taken straight to the success screen instead of paying again.

### A Subscriber Paid but Still Sees the Paywall

Access is established by Ezoic **after** payment is verified — not the moment the card form submits. `access:change` fires only once access is confirmed.

Ask the visitor to refresh the page. If that does not work:

- Reveal content from an [`access:change`](/docs/subscriptions/publisher-managed-access/#reacting-to-access-changes) listener — the durable signal — rather than assuming access the instant checkout closes. The deprecated [`onSuccess` callback](/docs/subscriptions/api-reference/#paywall-and-checkout-methods) also works for same-page updates, but it does not survive a login detour or 3DS redirect.
- Confirm your site waits for access verification before revealing full content.
- Confirm the code checks the same product handle that the purchase grants.
- Have the visitor use the "Already subscribed?" or **Log in** link in the paywall to restore their session.

### Content Does Not Update After Login or Checkout (React, Vue, Next.js)

In a single-page app, the widget cannot observe your client-side route changes, so gated content may become stale after a login or purchase. Load `min.js` **once** (in your root layout or shell, not per route), then:

- Re-run `hasAccess(...)` when the view changes, and reveal content, remove ads, or open the paywall accordingly.
- 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.
- Let magic-link returns and checkout resumption land on a real URL rather than intercepting them in your client router.

See [Single-Page Apps and Frameworks](/docs/subscriptions/site-integration/#single-page-apps-and-frameworks-react-vue-nextjs).

### Ads Still Appear for a Subscriber

Ad removal is applied on Ezoic's servers, so [`ezsubscriptions.disableAds()`](/docs/subscriptions/site-integration/#removing-ads-for-subscribers) reloads the page **once** on the page where access is first gained; every page view after that is ad-free. If ads still appear:

- Confirm your code calls `disableAds()` after `hasAccess(...)` returns `allowed` (and `allowAds()` otherwise).
- Note that only ads **Ezoic serves** are suppressed. Ads from another network, a header-bidding setup you run yourself, or tags you place directly on the page are outside Ezoic's control — remove those for subscribers yourself.
- After sign-out or an expired subscription, `allowAds()` restores ads on the visitor's **next** page view, so a refresh may be needed before ads reappear.

## Content Protection and SEO

### Full Content Is Visible in the Page Source

If full article content is shipped in the initial HTML, technically advanced visitors may be able to inspect it even if it is visually hidden.

For stronger protection:

1. Serve only teaser content initially.
2. Call `ezsubscriptions.hasAccess(...)`.
3. Fetch or render the protected body only after access is allowed.

The widget provides access checks, paywall UI, checkout, and login. Your site still controls how protected content is delivered. The same applies to a sold file download or a gated tool, not just articles — see [Protecting Full Content](/docs/subscriptions/site-integration/#protecting-full-content).

### Paywalled-Content Markup Fails Validation

Paywalled-content structured data is authored by your site — the widget does not emit it for you. For gated article pages, confirm:

- The page has `isAccessibleForFree: false`.
- The page has a `hasPart` object.
- `hasPart.cssSelector` matches the gated content container, such as `.paywalled-content`.
- JSON-LD appears in the rendered HTML.
- Google's [Rich Results Test](https://search.google.com/test/rich-results) can read the markup.

If the page already has article JSON-LD, merge the paywall fields into that object rather than creating conflicting duplicate article records. See [SEO and Paywalling Best Practices](/docs/subscriptions/seo-paywalling-best-practices/).

### A Gated Page Disappeared From Search

Check for common indexing mistakes:

- Accidental `noindex`.
- `robots.txt` blocking the URL.
- Incorrect canonical tags.
- Server-side gating that blocks crawler access.
- User-agent-only crawler handling.

Do not cloak content. Follow Google's [paywalled-content structured data guidance](https://developers.google.com/search/docs/appearance/structured-data/paywalled-content) and [SEO and Paywalling Best Practices](/docs/subscriptions/seo-paywalling-best-practices/).

### Search or Social Snippets Show Script Code

WordPress SEO plugins (Rank Math, Yoast) build the meta description from post content when none is set, and inline `<script>` code can be excerpted into it — your snippets then show `ezsubscriptions.cmd.push(...)` instead of a description. Set an explicit excerpt or meta description on any post that embeds integration scripts.

### Stripe Checkout Does Not Load

If your site uses a strict Content Security Policy, it can block Stripe from loading the checkout. Allow Stripe in your CSP — see [Stripe Content Security Policy](/docs/subscriptions/site-integration/#stripe-content-security-policy) for the allowlist entries.

Also check browser console errors and confirm payment setup is active.

## Donations

### The Donation Button Has No Effect

Confirm:

- Donations are enabled in the Ezoic dashboard, with an **active donation that has an active price** and active payment setup — not only the enable toggle. `openDonation()` logs a warning and has no effect on a site with no donation configured.
- Your custom button's click handler runs inside a widget `cmd` callback.
- `amountCents` is an integer number of cents.
- `amountCents` is at least the configured minimum donation amount.

Example:

```javascript
ezsubscriptions.openDonation({ amountCents: 2500 });
```

`2500` means `$25.00`. `openDonation()` loads your donation settings on first use, so no setup call is needed. See [Donation Integration](/docs/subscriptions/donations/).

## Subscribers and the Portal

### A Subscriber Cannot Remove a Card

A saved card may be tied to an active subscription. The subscriber should add or select another card for that subscription first, or cancel the subscription before removing the card. See [Subscriber Experience](/docs/subscriptions/subscriber-experience/#payment-methods).

### A Subscriber Cannot Log In

First, have the subscriber sign in through the standard flow:

1. Use the site's **Log in** link (or the paywall's "Already subscribed?" link) to open the sign-in screen for the site's authentication mode.
2. On Ezoic visitor accounts, enter the email address and use the one-time sign-in link Ezoic emails — or Continue with Google, if available. A visitor who set a password can use it instead. On bring your own login, sign in through the site's own login.

On Ezoic visitor accounts, if the emailed link does not arrive or no longer works (for example, on a new device or after purchasing as a guest), have the subscriber request a fresh link from the paywall or the login page and use the newest one: links expire after 15 minutes and are single-use.

On bring your own login, there is no Ezoic-issued sign-in link: the site's **Log in** link and the paywall both go straight to your own login, so a sign-in problem (for example a forgotten password) is your own system's to resolve.

The subscriber portal (`https://subscriber.ezoic.com`) will not resolve this either way: it is a separate, billing-only session, and signing in there does not unlock gated content back on your site. To regain onsite access, the subscriber must sign in through your site's **Log in** link or the paywall's one-time link above.

