# Ezoic Docs
This file is generated automatically from the Hugo documentation tree for AI agents and large-context scanning.
# EzoicAds
---
## ChatGPT
Source: https://docs.ezoic.com/docs/ezoicads/setup-mcp/chatgpt/
Connect [ChatGPT](https://chatgpt.com) to Ezoic's AI Setup Assistant and get working ad-integration code from a chat.
Ezoic Setup is available as an official app in the ChatGPT app directory.
## Connect
[Add to ChatGPT](https://chatgpt.com/apps/ezoic-setup/asdk_app_6a4d92eec2048191b9e50ff7f32b5b31)
Or install from inside ChatGPT:
1. Open the [Ezoic Setup app page](https://chatgpt.com/apps/ezoic-setup/asdk_app_6a4d92eec2048191b9e50ff7f32b5b31).
2. Click **Connect**.
3. Start a conversation and mention **Ezoic Setup**, or ask it to help you integrate Ezoic ads.
No Ezoic login is required.
## Next steps
- See [example questions](/docs/ezoicads/setup-mcp/) on the AI Setup Assistant overview.
- [Troubleshooting](/docs/ezoicads/setup-mcp/#troubleshooting) lives on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## Preface
Source: https://docs.ezoic.com/docs/ezoicads/getting-started/
This guide will walk you through setting up EzoicAds on your website in three simple steps. The process is straightforward and we're here to help every step of the way.
## Before You Start
Here's what you'll need:
1. An Ezoic account - [Create one here](https://pubdash.ezoic.com/join) if you don't have one yet
2. The ability to edit your website's code (or someone who can help you)
3. Your Ezoic Onboarding Specialist is available to guide you through any part of this process
Using an AI coding assistant (Cursor, Claude, ChatGPT, VS Code, Codex)? Connect the [AI Setup Assistant](/docs/ezoicads/setup-mcp/) and have it do the integration from your project.
Ready? Let's move on to Step 1: Ads.txt Setup.
---
## Step 1: Ads.txt Setup
Source: https://docs.ezoic.com/docs/ezoicads/adstxt/
Ads.txt setup is required for all publishers using Ezoic. This file helps maximize your ad revenue by ensuring authorized ad partners can bid on your inventory.
## What is Ads.txt?
Ads.txt is a simple text file that tells advertisers which companies are authorized to sell ads on your website. Think of it as a "verified seller" list that prevents unauthorized companies from selling fake ad space on your domain.
## Choose Your Setup Method
Select the method that best fits your website setup:
**Important:** Replace `[YOUR_DOMAIN].com` with your actual domain name in all code examples below.
### 1. WordPress Sites (Recommended)
**Easiest option for WordPress users:**
1. Install the [**Ezoic Integration Plugin**](https://wordpress.org/plugins/ezoic-integration/) from your WordPress admin dashboard
2. Activate the plugin and navigate to its settings
3. Enable the ads.txt management feature
4. The plugin handles everything automatically
The Ezoic WordPress plugin automatically manages your ads.txt file and keeps it updated. No technical setup required!
### 2. Server Redirects
**For users with server access:**
**Apache (.htaccess):**
```apache
Redirect 301 /ads.txt https://srv.adstxtmanager.com/19390/[YOUR_DOMAIN].com
```
**Nginx:**
```nginx
server {
location ~ /ads.txt {
return 301 https://srv.adstxtmanager.com/19390/[YOUR_DOMAIN].com;
}
}
```
**PHP:**
```php
```
### 3. Automated Updates (Advanced)
**For automatic daily updates:**
```bash
curl -L https://srv.adstxtmanager.com/19390/[YOUR_DOMAIN].com > ads.txt
```
Set up a daily cron job to run this command automatically and keep your ads.txt file current.
## Test Your Setup
1. **Visit** `yourdomain.com/ads.txt` in your browser
2. **Verify** you see a list of authorized ad sellers
3. **Clear your website cache** if the file doesn't appear immediately
## Complete!
Once your ads.txt file is working correctly, you've successfully completed Step 1. Your website is now properly configured to work with Ezoic's advertising partners, helping maximize your ad revenue.
Ready for the next step? Continue to **Step 2: Site Integration** to add the Ezoic JavaScript code to your website.
---
## Step 2: Site Integration
Source: https://docs.ezoic.com/docs/ezoicads/integration/
**WordPress Users:** If you're using WordPress, you can simplify this integration process by using our WordPress Plugin. The plugin handles all the setup steps automatically. [Learn more about the Ezoic WordPress Plugin integration guide](https://support.ezoic.com/kb/article/ezoic-wordpress-plugin-javascript-integration-guide).
Ezoic's JavaScript integration provides a flexible, client-side solution for displaying ads on your website. This integration method gives you full control over ad execution and is compatible with all website builds and content management systems.
The integration process involves two main components:
1. **Header Scripts** - Essential JavaScript files that initialize the Ezoic ad system
2. **Ad Placement Code** - JavaScript snippets that define where ads will appear on your pages
This approach ensures optimal ad performance while maintaining your website's functionality and user experience.
## Add Header Scripts
Add the **Ezoic Privacy Scripts** and **Header Script** at the very top of the `
` tag. Please ensure you load the privacy scripts before the header script.
### Privacy Scripts
The privacy scripts handle user consent management and must be loaded first to ensure compliance with privacy regulations:
```html
```
The `data-cfasync="false"` attribute prevents Cloudflare from optimizing these scripts, ensuring they load in the correct order for privacy compliance. The attribute is required to be placed in front of the src attribute to work properly.
### Header Script
The main Ezoic header script initializes the ad system on your website:
```html
```
### Script Placement Guidelines
For quicker ad setup and delivery, we recommend inserting these header scripts within the `` tag. If placing them in the head is not feasible, the next best option is to position them at the very top of the `` tag.
**Best Practices:**
- Load privacy scripts before the header script
- Place all scripts as high as possible in the `` section
- Avoid loading scripts conditionally or after page load
- Ensure scripts are not blocked by ad blockers or CSP policies
### Verification
Once you have completed adding these header scripts to your website, you have successfully completed Step 2 of EzoicAds integration.
To verify everything is working correctly, visit your website and ensure it loads normally without any broken functionality. If you encounter any issues or need assistance, please reach out to Ezoic's support team.
You're now ready to proceed to the final step: adding ad placements.
---
## Step 3: Ad Placements
Source: https://docs.ezoic.com/docs/ezoicads/implementation/
Now that you've integrated Ezoic's scripts and set up your ads.txt file, it's time to add ad placements to your website. This final step will start displaying ads and generating revenue.
For most sites, you only need this one snippet pattern. Paste it wherever you want an ad to appear, and Ezoic will control the sizing for that spot.
## Add the Ad Snippet
```html
```
Use the same snippet for each ad location on your page. For example, place one above your content, one between paragraphs, and one near the bottom of the article.
Ezoic controls which ad size serves based on your site's configuration, the visitor's device, and available demand. You do not need to request a specific fixed size for a basic setup.
Ads never stack: use one `showAds` snippet per spot. To show several ads, place the same snippet at different positions in your page.
**Important:** To ensure Ezoic ads work properly on your site, don't forget to remove any leftover ad code from other ad networks.
## Need More Control?
If you want to request specific sizes, set different sizes per device, target elements from JavaScript, or use placement IDs from the dashboard, see [ShowAds advanced usage](/docs/ezoicadsadvanced/advanced/#showads-advanced-usage).
## Setup Complete
Once you have completed these steps, you have successfully implemented EzoicAds on your site. If you have any questions or need assistance, please reach out to Ezoic's support team.
---
## Dynamic Content
Source: https://docs.ezoic.com/docs/ezoicads/dynamic-content/
This section is for websites where content loads or changes dynamically. For example, a modal may appear when a user clicks a button, or the page may change without a full reload.
## Changing Pages
When switching between pageviews dynamically, it is important to re-call `ezstandalone.showAds()` to force ads to refresh on the new URL.
```html
```
Calling this function with no values will automatically call every existing placeholder on the new page. It will also refresh the anchor and video ad locations.
## New Content
For additional placeholders within the same pageview, you can use the `ezstandalone.showAds` function.
If a user clicks a button, new content loads, and placeholders `104` and `105` are added, `ezstandalone.showAds` should be used to display them.
```html
```
## Changing content
If the content changes within the same pageview and a placeholder is no longer needed or visible, the placeholder needs to be properly cleaned up using `ezstandalone.destroyPlaceholders`.
```html
```
If this content becomes visible again, you can recall these locations using `ezstandalone.showAds`, such as in the "New Content" example above.
If your site needs to react after Ezoic has finished trying to fill an individual placeholder, listen for the [`ezSlotComplete` event](/docs/ezoicadsadvanced/slot-complete-event/). When `event.detail.filled` is `false`, Ezoic is done trying to fill that slot for the current lifecycle, so you can hide your own surrounding layout if needed.
## Infinite Scroll
For sites which implement an infinite scroll, it is recommended to use unique placeholder IDs on each subsequent article, which can be called using `ezstandalone.showAds` when the new article loads. For example, Article 1 may contain placeholders `102`, `103` and `104`, while Article 2 may contain placeholders `105` and `106`.
When the first article loads:
```html
```
When the user scrolls to the second article, the next set of ads will be loaded using:
```html
```
It is recommended to create a set of `in-content` placeholders specifically for infinite scroll.
If placeholder IDs are reused in the newly loaded articles, then `ezstandalone.destroyPlaceholders` must be used prior to `ezstandalone.showAds`. It is important - however - that the original placeholders are destroyed in the HTML, as having multiple instances of the same placeholder ID on a page will cause unpredictable ad behaviour.
When using `ezstandalone.destroyPlaceholders`, make sure you don't call it too early. This could cause the ads on the page the user is currently viewing to disappear!
## Removing all placeholders
You can remove all placeholders on the page by using the `destroyAll` function.
```html
```
As with `destroyPlaceholders` - these locations can later be recalled with the `showAds` function.
## Suppressing additional formats
If dynamic content appears on the page and ads should not show next to it, use `destroyAll()` or `destroyPlaceholders()` to remove display ad placeholders. To also prevent floating outstream video and interstitial ads from returning later in the same page session, temporarily disallow these requests using `ezstandalone.setOutstreamAllowed(false)` and `ezstandalone.setInterstitialAllowed(false)` respectively.
```html
```
The `reason` value is optional, but it can help identify why these formats were suppressed. When the page is ready for these formats again, you can set these variables to `true`, with the additional option of using the parameter `requestAdOnAllow: true` to immediately request this ad type.
An example for outstream below:
```html
```
Note that `requestAdOnAllow` for interstitial ads is dependent on frequency caps.
To check the current outstream state, use `isOutstreamAllowed()` and `isInterstitialAllowed()` respectively.
```html
```
These methods control outstream video and interstitial only. It does not replace placeholder cleanup with `destroyAll()` or `destroyPlaceholders()`.
## Show all placeholders
You can call ads in every placeholder on a given page by using the `showAds` function, without defining any value.
```html
```
---
## Claude
Source: https://docs.ezoic.com/docs/ezoicads/setup-mcp/claude/
Connect [Claude.ai](https://claude.ai) or Claude Desktop to Ezoic's AI Setup Assistant and get working ad-integration code from a chat.
Ezoic Setup is available as an official connector in the [Claude connectors directory](https://claude.ai/directory/connectors/ezoic-setup).
## Connect
[Add to Claude](https://claude.ai/directory/connectors/ezoic-setup)
Or install from inside Claude:
1. In a chat, click the **+** button next to the message box.
2. Choose **Add connector** → **Browse connectors**.
3. Search for **Ezoic Setup** and open it.
4. Click **Connect**.
5. Start a new chat and ask it to help you integrate Ezoic ads.
No Ezoic login is required.
### Manual custom connector
If the directory connector is not available:
1. Open **Settings** → **Connectors** → **Add custom connector**.
2. Name it **Ezoic Setup**.
3. Paste `https://setup-agent.ezoic.com/mcp` as the URL.
On **Claude Team / Enterprise**, only an Owner can add connectors under **Organization settings → Connectors**.
## Next steps
- Using the terminal? See [Claude Code](/docs/ezoicads/setup-mcp/claude-code/).
- See [example questions](/docs/ezoicads/setup-mcp/) on the AI Setup Assistant overview.
- [Troubleshooting](/docs/ezoicads/setup-mcp/#troubleshooting) lives on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## Claude Code
Source: https://docs.ezoic.com/docs/ezoicads/setup-mcp/claude-code/
Connect [Claude Code](https://docs.anthropic.com/en/docs/claude-code) to Ezoic's AI Setup Assistant from the terminal.
## Install
```bash
claude mcp add --transport http ezoic-setup https://setup-agent.ezoic.com/mcp
```
No Ezoic login is required.
## Next steps
- See [example questions](/docs/ezoicads/setup-mcp/) on the AI Setup Assistant overview.
- [Troubleshooting](/docs/ezoicads/setup-mcp/#troubleshooting) lives on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## Troubleshooting
Source: https://docs.ezoic.com/docs/ezoicads/troubleshooting/
When setting up EzoicAds on your site, it's important to ensure that everything is functioning properly. We provide a built-in JavaScript Integration Debugger that allows you to quickly diagnose issues with your ad setup. Below is a step-by-step guide on how to use this debugger and interpret its outputs.
## Step 1: Trigger the Debugger
To activate the debugger, simply append the following query parameter to the URL of the page where your EzoicAds are integrated:
```
ez_js_debugger=1
```
For example:
```
https://www.yourwebsite.com?ez_js_debugger=1
```
Once the query parameter is added and the page loads, a green modal dialog will appear at the bottom center of the screen. This is the JavaScript Integration Debugger.
## Step 2: Understanding the Debugger Output
The debugger modal contains several key pieces of information that will help you troubleshoot your setup. Here’s a breakdown of the information provided:
- **Script In Page `` Element**: The debugger verifies the presence of the Ezoic integration script (`sa.min.js`) in the page's `` element. This ensures the script is optimally positioned for effective page loading.
- **Consent**: This section checks for the use of Ezoic consent management tools, including an API call or the presence of the Consent Management Platform (CMP) script on the page. These checks ensure that user privacy is managed in compliance with regulations. For more information on privacy management, see the [Privacy Documentation](/docs/privacy/).
- The CMP script checks include verifying its presence on the page, its placement within the page's `` element, and ensuring it appears before Ezoic's integration script (`sa.min.js`) in the HTML structure.
- **Ad Request**: The debugger tracks whether an ad request to Ezoic's servers is successfully sent and provides key details, such as the word count and the maximum number of allowed ads.
- This section includes a word count consistency check to ensure ad requests are not sent too early—before the primary page content has fully loaded. Maintaining a consistent word count helps verify that dynamic content remains stable, ensuring accurate ad rendering.
- **Defined Placeholders**: This section provides detailed information about the ad placeholder ids used in the last network request to Ezoic.
- For each ID, the debugger displays the corresponding HTML element on the page, whether the ID was selected for ad insertion, and its associated ad unit name and size.
- **Unused Placeholders**: This section lists the IDs of ad placeholders that are present in the page's HTML but were not included in the most recent network request to Ezoic.
- **API Methods Called**: The debugger displays each method invoked on the `ezstandalone` API, displaying them in the order of execution along with their provided arguments. This detailed tracking helps identify potential implementation issues.
## Step 3: Using the Debugger for Troubleshooting
The information provided in the debugger modal is invaluable for diagnosing common issues such as:
- **Ads Not Displaying**: Check the placeholder detection and ad status to ensure placeholders are defined and ads are assigned.
- **Ad Requests Failing**: If ad requests aren't being sent or are failing, the debugger will provide error messages that point to potential misconfigurations.
- **Script Loading Issues**: If the script isn’t properly loaded, verify that the script tag is present in your source code and correctly configured.
## Conclusion
The EzoicAds JavaScript Integration Debugger is a simple but powerful tool to help you ensure that your ad setup is functioning as intended. By following these steps, you can quickly identify and resolve common issues, ensuring that your ads display properly and without disruption.
---
## Cursor
Source: https://docs.ezoic.com/docs/ezoicads/setup-mcp/cursor/
Connect [Cursor](https://cursor.com) to Ezoic's AI Setup Assistant and have the agent add Ezoic ads to your project.
## Connect
[Add to Cursor](https://cursor.com/install-mcp?name=ezoic-setup&config=eyJ1cmwiOiJodHRwczovL3NldHVwLWFnZW50LmV6b2ljLmNvbS9tY3AifQ%3D%3D)
Or add to `~/.cursor/mcp.json` (or your project's `.cursor/mcp.json`):
```json
{"mcpServers": {"ezoic-setup": {"url": "https://setup-agent.ezoic.com/mcp"}}}
```
Reload MCP servers or restart Cursor if needed. No Ezoic login is required.
## Next steps
- See [example questions](/docs/ezoicads/setup-mcp/) on the AI Setup Assistant overview.
- [Troubleshooting](/docs/ezoicads/setup-mcp/#troubleshooting) lives on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## VS Code
Source: https://docs.ezoic.com/docs/ezoicads/setup-mcp/vscode/
Connect [VS Code](https://code.visualstudio.com/) to Ezoic's AI Setup Assistant from the editor.
## Connect
[Add to VS Code](https://insiders.vscode.dev/redirect/mcp/install?name=ezoic-setup&config=eyJ0eXBlIjoiaHR0cCIsInVybCI6Imh0dHBzOi8vc2V0dXAtYWdlbnQuZXpvaWMuY29tL21jcCJ9)
Or from the command line:
```bash
code --add-mcp '{"name":"ezoic-setup","type":"http","url":"https://setup-agent.ezoic.com/mcp"}'
```
Or add to `.vscode/mcp.json` in your project:
```json
{"servers": {"ezoic-setup": {"type": "http", "url": "https://setup-agent.ezoic.com/mcp"}}}
```
Reload MCP servers or restart VS Code if needed. No Ezoic login is required.
## Next steps
- See [example questions](/docs/ezoicads/setup-mcp/) on the AI Setup Assistant overview.
- [Troubleshooting](/docs/ezoicads/setup-mcp/#troubleshooting) lives on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## Codex
Source: https://docs.ezoic.com/docs/ezoicads/setup-mcp/codex/
Connect [Codex CLI](https://github.com/openai/codex) to Ezoic's AI Setup Assistant.
## Configure
Add this to `~/.codex/config.toml`:
```toml
[mcp_servers.ezoic-setup]
url = "https://setup-agent.ezoic.com/mcp"
```
Restart Codex if it is already running. No Ezoic login is required.
## Next steps
- See [example questions](/docs/ezoicads/setup-mcp/) on the AI Setup Assistant overview.
- [Troubleshooting](/docs/ezoicads/setup-mcp/#troubleshooting) lives on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
# Privacy
---
## GDPR Compliance
Source: https://docs.ezoic.com/docs/privacy/gdpr/
## Compliance
### Privacy Policy
You must provide Ezoic with the link to your privacy policy. This can be done at [https://pubdash.ezoic.com/settings/privacy/privacypolicy](https://pubdash.ezoic.com/settings/privacy/privacypolicy). Your privacy policy must also include additional wording specific to Ezoic, the instructions for doing so and the wordings can be found at the same link.
### GDPR and Consent Management
To handle GDPR compliance, there are two options:
1. Using the free Ezoic TCF 2.3 consent management platform (Recommended)
2. Connect to an existing TCF 2.3 consent management platform
## Using the Ezoic CMP (Recommended)
1. Add the following scripts to the head of your website **before** any ad / tracking code, as covered in [the Ad Set-up steps](/docs/ezoicads/implementation/).
```html
```
__Warning:__ This code **must not be delayed**, otherwise personal information might be processed before consent is given.
### Customizing the ezoic CMP
The Ezoic CMP can be customized by visiting the [privacy section](https://pubdash.ezoic.com/settings/privacy/consentmanagement) within the ezoic dashboard.
# EzoicAds Advanced
---
## Ad Options Builder
Source: https://docs.ezoic.com/docs/ezoicadsadvanced/wordpress/ad-options-builder/
Build valid **Advanced Ad Options** JSON for a WordPress placement, then paste it into the plugin. The builder only emits keys the plugin accepts.
## Where to paste it
1. In WordPress: **Settings > Ezoic > Ad Settings**.
2. Open the placement.
3. Expand **Advanced Ad Options** and paste the JSON.
4. Save.
**Advanced Ad Options** appears when JavaScript integration is on and the plugin uses WordPress-generated placeholders. If you do not see it, check **Settings > Ezoic > JS Integration**.
On a paragraph placement, a pasted `devices` list is absorbed into **Different position per device** (checkboxes) and removed from the JSON box. Other placements keep `devices` in the JSON.
## What each option does
| Option | What it does |
|---|---|
| `devices` | Limits which devices request the ad. Omit to run everywhere. Values: `desktop`, `tablet`, `mobile`. |
| `sizes` | Allowlist of sizes on every device. Standalone treats this as a hard allowlist. |
| `desktop_sizes`, `tablet_sizes`, `mobile_sizes` | Per-device allowlists (preferred when sizes should differ). |
| `fluid` | Let the ad size itself to its container. |
| `required` | Keep the slot even when nothing fills. |
Sizes are `WIDTHxHEIGHT`. One size can be a string; several are a list.
Excluding a device also stops reserved space on that device, so you do not get an empty gap.
## Options you cannot set here
The plugin rejects these if you add them by hand:
- `id`, `anchor`, `location`, `into` - the plugin owns placement and identity
- `class`, `style` - styling the placeholder can leave empty white space when an ad does not load
Invalid keys or a non-`WIDTHxHEIGHT` size block save; the field shows what was wrong.
## Related
- [Advanced]() - same options for sites that call `ezstandalone.showAds()` directly
---
## Anchor Ads
Source: https://docs.ezoic.com/docs/ezoicadsadvanced/anchor-ads/
By default, Ezoic will show anchor ads if enabled in the dashboard under the [Ezoic Ads](https://pubdash.ezoic.com/ezoicads) tab.
## Turning Off Anchor Ads
To turn the Anchor Ad off for a specific page, the `ezstandalone.setEzoicAnchorAd` function can be used.
In order for this functionality to work, it must be called prior to `ezstandalone.showAds()`.
```html
```
## Checking If Anchor Ad Was Closed
To see if the Anchor Ad has been closed for a user's session, use the `hasAnchorAdBeenClosed` function.
Below is an example of using the function to determine whether to keep showing the Anchor Ad
```html
```
---
## Ezoic Video
Source: https://docs.ezoic.com/docs/ezoicadsadvanced/ezoic-video/
The Embed Code Generator from your Ezoic Video dashboard can be used to add our Video Player to your website.
---
## Advanced
Source: https://docs.ezoic.com/docs/ezoicadsadvanced/advanced/
## Overview
EzoicAds provides advanced features that allow publishers to fine-tune the behavior of the ad library. These features can be used to customize the behavior of the ad library to better suit the needs of your site.
### Configurable Options
There are configurable options available in EzoicAds that can be used to customize the behavior of the ad library. These options can be set using the `ezstandalone.config()` function.
#### Setting Configuration Options
To set configuration options, use the following syntax:
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({
// Configuration options go here
});
});
```
**Important:** These configuration options must be set after initialization of the EzoicAds [header scripts](https://docs.ezoic.com/docs/ezoicads/integration/#header-script) and before calling `ezstandalone.showAds()`.
### Available Options
| Option | Type | Default | Description |
| ---------------------------------------------- | ------- | ------------ | ---------------------------------------------------------------------------- |
| [`limitCookies`](#limit-cookies) | Boolean | `false` | Enables more precise control over cookie usage on your site. |
| [`anchorAdPosition`](#anchor-ad-position) | String | `bottom` | Change the position of the anchor ad to be at the top or bottom of the page. |
| [`anchorAdExpansion`](#anchor-ad-expansion) | Boolean | `true` | Allow collapsible anchor ads to expand in height on mobile devices. |
| [`disableInterstitial`](#disable-interstitial) | Boolean | `false` | Completely disable all interstitial ads on your site. |
| [`disableVideo`](#disable-video) | Boolean | `false` | Disable Ezoic video players and video ads on your site. |
| [`disableLeftSideRail`](#disable-side-rails) | Boolean | `false` | Disable the left side rail ad. |
| [`disableRightSideRail`](#disable-side-rails) | Boolean | `false` | Disable the right side rail ad. |
| [`disableSidebarFloating`](#disable-floating-sidebar) | Boolean | `false` | Disable floating (sticky) sidebar ads. |
| [`vignetteDesktop`](#vignette-ads) | Boolean | _(optional)_ | Enable or disable vignette (interstitial) ads on desktop devices. |
| [`vignetteMobile`](#vignette-ads) | Boolean | _(optional)_ | Enable or disable vignette (interstitial) ads on mobile devices. |
| [`vignetteTablet`](#vignette-ads) | Boolean | _(optional)_ | Enable or disable vignette (interstitial) ads on tablet devices. |
---
#### Limit Cookies
When enabled, the Limit Cookies feature ensures that only essential cookies required by Ezoic are included by default. This provides greater control over user privacy and helps with compliance to various data protection regulations.
To enable Limit Cookies:
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({ limitCookies: true });
});
```
---
#### Anchor Ad Position
Allows you to change the position of the Anchor Ad to be at the top or bottom of the page.
Acceptable values are 'top' or 'bottom'. The Anchor Ad will be located at the bottom of the page by default.
For example to set the anchor ad position to be at the top of the page:
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({ anchorAdPosition: "top" });
});
```
---
#### Anchor Ad Expansion
Allows you to set the expansion behavior for collapsible anchor ads on mobile devices.
Collapsible anchor ads are allowed to expand by default.
To disable anchor ad expansion:
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({ anchorAdExpansion: false });
});
```
---
#### Disable Interstitial
Allows you to completely disable all interstitial ads on your site. When enabled, no interstitial ads (including vignette ads) will be displayed.
To disable all interstitial ads:
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({ disableInterstitial: true });
});
```
This setting takes precedence over individual vignette device settings. If `disableInterstitial` is set to `true`, no interstitial ads will display regardless of `vignetteDesktop`, `vignetteMobile`, or `vignetteTablet` settings.
---
#### Disable Video
Allows you to disable Ezoic video players and video ads on your site. When enabled, no Ezoic video players will be inserted into your pages.
To disable video:
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({ disableVideo: true });
});
```
---
#### Disable Side Rails
Side rail ads display in the unused gutter space to the left and right of your page content on wide screens. You can disable each side independently.
To disable both side rails:
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({
disableLeftSideRail: true,
disableRightSideRail: true
});
});
```
To disable only the left side rail:
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({ disableLeftSideRail: true });
});
```
---
#### Disable Floating Sidebar
Allows you to disable floating (sticky) sidebar ads, which remain visible in the sidebar as the user scrolls.
To disable floating sidebar ads:
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({ disableSidebarFloating: true });
});
```
---
#### Vignette Ads
Vignette ads (also known as interstitial ads) are full-screen ads that display between page content or during natural transition points. You can control their display behavior on different device types.
By default, vignette ads follow your site's global settings. Use these options to override the behavior for specific device types:
**Enable vignette ads on desktop only:**
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({
vignetteDesktop: true,
vignetteMobile: false,
vignetteTablet: false
});
});
```
**Disable vignette ads on mobile devices:**
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({ vignetteMobile: false });
});
```
**Enable vignette ads on all devices:**
```javascript
ezstandalone.cmd.push(function() {
ezstandalone.config({
vignetteDesktop: true,
vignetteMobile: true,
vignetteTablet: true
});
});
```
If you don't specify a vignette option, the system will use your site's global vignette settings configured in your Ezoic dashboard.
---
_More options will be added in the future. Please check back for updates._
---
### Tracking Data
If you would like to track data even on pages that are not showing ads you can include the script below. This is specifically helpful in the starting stages of testing when you are not ready to show ads yet.
```html
```
---
### Splitting Traffic
Unlike our other integration methods, splitting traffic happens clientside.
Using the `ezstandalone.isEzoicUser` function, you can split traffic on a percentage of your traffic.
Like the traffic splitting in our cloud integrations the `isEzoicUser` function splits based on a user's session, not pageviews.
### Example
Below is an example of splitting 50 percent of your traffic with Ezoic.
```html
```
---
## ShowAds Advanced Usage
The basic placement snippet on [Ad Placements](/docs/ezoicads/implementation/) lets Ezoic choose the right size automatically. Use these options only when you need custom sizing, JavaScript placement control, or placement IDs from the dashboard.
### Size Presets
The easiest way to control sizes is a named preset — one token instead of listing every `WIDTHxHEIGHT` yourself:
```html
```
| Preset | Sizes |
| --- | --- |
| `standard` | Resolves to the preset below matching the visitor's device |
| `mobile-standard` | `250x250`, `300x250`, `336x280`, `320x50`, `320x100`, `120x600`, `300x600`, `360x640`, `315x560` |
| `desktop-standard` | `300x250`, `336x280`, `728x90`, `580x400` |
| `tablet-standard` | `300x250`, `336x280`, `728x90`, `580x400`, `160x600`, `300x600` |
Preset names are accepted anywhere size values are accepted — the `sizes` key and the device override keys — as a bare string or mixed into an array with explicit sizes (`sizes: ["standard", "970x250"]`). Names are case-insensitive and treat underscores like hyphens (`Mobile_Standard` works).
Omitting `sizes` entirely also works well: Ezoic serves from the position's configured sizes for the device (the standard set plus position-appropriate extras). Fluid (native-style) ads are added automatically where the placement is eligible, so you do not request them as a fixed size.
### Custom Sizing
For exact control, pass specific fixed sizes — a single `WIDTHxHEIGHT` value or an array of values. Only the listed sizes are requested:
```html
```
```html
```
### Different Sizes Per Device
Use `mobile_sizes`, `tablet_sizes`, and `desktop_sizes` to split allowed sizes by device. `sizes` remains the fallback for any device type you do not override:
```html
```
Tablets use `tablet_sizes` if provided, otherwise `desktop_sizes`, otherwise `sizes`.
### Placing Ads From JavaScript
If your ad spots are not marked by inline scripts, such as in single-page apps, dynamically loaded content, or infinite scroll, tell `showAds` where to place the ad:
- **`anchor`** — an element or CSS selector. The ad is inserted right after it: `ezstandalone.showAds({ sizes: "300x250", anchor: "#after-comments" })`
- **`location`** — a named position Ezoic resolves for you: `ezstandalone.showAds({ location: "under_first_paragraph" })`. Supported locations include `top_of_page`, `under_page_title`, `under_first_paragraph`, `under_second_paragraph`, `mid_content`, `long_content`, `longest_content`, `bottom_of_page`, and the sidebar family `sidebar`, `sidebar_middle`, `sidebar_bottom`.
- **Neither** — the ad is appended at the end of your content, which suits feeds and infinite scroll where new content keeps arriving.
### Marking Ad Spots In HTML
Instead of one script per spot, you can mark ad spots with plain elements and make a single `showAds` call with a CSS selector. Each matched element hosts one ad, and per-ad settings come from `data-*` attributes on the element:
```html
```
The ad is placed inside the matched element, so you can style and position the container. Available `data-*` attributes include `data-sizes`, `data-mobile_sizes`, `data-desktop_sizes`, `data-tablet_sizes`, `data-devices`, `data-fluid`, `data-required`, and `data-location`.
Elements are only used once: calling `showAds(".ezoicad")` again after a route change or after appending new content skips spots that already have ads and only fills newly added elements.
### Optional Per-Ad Attributes
You can pass these attributes in the object form of `showAds`:
- **`id`** — the unique placement ID where the ad should be shown. When omitted, the ad is placed where the calling script is.
- **`devices`** — only place the ad on the listed device types (`"mobile"`, `"tablet"`, `"desktop"`). On other devices the snippet does nothing: `ezstandalone.showAds({ devices: ["desktop"], sizes: "728x90" })`.
- **`class`** / **`style`** — a CSS class or inline style applied to the ad's container div: `ezstandalone.showAds({ sizes: "300x250", class: "my-ad-frame", style: "margin: 24px auto;" })`.
- **`fluid`** — control fluid ads for this spot. `fluid: false` prevents them where variable-height creatives would break your layout; `fluid: true` allows them where your configuration would otherwise disable them. Safety limits still apply.
- **`required`** — id-less ads default to `required: true`, meaning the ad serves even past the page's automatic ad-density cap. Pass `required: false` to let Ezoic skip the ad when the page already carries enough ads.
### Using Placement IDs
Placement IDs are useful when you want dashboard-level control for specific ad spots, such as per-location reporting, experiments, or custom size settings. First, create ad placements in your [Ezoic Dashboard](https://pubdash.ezoic.com/ezoicads/adpositions/placeholders), or work with your Ezoic Onboarding Specialist for optimized placement recommendations.
Once you have your placement IDs, add the matching placeholder `div` where the ad should appear and call `showAds` with that ID:
```html
```
Replace `101` with your actual placement ID from the dashboard. Insert this code for each ad location on your site.
Pages with multiple placements should pass all placement IDs into a single `showAds()` call. This reduces server requests and improves loading speed.
_Example: `ezstandalone.showAds(101, 102, 103, 104)`_
Do not add styling to the actual placeholder `div`. Adding styles or reserving space for the placeholder may result in empty white space if an ad does not load.
### Placement ID And Batch Examples
`showAds` can accept an array of objects when you want to configure several placements in one call:
```html
```
In this example:
1. **Placeholder 103** requests either `336x280` or `126x126` and is required.
2. **Placeholder 104** requests `1000x450` if Ezoic decides this ad should be shown.
3. **Placeholder 105** is required and has no size request, so Ezoic decides the best size for this placeholder.
### Notes
- Sizes must be fixed `WIDTHxHEIGHT` values.
- If sizes are specified, the ad will only display when an ad of the specified sizes is available.
- If sizes are omitted or left empty, Ezoic uses its default behavior to determine the ad size.
- Use custom sizing only when your layout needs it. The basic setup is recommended for most ad placements.
---
## RefreshAds
`refreshAds` allows for placeholders to be refreshed without needing to call `destroyPlaceholders` and `showAds`. This is useful for sites with dynamic content or infinite scroll where the content changes but the placeholders remain the same.
Multiple placeholders can be passed in as comma-separated list of IDs or as an array of IDs.
### Example
```html
```
On a typical page, Ezoic will automatically refresh ads once they have loaded. There is no need to use the `refreshAds()` function outside of dynamic content.
---
## Ad Slot Complete Event
Source: https://docs.ezoic.com/docs/ezoicadsadvanced/slot-complete-event/
The `ezSlotComplete` event lets your site respond when Ezoic has finished an ad slot's current loading lifecycle.
This is useful when your layout needs to react after Ezoic has either filled a placement or stopped trying to fill it. For example, you may want to hide a surrounding wrapper when a placement is not filled.
## Listening for the event
Add a listener to `document`:
```javascript
document.addEventListener("ezSlotComplete", function (event) {
if (event.detail.placeholderId !== 123) {
return;
}
var wrapper = document.getElementById("ad-wrapper-123");
if (event.detail.filled) {
// Placeholder 123 was filled.
if (wrapper) {
wrapper.style.display = "";
}
} else {
// Placeholder 123 did not fill.
if (wrapper) {
wrapper.style.display = "none";
}
}
});
```
## When the event fires
`ezSlotComplete` fires after Ezoic has finished processing a slot for the current load or refresh lifecycle.
- If `event.detail.filled` is `true`, Ezoic filled the slot.
- If `event.detail.filled` is `false`, Ezoic has stopped trying to fill that slot for the current lifecycle. In this case, it is safe to hide your own surrounding layout for that slot.
The event does not fire while Ezoic is still retrying the slot.
Hide your own wrapper or surrounding layout, not the Ezoic placeholder element itself. The placeholder element should remain available for future Ezoic calls, refreshes, or dynamic content behavior.
## Example: hide an unfilled slot wrapper
Use a publisher-owned wrapper around the Ezoic placeholder:
```html
```
Then hide the wrapper if Ezoic finishes the slot without a fill:
```javascript
document.addEventListener("ezSlotComplete", function (event) {
if (event.detail.placeholderId !== 123) {
return;
}
var wrapper = document.getElementById("ad-wrapper-123");
if (!wrapper) {
return;
}
if (event.detail.filled) {
wrapper.style.display = "";
} else {
wrapper.style.display = "none";
}
});
```
If you call `ezstandalone.showAds()` or refresh the same placeholder again later, make sure your wrapper is visible before the next ad request. The `filled: true` branch above restores the wrapper for that case.
## Event details
The event is a browser `CustomEvent`. The payload is available on `event.detail`.
| Field | Type | Description |
|---|---|---|
| `placeholderId` | number | The Ezoic placeholder ID associated with the slot. |
| `filled` | boolean | `true` when Ezoic filled the slot; `false` when Ezoic stopped trying to fill it for the current lifecycle. |
| `reason` | string | `"filled"` or `"no_fill"`. |
| `slotId` | string | The rendered ad slot element ID, when available. |
| `adUnitPath` | string | The Google Ad Manager ad unit path, when available. |
| `positionType` | number | The Ezoic position type for the slot. |
| `isAnchor` | boolean | Whether the slot is an Ezoic anchor slot. |
## Anchor ads
Some anchor ads are owned and managed by Google. Ezoic does not send `ezSlotComplete` for those Google-owned anchors, because publisher code should not hide or remove layout that Google manages.
Ezoic-owned slots, including Ezoic-owned anchors, can emit `ezSlotComplete` when their current lifecycle finishes.
---
## Scroll Rail Ads
Source: https://docs.ezoic.com/docs/ezoicadsadvanced/scroll-rail/
A scroll rail is a column of display ads that fills a tall sidebar container on desktop. Ads load as the reader scrolls, so the rail stays lightweight on long pages.
Pass the id of one of your own container elements to `ezstandalone.showScrollRail` and Ezoic mounts the rail inside it. The rail is added after any content already in the container and never overlays or repositions your content.
## Usage
```html
```
The argument is the element's id as a plain string — no `#` prefix. The element must be on the page when you call it.
## Requirements
- **Desktop only.**
- **At least 250px wide.** Make it 300px or wider to allow larger ad sizes (300x600).
- **At least ~1500px tall.** That's enough room for one ad; taller containers get more.
If the container is too narrow or too short, nothing is rendered.
## How Many Ads Appear
One ad per ~1500px of container height:
| Container height | Ads |
|---|---|
| 3,000px | 2 |
| 7,500px | 5 |
| 15,000px | 10 |
Height is measured from below any existing content in your container down to the end of the page content — a container taller than the page itself won't yield more ads.
## Multiple Rails on One Page
Call `showScrollRail` once per container:
```js
ezstandalone.cmd.push(function () {
ezstandalone.showScrollRail('left-sidebar');
ezstandalone.showScrollRail('right-sidebar');
});
```
Each rail sizes itself to its own container. Rails are set up in the order they are created.
## Single-Page Apps
The rail automatically re-initializes into the same container id on each new pageview. If a route uses a different container element, call `showScrollRail` again with the new id after the navigation.
---
## Rewarded Ads
Source: https://docs.ezoic.com/docs/ezoicadsadvanced/rewarded/
Rewarded ads are a type of ad that allows users to view an ad for a minimum amount of time in exchange for a reward.
Rewarded ads fill the entire display, similar to how interstitial ads are displayed. Two key differences are that video ads can be displayed in addition to banner ads, and there is a timer that must be completed before the reward is granted. The time remaining is displayed to the user as it counts down.
The user can close the ad at any time, but if it is closed before the timer is up, the reward will not be granted. The user will be prompted to confirm that they want to close the ad early and forfeit the reward.
The ad remains on screen after the timer has expired, at which point the user can close the ad without forfeiting the reward.
## Integration
We provide an interface for requesting and displaying the ads (detailed below), and we notify your code when the ad window is closed, and whether the reward was granted, through a callback.
You are responsible for integrating the rewards into your app. This includes asking the user if they want to view the ad, and handling the reward when it is granted.
Rewarded ads can be requested to be shown at any time. After requesting, there will be a short delay (1-2 seconds) while we fetch an ad before it is ready to be shown.
You must always ask the user if they want to view the ad before showing it, and you cannot mislead or incentivize the user to view the ad.
**Note on Callback Functions:** The examples below use arrow functions for callbacks. Arrow functions preserve the lexical `this` context from the enclosing scope. If you need to use `this` within your callback and are using anonymous function expressions instead of arrow functions, make sure to use `.bind(this)` to preserve the correct context:
```javascript
// Arrow function (recommended) - preserves `this` context
window.ezRewardedAds.requestAndShow((result) => {
this.handleResult(result); // `this` refers to enclosing scope
});
// Anonymous function with .bind(this) if `this` is needed
window.ezRewardedAds.requestAndShow(function(result) {
this.handleResult(result); // `this` is preserved via .bind()
}.bind(this));
```
## Reward Restrictions
You are free to choose the reward that you offer to the user. However, there are some restrictions on what you can offer:
1. The reward may not be a direct monetary item under any circumstance. Examples include cash, cryptocurrency, or gift cards.
2. The reward may be an indirect monetary item, as long as it is non-transferrable, and only redeemable within your platform, website, or app.
- Indirect monetary items are items with monetary value that aren't a direct form of payment in the real world. If the reward is a discount or voucher for a physical item, they must not exceed 25% of the item's value.
- Examples of indirect monetary items include discounts, loyalty rewards or points, product free shipping, product or service free trials, game character extra lives, and game character skins.
3. Random rewards are allowed with some restrictions:
- You must disclose the odds of receiving each reward before the user agrees to view the ad.
- Details on all rewards must be easily accessible to the user.
- The odds of receiving any reward must be greater than zero.
4. More than one rewarded ad can be required to be watched back-to-back for one reward, but this must be clearly disclosed to the user before they accept to watch the ads.
You can read more about Google's rewarded ad policies here: https://support.google.com/admanager/answer/7496282
## Initialization
The following code must be included before any other rewarded ad functions are called, no matter what integration type you are using. Ideally, this code should be included in the `` of your page, or at least at the top of the ``.
```javascript
window.ezRewardedAds = window.ezRewardedAds || {};
window.ezRewardedAds.cmd = window.ezRewardedAds.cmd || [];
```
### Javascript Integration
If you are integrated with Javascript, the page must have the `ezstandalone` initialization, as well as at least one call to initialize the ads. If the page does not already initialize ads (e.g., with `showAds()` or similar), you can use `initRewardedAds()` to load the rewarded ads code along with any other ad types you want enabled. Note that this will not request a rewarded ad, that must be done separately.
If you already have ads on the page, you only need the `ezRewardedAds` initialization code.
```html
```
If the page does not already have ads, the following code is the minimal integration required to load the rewarded ads code:
```html
```
### Cloud Integration
If you are cloud integrated, then most of the code needed to initialize rewarded ads will be loaded automatically when the page loads.
The only code you need to manually add to the `` of your site is simply:
```html
```
### Controlling Site-Wide Ad Types
When using `initRewardedAds()`, you can control which other ad types are enabled site-wide by passing a configuration object. This is useful if you want to disable certain ad types on pages that use rewarded ads, and serves as an alternative to setting up page rules in your Ezoic dashboard.
**Note:** You can still use page rules in your dashboard to control ad types if you prefer that approach. The function parameters provide a code-based alternative for developers who want to control ad placement programmatically.
#### Parameters
The `initRewardedAds()` function accepts an optional configuration object with the following properties:
- **anchor** (Boolean, optional): Whether to enable anchor ads. Default is `true`.
- **interstitial** (Boolean, optional): Whether to enable interstitial ads. Default is `true`.
- **video** (Boolean, optional): Whether to enable video ads (floating outstream). Default is `true`.
- **sideRails** (Boolean, optional): Whether to enable side rail ads. Default is `true`.
#### Usage Examples
##### Default (All Ad Types Enabled)
```javascript
ezstandalone.initRewardedAds();
// Equivalent to:
// ezstandalone.initRewardedAds({anchor: true, interstitial: true, video: true, sideRails: true});
```
##### Only Rewarded Ads (Disable All Other Types)
```javascript
ezstandalone.initRewardedAds({
anchor: false,
interstitial: false,
video: false,
sideRails: false
});
```
##### Custom Configuration
```javascript
ezstandalone.initRewardedAds({
anchor: true, // Keep anchor ads
interstitial: false, // Disable interstitial ads
video: true, // Keep video ads
sideRails: false // Disable side rail ads
});
```
## Checking if the code is loaded
The rewarded ads code will not be ready immediately after the page loads, although the delay is quite short, around 1-2 seconds on most connections.
You should also consider that the code may not ever be loaded in some situations, such as if the network request fails, or if the user is using a browser that is blocking ad code.
There are two ways to ensure that the code is loaded before calling any of the functions:
1. **Checking the `ezRewardedAds.ready` property**:
You can check if the `ezRewardedAds` object is ready by checking the `ready` property. This property will be `true` once the code is loaded.
This is useful when you have a specific moment in time that you want to display a rewarded ad, such as at the end of a level in a game, or when the user clicks a button. This also ensures that you are only interrupting user interaction when the rewarded ad code is ready to be used.
If you are using this method, you don't need to use the `cmd` queue.
```javascript
if (window.ezRewardedAds.ready) {
// Your code here
} else {
// The code is not ready yet
}
```
2. **Using the `cmd` queue**:
The `cmd` queue is a list of functions that will be called as soon as the rewarded ads code is ready.
This method is useful if you would like to show a rewarded ad as soon as possible after the page loads.
You can also use the `cmd` queue to call your own functions once the code is loaded, such as displaying a button to request a rewarded ad.
Keep in mind that if the rewarded ad code fails to load, the functions in the `cmd` queue will not be called.
```javascript
window.ezRewardedAds.cmd.push(() => {
// Your code here
});
```
## Quick Start
### Javascript Integration
The following example shows how to load the rewarded ads code and display a button that will request a rewarded ad when clicked. The button will only be displayed once the code is loaded.
```html
```
### Cloud Integration
This is the same example as above, but without the `ezstandalone` code. The rewarded ads code will be loaded automatically when the page loads.
```html
```
## Usage
### requestAndShow()
This method requests a rewarded ad and shows it immediately if the ad is ready. It takes a callback function as a parameter, which is called when the ad is closed and indicates whether the reward was granted. You must still ensure that you are notifying the user that they will be watching an ad to take an action (For example, text next to the button saying "watch an ad for a free life").
#### Usage Examples
##### Basic Example
This example shows the most basic usage. All default values are used, and we don't need to check the reward outcome, since alwaysCallback is set to `false` by default.
All we need to set is the callback function, which is the action that will be taken if and only if the reward is granted
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.requestAndShow(grantReward);
});
function grantReward() {
console.log("Reward granted!");
}
```
##### Full Example
This example uses all available options, and serves mostly as a reference to copy-paste from.
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.requestAndShow(
(result) => { // Callback function
if (result.status) {
if (result.reward) {
console.log("Reward granted!");
} else {
console.log("User closed the ad early, no reward granted.");
}
} else {
console.log("Rewarded ad request failed:", result.msg);
}
},
{ // Config object
rewardName: "Main Page Credits Reward",
alwaysCallback: true,
rewardOnNoFill: false
}
);
});
```
#### Parameters
- **callback** (Function): A function executed when the ad window is closed. The function receives a single result object with the following properties:
- **status** (Boolean):
- `true` if the ad was closed and no error occurred.
- `false` if an error occurred (e.g., if the ad was not ready or if there was an internal error).
- **reward** (Boolean): Whether the reward was granted.
- **msg** (String): A message detailing the outcome (e.g., `"ad closed"` or an error message).
- **adInfo** (Object): Information about the ad (only present when `status` is `true`). Contains:
- **id** (String): Unique identifier for the ad.
- **estimatedPayout** (Number): Estimated payout value for the ad.
- **config** (Object, optional): An object containing configuration options. The object can contain the following properties:
- **rewardName** (String, optional): The name of the rewarded ad to identify usage or location for analytics. Accepts alphanumeric characters, spaces, and some punctuation. Default is `""`.
- **alwaysCallback** (Boolean, optional): Whether to call the callback function even if the reward is not granted. Default is `false`.
- **rewardOnNoFill** (Boolean, optional): Whether to grant the reward in the event that an ad does not fill, or there is an internal error. Useful when combined with `alwaysCallback = false` and you don't want to prevent a user action if an ad cannot be shown. The reward will not be granted if the user closes the ad early. Default is `false`.
- **minCPM** (Number, optional): The minimum CPM (cost per thousand impressions) for the ad to be shown. If no ad meets this CPM, the ad will not be shown and the callback will be called with `status = false`. Default is `0`, which means there is no minimum CPM.
### requestWithOverlay()
This method requests a rewarded ad and displays a call-to-action asking the user if they want to view the ad. It provides several options for customizing the overlay, including the title, message, and button text. It also provides options for adjusting the overlay and reward behavior.
#### Parameters
- **callback** (Function): A function executed when the ad request completes. The function receives a single result object with the following properties:
- **status** (Boolean):
- `true` if the ad loaded and the user had a chance to be able to view the ad.
- `false` if an error occurred or if the ad went unfilled.
- **reward** (Boolean): Whether the reward was granted.
- **msg** (String): A message detailing the outcome (e.g., `"ad ready"` or an error message).
- **adInfo** (Object): Information about the ad (only present when `status` is `true`). Contains:
- **id** (String): Unique identifier for the ad.
- **estimatedPayout** (Number): Estimated payout value for the ad.
- **text** (Object): An object containing the text to display in the call-to-action. The object can contain the following properties:
- **header** (String, optional): The title of the call-to-action. Default is `"Watch Ad to Continue?"`.
- **body** (String Array, optional): The message to display in the call-to-action. Each array entry is one line. Default is no body text.
- **accept** (String, optional): The text for the button. Default is `"Watch ad"`.
- **cancel** (String, optional): The text for the cancel button. Default is `"Cancel"`.
- **config** (Object, optional): An object containing configuration options for the call-to-action. The object can contain the following properties:
- **rewardName** (String, optional): The name of the rewarded ad to identify usage or location for analytics. Accepts alphanumeric characters, spaces, and some punctuation. Default is `""`.
- **alwaysCallback** (Boolean, optional): Whether to call the callback function even if the reward is not granted. Default is `false`.
- **lockScroll** (Boolean, optional): Whether to lock the scroll position when the call-to-action is displayed. This is useful if you are gating content behind a reward. Default is `false`.
- **rewardOnNoFill** (Boolean, optional): Whether to grant the reward in the event that an ad does not fill, or there is an internal error. Useful when combined with `alwaysCallback = false` and you don't want to prevent a user action if an ad cannot be shown. The reward will not be granted if the user declines to watch the ad, or if they close the ad early. Default is `false`.
- **dontAsk** (Boolean, optional): If `true`, the call-to-action will not be shown, and the ad will be requested and shown immediately. You must still ensure that you are notifying the user that they will be watching an ad to take an action (For example, text next to the button saying "watch an ad for a free life"). Default is `false`.
- **minCPM** (Number, optional): The minimum CPM (cost per thousand impressions) for the ad to be shown. If no ad meets this CPM, the ad will not be shown and the callback will be called with `status = false`. Default is `0`, which means there is no minimum CPM.
#### Usage Examples
##### Basic Example
This example shows the most basic usage. All default values are used, and we don't need to check the reward outcome, since alwaysCallback is set to `false` by default.
All we need to set is
- The callback function, which is the action that will be taken if and only if the reward is granted
- The body text, which we need to set to inform the user of what the reward is for.
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.requestWithOverlay(grantReward, {body: ['You will get a reward for watching this ad.']});
});
function grantReward() {
console.log("Reward granted!");
}
```
##### Replacing a button event
This example sets `rewardOnNoFill` to `true`, which means that callback will still be called even if an ad cannot be found, or if there is another error.
This is useful if you want to replace a button event with a rewarded ad, but you don't want to prevent the user from taking the action if an ad cannot be found.
This also checks if the rewarded ads code has loaded, and if it hasn't, it will call the `takePremiumAction()` function directly.
```html
```
##### Using alwaysCallback
This example sets `alwaysCallback` to `true`, which means that the callback will be called even if the reward is not granted.
This allows you to handle the case where the user declines to watch the ad, or if an ad cannot be found.
This is useful if you want to take an action regardless of whether the ad was shown or not.
This example also demonstrates how to use an arrow function as the callback to allow for the result parameter to be passed to another function, in this case `handleRewardResult`.
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.requestWithOverlay(
(result) => {
return handleRewardResult(result);
},
{body: ["You will receive 10 credits for watching this ad."]},
{alwaysCallback: true}
);
});
function handleRewardResult(result) {
if (result.status) {
if (result.reward) {
takePremiumAction();
} else {
displayMessageToUser("Sorry, you did not receive a reward.");
}
} else {
displayMessageToUser("Sorry, there was an error loading the ad.");
// It is up to you whether to still take the premium action in this case, since the user did not have a chance to view the ad.
}
}
function takePremiumAction() {
console.log("Premium action taken!");
}
function displayMessageToUser(message) {
console.log(message);
}
```
##### Using rewardName for Analytics
This example demonstrates how to use the `rewardName` parameter to track different rewarded ad placements for analytics purposes.
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.requestWithOverlay(
grantExtraLife,
{body: ['Watch an ad to get an extra life!']},
{rewardName: 'Grant Extra Life'}
);
});
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.requestWithOverlay(
unlockPremiumFeature,
{body: ['Watch an ad to unlock this premium feature.']},
{rewardName: 'Premium Feature Unlock'}
);
});
function grantExtraLife() {
console.log("Extra life granted!");
}
function unlockPremiumFeature() {
console.log("Premium feature unlocked!");
}
```
##### Full Example
This example uses all available options, and serves mostly as a reference to copy-paste from.
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.requestWithOverlay(
(result) => { // Callback function
if (result.status) {
if (result.reward) {
console.log("Reward granted!");
} else {
console.log("User either declined to watch the ad, or they closed the ad early.");
}
} else {
console.log("Rewarded ad request failed:", result.msg);
}
},
{ // Text object
header: "Watch Ad to Continue?",
body: ["You will receive 10 credits for watching this ad.", "Click the button below to watch the ad."],
accept: "Watch ad",
cancel: "Cancel"
},
{ // Config object
rewardName: "Main Page Credits Reward",
alwaysCallback: true,
lockScroll: true,
rewardOnNoFill: false
}
);
});
```
## Advanced Usage
These functions allow for direct access to the two stages of rewarded ads: requesting and showing the ad.
You can use these functions to create your own custom overlay or UI for asking the user if they want to view the ad, or if you want to integrate the ad more directly into your site, for example a button in a game.
Keep in mind that you are still required to ask the user in some way whether they want to view the ad.
### request()
This method requests a rewarded ad. It takes a callback function as a parameter, which is called once the ad is ready, if the ad went unfilled, or if an error occurs.
- `request()` **can** be called:
- even if `show()` was never called (e.g., if the user declined to view the ad). After calling `request()` again, `show()` can then be called as normal.
- before the Ezoic rewarded ads code loads. It will be queued and executed once the code is loaded (this is the purpose of `window.ezRewardedAds.cmd.push()` in the example). There should only be a very brief delay between the page loading (or, in the case of a standalone integration, the first call to `ezstandalone.showAds()` or `initRewardedAds()`) and the rewarded ad code loading.
- once `request()` has been called, it **cannot** be called:
- until the callback is called. Only one request can be made at a time.
- while an ad is already showing (ie. after `show()` is called but before the ad window is closed).
#### Parameters
- **callback** (Function): A function executed when the ad request completes. The function receives a single result object with the following properties:
- **status** (Boolean):
- `true` if the ad is ready.
- `false` if an error occurred or if the ad went unfilled.
- **msg** (String): A message detailing the outcome (e.g., `"ad ready"` or an error message).
- **adInfo** (Object): Information about the ad (only present when `status` is `true`). Contains:
- **id** (String): Unique identifier for the ad.
- **estimatedPayout** (Number): Estimated payout value for the ad.
- **config** (Object, optional): An object containing configuration options. The object can contain the following properties:
- **minCPM** (Number, optional): The minimum CPM (cost per thousand impressions) for the ad to be shown. If no ad meets this CPM, the ad will not be shown and the callback will be called with `status = false`. Default is `0`, which means there is no minimum CPM.
#### Usage Example
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.request((result) => {
if (result.status) {
console.log("Rewarded ad is ready");
// Ask the user if they want to view the ad
} else {
console.log("Rewarded ad request failed:", result.msg);
}
});
});
```
### show()
This method displays the rewarded ad that was loaded using `request()`. It takes one parameter: a callback function that is called when the ad is closed and indicates whether the reward was granted.
#### Parameters
- **callback** (Function): A function executed when the ad window is closed. The function receives a single result object with the following properties:
- **status** (Boolean):
- `true` if the ad was closed and no error occurred.
- `false` if an error occurred (e.g., if the ad was not ready or if there was an internal error).
- **reward** (Boolean): Whether the reward was granted.
- **msg** (String): A message detailing the outcome (e.g., `"ad closed"` or an error message).
- **adInfo** (Object): Information about the ad (only present when `status` is `true`). Contains:
- **id** (String): Unique identifier for the ad.
- **estimatedPayout** (Number): Estimated payout value for the ad.
- **config** (Object, optional): An object containing configuration options. The object can contain the following properties:
- **rewardName** (String, optional): The name of the rewarded ad to identify usage or location for analytics. Accepts alphanumeric characters, spaces, and some punctuation. Default is `""`.
**Usage Example:**
```javascript
window.ezRewardedAds.show((closeResult) => {
if (closeResult.status) {
console.log("Rewarded ad closed");
if (closeResult.reward) {
console.log("Reward granted!");
}
} else {
console.log("Failed to show rewarded ad:" + closeResult.msg);
}
});
```
**Usage Example with rewardName:**
```javascript
window.ezRewardedAds.show((closeResult) => {
if (closeResult.status) {
console.log("Rewarded ad closed");
if (closeResult.reward) {
console.log("Extra credits reward granted!");
addCreditsToAccount(10);
}
} else {
console.log("Failed to show rewarded ad:" + closeResult.msg);
}
}, {
rewardName: "Extra Credits Reward"
});
function addCreditsToAccount(amount) {
console.log(`Added ${amount} credits to account`);
}
```
### register()
This method registers that a rewarded ad implementation is present on the current pageview. It creates a tracking entry so the implementation is counted, **without** requesting or showing an ad. It takes no arguments and no callback.
Use this on pages where a rewarded ad can be triggered but might not be during the pageview (for example, a reward button the user may never click). Calling `register()` ensures the implementation is still recorded, even if no ad is ultimately requested.
- `register()` is **idempotent** for a given pageview. Calling it more than once has no additional effect.
- `register()` does not need to be called before requesting an ad. It is only useful when you want to record the presence of a rewarded implementation independently of whether an ad is requested.
Like the other methods, it can be queued with the `cmd` queue so it runs as soon as the rewarded ads code is ready.
#### Usage Example
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.register();
});
```
### Complete Advanced Usage Example
Below is a full example demonstrating how to request and show a rewarded ad, including prompting the user to confirm ad viewing
The example uses the built-in `confirm()` function to ask the user if they want to view the ad. This is just an example, and you can use any method you like to ask the user if they want to view the ad.
The example uses console logging to indicate the status of the ad and whether the reward was granted. You can replace this with your own code to handle the various outcomes.
```javascript
// Ensure the global object is initialized
window.ezRewardedAds = window.ezRewardedAds || {};
window.ezRewardedAds.cmd = window.ezRewardedAds.cmd || [];
window.ezRewardedAds.cmd.push(() => { // Queue a command to request and show a rewarded ad
window.ezRewardedAds.request((requestResult) => { // Request a rewarded ad
if (requestResult.status) {
if (confirm("Watch ad for reward?")) { // Ask the user if they want to watch the ad for a reward
window.ezRewardedAds.show((closeResult) => {
if (closeResult.status) {
console.log("Rewarded ad closed");
if (closeResult.reward) {
console.log("Reward granted!");
}
} else {
console.log("Failed to show rewarded ad:" + closeResult.msg);
}
});
} else {
// User declined to watch the ad
}
} else {
console.log("Rewarded ad request failed:", requestResult.msg);
}
});
});
```
Note that the call to `show()` does not need to be inside the `request()` callback. You can call `show()` at any time after the callback provided to `request()` has been called and `result.status` is true:
```javascript
// Ensure the global object is initialized
window.ezRewardedAds = window.ezRewardedAds || {};
window.ezRewardedAds.cmd = window.ezRewardedAds.cmd || [];
// Request a rewarded ad
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.request((requestResult) => {
if (requestResult.status) {
// Ask the user if they want to view the ad. For example, display a button that calls showRewardedAd() when clicked
} else {
console.log("Rewarded ad request failed:", requestResult.msg);
}
});
});
function showRewardedAd() {
window.ezRewardedAds.show((closeResult) => {
if (closeResult.status) {
console.log("Rewarded ad closed");
if (closeResult.reward) {
console.log("Reward granted!");
}
} else {
console.log("Failed to show rewarded ad:" + closeResult.msg);
}
});
}
```
## Error Handling & Considerations
- **Single Request at a Time:**
If an ad is already loading, subsequent calls to `request()` or `requestWithOverlay()` will trigger an error callback (`result.status = false`). Wait for the current ad load to complete before calling `request()` again.
- **Ad Readiness:**
Calling `show()` when no ad is ready will result in an error callback (`result.status = false`) with a message like `"rewarded ad not ready. call request() first"`.
- **Valid Callbacks:**
Always pass a valid function to the callback argument to all methods. If an argument is not provided, or if the provided argument is not a function, a warning is logged and no further action will be taken.
## Specialized Rewarded Ad Types
### Content Locker
Content Locker is a specialized implementation of rewarded ads that gates specific content or actions behind ad viewing. It automatically selects the highest revenue ad format from multiple types (rewarded video, interstitial, or outstream video ads) to maximize revenue while providing a seamless user experience.
#### Key Features
- **Multi-format competition**: Automatically competes rewarded video ads against interstitial and outstream video ads, selecting the format with the highest CPM
- **Smart fallbacks**: If the primary rewarded ad doesn't fill, it intelligently falls back to alternative ad formats
- **Flexible actions**: Supports both URL redirects and callback functions as the gated action
- **Loading states**: Optional loading overlay to improve user experience during ad loading
- **Customizable UI**: Configurable call-to-action text and styling
#### Usage
The Content Locker is accessed via the `contentLocker()` method:
```javascript
window.ezRewardedAds.contentLocker(action, config)
```
#### Parameters
- **action** (String or Function): The action to gate behind the ad
- If a string, it will be treated as a URL to redirect to after the ad
- If a function, it will be called after the ad is completed
- **config** (Object, optional): Configuration options for the content locker
#### Configuration Options
- **loadingOverlay** (Boolean, optional): Whether to show a loading overlay while the ad loads. Default is `true`.
- **readyCallback** (Function, optional): Called when the ad is ready to show (before display). Default is `null`.
- **rewardName** (String, optional): The name of the rewarded ad to identify usage or location for analytics. Accepts alphanumeric characters, spaces, and some punctuation. Default is `""`.
- **minCPM** (Number, optional): The minimum CPM (cost per thousand impressions) for the ad to be shown. If no ad meets this CPM, the ad will not be shown and the callback will be called with `status = false`. Default is `0`, which means there is no minimum CPM.
- **callToAction** (Object, optional): Customizes the call-to-action overlay text:
- **disabled** (Boolean): If `true`, skips the overlay and shows the ad immediately. Default is `false`.
- **header** (String): Header text for the overlay. Default is `"Before Continuing"`.
- **body** (String): Body text for the overlay. Default is `"Please watch a short ad from our sponsors"`.
- **button** (String): Button text for the overlay. Default is `"Watch Ad and Continue →"`.
#### Basic Examples
##### URL Redirect
Gate a URL redirect behind an ad:
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.contentLocker("https://example.com/premium-content");
});
```
##### Function Callback
Gate a function call behind an ad:
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.contentLocker(() => {
console.log("Premium action executed!");
unlockPremiumFeature();
});
});
function unlockPremiumFeature() {
// Your premium feature code here
}
```
#### Advanced Examples
##### Custom Call-to-Action Text
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.contentLocker(
"https://example.com/premium-content",
{
callToAction: {
header: "Unlock Premium Content",
body: "Watch a quick ad to access exclusive content",
button: "Unlock Content"
}
}
);
});
```
##### With Loading Overlay and Ready Callback
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.contentLocker(
() => {
downloadFile();
},
{
loadingOverlay: true,
readyCallback: (result) => {
console.log("Ad ready status:", result.status);
},
callToAction: {
header: "Download Premium File",
body: "Support our site by watching a short ad",
button: "Start Download"
}
}
);
});
```
##### Skip Overlay (Direct Ad Display)
For cases where you want to show the ad immediately without asking:
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.contentLocker(
"https://example.com/download",
{
callToAction: {
disabled: true // Skip the overlay, show ad immediately
}
}
);
});
```
##### Using rewardName for Analytics Tracking
Track different content locker placements for analytics by using the `rewardName` parameter:
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.contentLocker(
"https://example.com/premium-article",
{
rewardName: "Premium Article Access"
}
);
});
```
#### Integration with HTML Elements
Content Locker can be easily integrated with buttons or links:
```html
```
## Rewarded Ad Events
The callbacks documented above run when the ad window **closes**. If a user earns the reward but then leaves the page without dismissing the ad, that callback never runs and the reward is lost.
The `adCompleted` event fires the moment the reward is earned, while the ad is still on screen, so you can grant the reward right away.
Register the handler once per page, as early as possible. The registration stays active for the lifetime of the page, including navigation within a single-page app.
```javascript
window.ezRewardedAds.cmd.push(() => {
window.ezRewardedAds.addEventListener("adCompleted", (result) => {
console.log("Reward earned for:", result.rewardName);
addCreditsToAccount(10);
});
});
```
To remove a handler, pass the same event name and the same function reference to `window.ezRewardedAds.removeEventListener()`.
**Note:** This is not a DOM event. Calling `window.addEventListener("adCompleted", ...)` will not work.
### Event Payload
The handler receives a single result object, shaped like the close callback's result:
- **type** (String): The event name, `"adCompleted"`.
- **status** (Boolean): Always `true` for this event.
- **reward** (Boolean): Always `true` for this event. The reward has been earned.
- **msg** (String): A message describing the event, `"ad completed"`.
- **adInfo** (Object): Information about the ad. Contains:
- **id** (String): Unique identifier for the ad. This matches the `reward_id` used by the BDA API described below.
- **estimatedPayout** (Number): Estimated payout value for the ad.
- **rewardName** (String): The `rewardName` given to the call that showed the ad. Only present when that call set one. This field is unique to the event; the close callbacks do not include it.
### Important Considerations
- **Fires at most once per ad**, for rewarded video, display, and outstream ads.
- **The event and the close callback can both run for the same ad.** Make your granting code safe to call twice, for example by tracking `adInfo.id`.
- **Keep the close callback.** This event does not replace it.
- **The handler is page-wide.** It runs for every rewarded ad completed on the page, including any that Ezoic starts on your behalf. To grant only for your own calls, set a `rewardName` on those calls and check it in the handler.
## Pulling Rewarded ID Stats via BDA API
You can retrieve detailed statistics for your rewarded ads using the BDA (Big Data Analytics) API. This allows you to track performance metrics, revenue data, and other analytics for your rewarded ad implementations.
### API Endpoint
The BDA API endpoint for custom data retrieval is:
```
POST https://api-gateway.ezoic.com/gateway/bdaservices/getCustomData/
X-API-Key: YOUR_API_KEY
```
**Note:** You can find your API key and additional documentation about generating API requests in your publisher dashboard under **Settings > API**.
### Sample Request
Here's an example of how to request information for a specific rewarded id:
**Note:** Replace `YOUR_DOMAIN_ID` with your actual domain ID (use the getDomains endpoint from the API documentation in pubdash to find this), and `YOUR_REWARD_ID` with the specific reward ID you want to query.
```bash
curl -X POST "https://api-gateway.ezoic.com/gateway/bdaservices/getCustomData/" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
--data '{
"StartDate": "2025-08-12",
"EndDate": "2025-08-13",
"Platform": "ALL",
"DomainId": YOUR_DOMAIN_ID,
"DimensionColumns": [
{
"Data": "reward_id",
"Type": "string"
},
{
"Data": "reward_status",
"Type": "string"
},
{
"Data": "is_matched",
"Type": "number"
}
],
"MetricColumns":[
{
"Data": "reward_revenue",
"Type": "number"
}
],
"Order":{
"ColumnNumber":0,
"Direction":"DESC"
},
"Filters": [
{
"Type": "INCLUDE",
"FilterKey": "reward_id",
"OperationId": "EXACTLY",
"FilterValue": "YOUR_REWARD_ID"
}
]
}'
```
### Sample Response
The API returns data in the following format:
```json
{
"Message": "",
"Success": true,
"Data": [
{
"is_matched": "1",
"reward_id": "YOUR_REWARD_ID",
"reward_revenue": "$0.00",
"reward_status": "not completed"
}
]
}
```
### Understanding the Response Data
Each data object in the response above contains the following fields:
#### is_matched
- **Type**: String (representing a number)
- **Values**: "1" (processed) or "0" (not processed)
- **Description**: Indicates whether the rewarded ad data has been fully processed in Ezoic's system. A value of "1" means the data has been processed and the revenue and status information is finalized. A value of "0" means the data is not yet processed, and the revenue or status values are not final yet.
#### reward_id
- **Type**: String
- **Description**: A unique identifier for each specific rewarded ad request. This ID is generated when a rewarded ad is requested and can be obtained from the `adInfo.id` property in the callback functions documented above. Use this ID to filter for specific rewarded ad instances in your analytics.
#### reward_revenue
- **Type**: String (formatted as currency)
- **Description**: The revenue generated from this specific rewarded ad ID.
#### reward_status
- **Type**: String
- **Possible Values**:
- "completed" - User watched the full ad and received the reward
- "not completed" - User closed the ad early or didn't meet the completion requirements
- **Description**: Indicates the completion status of the rewarded ad.
---
## Next.js
Source: https://docs.ezoic.com/docs/ezoicadsadvanced/nextjs/
Ezoic’s standalone script is designed for traditional multi-page sites. Since Next.js uses client-side navigation, we must manually trigger Ezoic to scan the DOM and cleanup placeholders.
## Global Types
Create `types/ezoic.d.ts` to ensure type safety across your components.
```typescript
declare global {
interface Window {
ezstandalone?: {
cmd: Array<() => void>;
showAds: (...ids: number[]) => void;
destroyPlaceholders: (...ids: number[]) => void;
};
}
}
export {};
```
## Global Script Initialization
Add the scripts to `app/layout.tsx`. Initializing the `cmd` queue here ensures that any subsequent calls to Ezoic are buffered until the library is fully loaded.
```typescript
import Script from "next/script";
import EzoicRouteHandler from "@/components/EzoicRouteHandler";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
## runEzoic Helper
Create `lib/ezoic.ts`. This safely pushes functions to the Ezoic command queue, preventing errors if the script hasn't initialized when a component mounts.
```typescript
export function runEzoic(fn: () => void) {
if (typeof window === "undefined") return;
window.ezstandalone = window.ezstandalone || {};
window.ezstandalone.cmd = window.ezstandalone.cmd || [];
window.ezstandalone.cmd.push(fn);
}
```
## Ad Placeholder Component
This component manages the lifecycle of a single ad slot. It ensures the ad is displayed on mount and properly destroyed on unmount.
```typescript
"use client";
import { useEffect, useState } from "react";
import { runEzoic } from "@/lib/ezoic";
export default function EzoicAd({ id }: { id: number }) {
const [isRendered, setIsRendered] = useState(false);
useEffect(() => {
setIsRendered(true);
runEzoic(() => {
window.ezstandalone?.showAds(id);
});
return () => {
runEzoic(() => {
window.ezstandalone?.destroyPlaceholders(id);
});
};
}, [id]);
return (
{isRendered && }
);
}
```
## Handling SPA Navigation (App Router)
Create `components/EzoicRouteHandler.tsx`. Since the page doesn't refresh on route changes, we use `usePathname` to trigger a re-scan of the page for new ads.
```typescript
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { runEzoic } from "@/lib/ezoic";
export default function EzoicRouteHandler() {
const pathname = usePathname();
useEffect(() => {
runEzoic(() => {
window.ezstandalone?.destroyPlaceholders();
requestAnimationFrame(() => {
window.ezstandalone?.showAds();
});
});
}, [pathname]);
return null;
}
```
## Pages Router Implementation
For the Pages Router, handle the route events inside `_app.tsx`.
```typescript
import { useEffect } from "react";
import { useRouter } from "next/router";
import { runEzoic } from "@/lib/ezoic";
export default function App({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const handleRoute = () => {
runEzoic(() => {
window.ezstandalone?.destroyPlaceholders();
window.ezstandalone?.showAds();
});
};
router.events.on("routeChangeComplete", handleRoute);
return () => router.events.off("routeChangeComplete", handleRoute);
}, [router.events]);
return ;
}
```
---
## Framework SDKs
Source: https://docs.ezoic.com/docs/ezoicadsadvanced/framework-sdks/
Ezoic publishes official open-source SDKs that wrap the EzoicAds standalone
integration for the major JavaScript frameworks. Each SDK manages the consent
(CMP) and ad scripts in the required order, provides native components or
services for ad placeholders, and handles single-page-app navigation so you
don't have to hand-write the raw snippets.
Using an AI coding assistant? The [AI Setup Assistant](/docs/ezoicads/setup-mcp/) can add these SDKs to your project.
| Framework | Package | Source |
| --------- | ------- | ------ |
| Angular | `@ezoic/angular-sdk` | [github.com/ezoic/ezoic-angular-sdk](https://github.com/ezoic/ezoic-angular-sdk) |
| React | `@ezoic/react-sdk` | [github.com/ezoic/ezoic-react-sdk](https://github.com/ezoic/ezoic-react-sdk) |
| Vue 3 | `@ezoic/vue-sdk` | [github.com/ezoic/ezoic-vue-sdk](https://github.com/ezoic/ezoic-vue-sdk) |
The SDKs are under active development (pre-1.0). See each repository's README
for current install instructions, supported features, and roadmap.
These SDKs are for **web apps** running in the browser. Building a React
Native **mobile app** for iOS or Android? Use the separate
[Ezoic React Native SDK](/docs/mobileapps/react-native/)
(`@ezoic/react-native-sdk`), which bridges Ezoic's native mobile ad stacks.
## What the SDKs handle for you
- **Script management** - the Gatekeeper consent scripts, the
`ezstandalone.cmd` queue stub, and the async `sa.min.js` bundle are injected
once, in the correct order, idempotently, and SSR-safe.
- **Display ads** - native placeholder components (for example `` in
React and Vue, `EzoicService` in Angular) with typed `showAds`,
`refreshAds`, and `destroyPlaceholders` passthroughs.
- **Single-page-app routing** - page-view hooks that destroy and re-request
ads on client-side navigation.
- **Consent** - helpers for reading TCF consent state and the
[configuration options](/docs/ezoicadsadvanced/advanced/#available-options)
such as format toggles.
- **Rewarded ads and video** - components and services for
[rewarded ads](/docs/ezoicadsadvanced/rewarded/) and Ezoic video, rolling
out per the roadmap in each repository.
For Next.js specifically, see the [Next.js guide](/docs/ezoicadsadvanced/nextjs/).
## Live Examples
Working example implementations of every integration style are available at
[examples.ezoic.com](https://examples.ezoic.com/) - id-less setup, placement
IDs, Angular, React, Vue, and Web Games - each with copyable source. The
entire examples site is open source at
[github.com/ezoic/examples.ezoic.com](https://github.com/ezoic/examples.ezoic.com).
# Identity
---
## Getting started
Source: https://docs.ezoic.com/docs/identity/getting-started/
## Overview
The Ezoic Identity product allows publishers to share a hashed version of a user's email with vendors such as Google and Prebid's User Identity modules and bidders. Sharing hashed emails or phone numbers can provide a large uplift as it allows advertisers to better reach their target audiences without the use of cookies. This document will guide you through the steps to get started.
Please ensure compliance with any applicable regulations regarding disclosures and obtain required consent before sharing user data with upstream vendors.
## Passing Hashed or Unhashed User Emails
Call the following function to pass either sha256, md5, or sha1 hashes of a user's plaintext email address. You may also submit an unhashed email which Ezoic will hash for you for maximum coverage before passing it to our vendors.
### Unhashed Email
```javascript
window.ezoicIdentity = window.ezoicIdentity || {};
window.ezoicIdentity.queue = window.ezoicIdentity.queue || [];
window.ezoicIdentity.queue.push(function(){
window.ezoicIdentity.setIdentity({
email: PLAINTEXT_EMAIL
});
});
```
### Hashed Email
When passing a hashed email address, please note the following:
- Validate the email address, e.g., through use of a regular expression
- Remove leading and trailing whitespace
- Convert all characters to lowercase
- hash the email using sha256, md5, or sha1 **(sha256 is recommended)**
- In gmail.com email addresses, remove the following characters from the username part of the email address:
- Periods: `john.smith@gmail.com` should look like `johnsmith@gmail.com` before hashing
- Plus signs and any characters following up until the `@`: `johnsmith+home@gmail.com` to `johnsmith@gmail.com`
At least one hashed email address must be passed to the function. If you have multiple hashed emails, you can pass them all. SHA256 is recommended, but you can pass any combination of the three hashes.
```javascript
window.ezoicIdentity = window.ezoicIdentity || {};
window.ezoicIdentity.queue = window.ezoicIdentity.queue || [];
window.ezoicIdentity.queue.push(function(){
window.ezoicIdentity.setIdentity({
md5: MD5_HASHED_EMAIL,
sha256: SHA256_HASHED_EMAIL,
sha1: SHA1_HASHED_EMAIL
});
});
```
### Integrated Email Service IDs
If you've already integrated your email service provider with Ezoic, you can pass the custom user ID, also known by `zuserid` or `zid`, via the JS.
```javascript
window.ezoicIdentity = window.ezoicIdentity || {};
window.ezoicIdentity.queue = window.ezoicIdentity.queue || [];
window.ezoicIdentity.queue.push(function(){
window.ezoicIdentity.setIdentity({
userid: USER_ID
});
});
```
### Phone Number (Hashed or Unhashed)
When providing a phone number, you are able to pass in either a hashed or unhashed phone number. If you pass in only an unhashed phone number, Ezoic will hash it for you before passing it to our vendors. You are also able to hash the phone number yourself using the SHA-256 algorithm and provide only the hash when calling the function. When hashing the phone number, please note the following before hashing:
- Ensure the phone number is in [E.164](https://en.wikipedia.org/wiki/E.164) format, e.g., through use of a regular expression
- E.164 phone numbers can have a maximum of fifteen digits
- Normalized E.164 phone numbers use the following syntax with no spaces, hyphens, paraenthesis, or other characters:
- `[+][country code][area code][local phone number]`
- US Example: `1(234)567-8901` would be normalized to `+12345678901`
- Signapore: `65 1234 5678` would be normalized to `+6512345678`
- Sydney, Australia: `(02) 1234 5678` is normalized to drop the leading zero for the city then prepend the country code to `+61212345678`
```javascript
window.ezoicIdentity = window.ezoicIdentity || {};
window.ezoicIdentity.queue = window.ezoicIdentity.queue || [];
window.ezoicIdentity.queue.push(function(){
window.ezoicIdentity.setPhoneNumber({
phone: UNHASHED_PHONE_NUMBER,
sha256: SHA256_HASHED_PHONE_NUMBER
});
});
```
### Integrated Phone Service IDs
If you've already integrated your phone notification service provider with Ezoic, you can pass the custom user ID, also known by `zuserid` or `zid` in the phone service provider, via the JS.
```javascript
window.ezoicIdentity = window.ezoicIdentity || {};
window.ezoicIdentity.queue = window.ezoicIdentity.queue || [];
window.ezoicIdentity.queue.push(function(){
window.ezoicIdentity.setPhoneNumber({
userid: USER_ID
});
});
```
### Deleting an Identity
If you need to remove a previously set identity from the page, you can call `deleteIdentity()`. This will clear any identity values stored in memory for the current session and remove any Ezoic Identity cookies / storage values that may have been created.
```javascript
window.ezoicIdentity = window.ezoicIdentity || {};
window.ezoicIdentity.queue = window.ezoicIdentity.queue || [];
window.ezoicIdentity.queue.push(function(){
window.ezoicIdentity.deleteIdentity();
});
```
### Testing
The function creates a cookie in the browser's session storage called `ezidentity` that holds the hashes that were passed through. To test the implementation simply look for the `ezidentity` cookie.
---
## Newsletter widgets
Source: https://docs.ezoic.com/docs/identity/newsletter-widgets/
Use `ezstandalone.showNewsletter` to collect newsletter emails on your site. Visitors submit through a built-in overlay, a sized in-page unit, or your existing signup form. Captured emails appear in **Identity → Captured Emails**. Identifying readers who already arrive from a newsletter campaign is a separate step — see [Email Service Integration](/docs/identity/email-service-integration/).
Ezoic's email capture is skipped when visitor privacy signals deny collection (for example Global Privacy Control, GDPR consent missing the storage purpose, or US state privacy opt-outs). Publisher-owned custom form submits to your own ESP are unaffected.
`showNewsletter` returns **`true`** if the request was accepted (or queued to show), and **`false`** for an unknown `placement` or if the page is not ready. A missing or undersized native container does not mount a widget — do not treat the first call’s return value as a size check. That return value is not whether an email was captured.
Ezoic does not auto-open these widgets. Call `showNewsletter` from a button, a completed article, a paywall, or any other moment you choose. There is no scroll, time, or click trigger built in.
## Interstitial
The interstitial is a centered card over a dimmed page, with a close control. Use it when you want the visitor's full attention.
```html
```
Default copy is **Subscribe to our newsletter**, **Enter your email to stay updated.**, and a **Subscribe** button. A **Privacy Policy** link appears when you pass `privacyPolicyUrl` or when a safe `http(s)` privacy policy URL is already available for the site. The link is omitted otherwise.
## Native in-page units
Native units mount into a container you already have on the page. Fixed sizes use IAB boxes. `fluid` fills whatever box you give it.
| `size` | Layout |
|---|---|
| `300x250` | Stacked: title, body, joined email + Subscribe, Privacy Policy when a URL is available |
| `336x280` | Same stacked layout, larger box |
| `728x90` | Row: title + form. No body, no Privacy Policy |
| `320x50` | Form only |
| `fluid` | Stacked layout sized to the container at mount |
The host element must already be in the document. For a fixed size, the host must be **at least** that width and height or nothing is rendered. `fluid` uses the container's box at mount time (it does not keep stretching on later resize).
```html
```
Fluid example — the widget stretches to the container:
```html
```
You can pass a live node as `element` instead of `containerId`. Do not pass HTML strings.
## Existing form (`custom`)
If you already have a Mailchimp, Mailjet, Klaviyo, or other newsletter form, bind it without replacing the markup. Ezoic copies the email and posts it in the background. Your form still submits to your ESP — the call does not `preventDefault` or restyle the form.
```html
```
With no host argument, Ezoic finds newsletter-like forms on the page (email field plus a subscribe/sign up/join control, or a form marked for a known ESP) and also watches for forms that mount later. Login forms with a password field are skipped.
To target one form:
```js
ezstandalone.cmd.push(function () {
ezstandalone.showNewsletter({
placement: "custom",
containerId: "my-newsletter-form"
});
});
```
Optional markers on your markup: `[data-ez-newsletter]` on the host, `[data-ez-email]` on the email field, `[data-ez-submit]` on the submit control. `copy` and `colors` are ignored on `custom` — keep styling the form yourself.
## Customize colors
Layout and typeface stay fixed. You can change colors with a `colors` object on the call, or with CSS variables on `[data-ez-newsletter]`.
This example uses a teal accent (`#0f766e`) on the interstitial:
```js
ezstandalone.cmd.push(function () {
ezstandalone.showNewsletter({
placement: "interstitial",
colors: {
accent: "#0f766e",
accentText: "#ffffff"
}
});
});
```
The same tokens apply to every native size. Layouts stay as in the table above; only colors change:
```js
ezstandalone.cmd.push(function () {
ezstandalone.showNewsletter({
placement: "native",
size: "336x280",
containerId: "newsletter-slot",
colors: {
accent: "#0f766e",
accentText: "#ffffff",
border: "#99f6e4",
inputBackground: "#f0fdfa"
}
});
});
```
Or set the CSS variables once for every widget on the page:
```css
[data-ez-newsletter] {
--ez-newsletter-accent: #0f766e;
--ez-newsletter-accent-ink: #fff;
--ez-newsletter-border: #99f6e4;
--ez-newsletter-input-bg: #f0fdfa;
}
```
| JS `colors` key | CSS variable | Default |
|---|---|---|
| `background` | `--ez-newsletter-bg` | `#fff` |
| `text` | `--ez-newsletter-ink` | `#1a1a1a` |
| `muted` | `--ez-newsletter-muted` | `#6b6b6b` |
| `accent` | `--ez-newsletter-accent` | `#111827` |
| `accentText` | `--ez-newsletter-accent-ink` | `#fff` |
| `border` | `--ez-newsletter-border` | `#eee` |
| `inputBackground` | `--ez-newsletter-input-bg` | `#f4f4f4` |
| `overlay` | `--ez-newsletter-overlay` | `rgba(0,0,0,0.5)` |
Use hex (`#0f766e`) or `rgb()` / `hsl()` values. Invalid strings are skipped; the widget still shows with remaining defaults. Overlay only applies to the interstitial.
## Customize copy
Override title, body, button label, and optional purpose text. Blank values fall back to the defaults above.
```js
ezstandalone.cmd.push(function () {
ezstandalone.showNewsletter({
placement: "interstitial",
copy: {
title: "Get new posts in your inbox",
body: "One email a week. No spam.",
submitLabel: "Subscribe"
},
purposeText: "We'll email you new articles and site updates.",
privacyPolicyUrl: "https://www.example.com/privacy-policy",
colors: {
accent: "#0f766e",
accentText: "#ffffff"
}
});
});
```
`728x90` still hides body and the legal line. `320x50` still shows only the form, even if you pass a custom title and body.
---
## Email Service Integration
Source: https://docs.ezoic.com/docs/identity/email-service-integration/
To collect new subscribers on your site first, see [Newsletter widgets](/docs/identity/newsletter-widgets/). To identify visitors who already arrive from newsletters and email campaigns, sync your list as described below.
Our identity solution empowers publishers to optimize ad performance for traffic originating from newsletters and email campaigns. By passing secure signals through URL parameters, we can leverage hashed email addresses and other first-party data. This data is integrated with various identity solutions, allowing us to enhance ad bid requests. As a result, advertisers can place higher bids and increase ad fill rates, ultimately boosting revenue for publishers.
**Here's how it works**:
1. ***Syncing Emails***: Publishers sync their email lists with Ezoic's secure system, which utilizes a unique subscriber or contact ID provided by the newsletter platform. If a subscriber/contact ID isn't available, Ezoic generates a unique ID and adds it back to the publisher's email list as a tag, merge tag, or custom field, depending on their email service provider. This ID is then used by Ezoic to identify website visitors when they arrive from newsletters or email campaigns. Ezoic supports direct API integrations with many top email newsletter providers, and also offers the option to connect through our Ezoic API for seamless integration with any other provider.
2. ***Embedding IDs in Links***: The publisher includes this unique user ID as a UTM parameter in any outbound website links within their newsletters or email campaigns (for example: `www.publisher-website.com/content-page?utm_content=zid-123456`).
3. ***Identifying Visitors***: When the user arrives on the website after clicking a link containing one of these user IDs, Ezoic reads the ID from the URL. We use this ID to identify the user and generate an email hash, which is then sent, along with any other available first-party data, to multiple identity providers, such as UID2 by The Trade Desk.
4. ***Improving Ad Targeting***: Advertisers and agencies using demand-side platforms (DSPs) can match the email hashes to those their campaigns are targeting, effectively recognizing users across different platforms. The identity tokens derived from these hashes are then sent to the DSPs, enabling them to bid on ad inventory that aligns with their campaigns' target criteria.
This process leads to improved fill rates from demand partners and higher CPM rates. For instance, on iOS devices, whether using Safari or Chrome, advertisers cannot run premium retargeting campaigns that rely on third-party cookies, as these are not available on iOS. However, with an email hash, advertisers can still match their retargeting campaigns to customers or leads based on their email addresses.
## Getting Started with Integration
Ezoic offers direct API integrations with many popular providers and provides our own simple, secure API for all other connections. Click on one of the links below for your preferred integration method to get step-by-step instructions.
[BeeHiiv](/docs/identity/integrations/beehiiv/)
[ActiveCampaign](/docs/identity/integrations/activecampaign/)
[MailChimp](/docs/identity/integrations/mailchimp/)
[Kit](/docs/identity/integrations/kit/)
[Mailerlite](/docs/identity/integrations/mailerlite/)
---
## Embedding IDs in Links
Source: https://docs.ezoic.com/docs/identity/embedding-ids-in-links/
You can include a user id, email hash, or phone number hash in any URL parameter and Ezoic will automatically read it and provide the identity information to advertising providers. The **value prefix** determines what type of identity is being passed — email or phone.
We recommend using a UTM parameter, though ***any*** parameter will work. The parameter name does not matter — only the value prefix.
## Email Identity
| Field | Prefix | Example |
|-------|--------|---------|
| User ID | `zid` | www.publisher-website.com/mypage?utm_content=zid-123 |
| MD5 | `zmd5` | www.publisher-website.com/mypage?utm_source=zmd5-1a2b3c |
| SHA256 | `zsha256` | www.publisher-website.com/mypage?utm_myparam=zsha256-1a2b3c |
| SHA1 | `zsha1` | www.publisher-website.com/mypage?myparam=zsha1-1a2b3c |
Providing the user ID will often be the most effective way to pass identity data but will only be available if you have completed an email service integration.
You can include multiple hashed emails in the URL. SHA256 is recommended, but you can pass any combination of the three hashes.
## Phone Number Identity
| Field | Prefix | Example |
|-------|--------|---------|
| SHA256 | `zphonesha256` | www.publisher-website.com/mypage?utm_phone=zphonesha256-1a2b3c |
`zsha256` is for **email** hashes only. For phone number hashes, use `zphonesha256`. Using `zsha256` with a phone hash will cause it to be treated as an email hash.
Phone numbers should be normalized to [E.164](https://en.wikipedia.org/wiki/E.164) format before hashing. See the [Getting Started](/docs/identity/getting-started/#phone-number-hashed-or-unhashed) page for normalization details.
## Passing Both Email and Phone
You can pass both email and phone identity in the same URL using separate parameters:
```
www.publisher-website.com/mypage?utm_email=zsha256-&utm_phone=zphonesha256-
```
---
## Google One Tap
Source: https://docs.ezoic.com/docs/identity/google-one-tap/
## What is Google One Tap?
Google One Tap is a user-friendly sign-in mechanism provided by Google Identity Services. It allows users to quickly and securely sign in or sign up to your site with a single click—no password required. One Tap presents a subtle prompt at the top of the page, enabling seamless authentication using their Google account. This improves conversion rates, enhances user experience, and reduces friction during sign-in flows.
## Why Google One Tap Matters for Publishers
Implementing Google One Tap with Ezoic enables your site to automatically collect **identified users**—visitors who have signed in using a verified identity like their Google account. This identity data is critical for maximizing monetization:
- **Improved Targeting**: Advertisers pay more for impressions served to known, verified users.
- **Increased Revenue**: Identity unlocks access to higher-yield demand sources and helps generate better match rates in header bidding.
- **Privacy-First Compliance**: Identity-based ads support long-term ad revenue while aligning with industry privacy standards and cookieless future requirements.
The more users who sign in through One Tap, the more valuable your traffic becomes.
## How to Set Up Google One Tap
To implement Google One Tap on your site through Ezoic, follow the step-by-step instructions provided in this support article:
[How to Set Up Google One Tap for Identity](https://support.ezoic.com/kb/article/how-to-set-up-google-one-tap-for-identity)
## Controlling Automatic Prompts
In the publisher dashboard, go to **Settings → Identity → Google One Tap**. The **Don't show sign-in prompts automatically** toggle controls whether Ezoic shows sign-in UI on its own.
When enabled, Ezoic will not show the One Tap prompt or the fallback sign-in modal automatically. The One Tap library still loads on your pages so you can trigger sign-in when you choose—for example, from a button click or another user action.
When disabled (the default), Ezoic shows One Tap and the fallback modal according to its normal automatic behavior.
## Triggering One Tap Manually
Sites using EzoicAds standalone (`ezstandalone`) can show the Google One Tap prompt on demand with `ezstandalone.showOneTap()`.
The method returns:
- **`true`** if the prompt was shown or queued. If you call it before the One Tap library has finished initializing, the prompt appears as soon as it is ready.
- **`false`** if One Tap is unavailable on the page—for example, One Tap is not enabled for the domain, no Google client ID is configured, or the visitor is already signed in.
`showOneTap()` works regardless of the **Don't show sign-in prompts automatically** setting. It also ignores Ezoic's 24-hour snooze that normally applies after a visitor dismisses the prompt.
Example using the standard command-queue pattern:
```html
```
Google applies its own frequency capping (a cooldown after repeated dismissals). That behavior is controlled by Google, not Ezoic, and still applies when you trigger One Tap manually.
---
## Social Login Data Deletion
Source: https://docs.ezoic.com/docs/identity/social-login-data-deletion/
## Social Login Data Deletion
Publisher websites may use Ezoic Social Login to let visitors sign in with a social identity provider. When you choose to sign in, the provider may share the email address you authorize for that website. Ezoic uses that email address to create identity signals for advertising, analytics, and related site services where permitted.
Ezoic Social Login does not collect your social provider password and does not store long-term social provider access tokens.
## Request Deletion From Ezoic
These instructions cover Ezoic-controlled Social Login identity data. They do not cover account data controlled by the publisher website or data controlled by your social identity provider.
To request deletion of Ezoic-controlled identity data associated with your Social Login email, contact Ezoic at [privacy@ezoic.com](mailto:privacy@ezoic.com) and include:
- The email address used with Social Login
- The website where you used Social Login
- A request to delete your Ezoic Social Login identity data
Ezoic will use the provided email address to locate the corresponding identity records, including hashed email identifiers, and process the request by deleting or suppressing Ezoic-controlled identity data, subject to legally required retention.
## Clear Identity Data From Your Browser
Some Social Login identity data is stored in your browser for the website where you signed in. Because browser storage is scoped to each website, an Ezoic-hosted page cannot reliably clear that data from every publisher site.
To clear browser-side identity data, return to the website where you used Social Login and use the site's clear identity, sign out, or delete sign-in data option if one is available. If the site does not provide a clear identity option, you can clear cookies and site data for that website in your browser settings or contact the website operator for help.
Publishers can clear Ezoic browser-side identity data from their own site by running:
```javascript
window.ezoicIdentity?.deleteIdentity?.();
```
This clears Ezoic identity data stored in that browser for the current site, including the `ezidentity` identity state and supported Ezoic identity provider storage.
## Remove Provider App Access
You may also remove the app or site from your social provider account settings. Removing app access at the provider does not automatically delete Ezoic-controlled identity data. To request deletion from Ezoic, use the contact instructions above.
---
## Visitor Accounts
Source: https://docs.ezoic.com/docs/identity/visitor-accounts/
## What are Visitor Accounts?
**Visitor Accounts** lets your site's visitors create accounts, sign in, stay signed in across page loads, verify their email, and reset forgotten passwords — all on your own domain, with no authentication backend to build or maintain. Ezoic hosts the account system; you add a small script and either a drop-in form or your own custom UI powered by the `window.ezAuth` JavaScript API.
Visitor accounts are isolated per site: an account created on your domain exists only for your domain.
Supported sign-in methods:
- **Email + password** — works out of the box, no provider setup.
- **Google sign-in** — server-verified Google Identity Services login, available once you configure a Google client ID in your dashboard.
Visitor Accounts also powers visitor sign-in for [Ezoic Subscriptions](/docs/subscriptions/visitor-authentication/). If you use the Subscriptions widget, it loads and reuses the same `window.ezAuth` integration automatically.
## The Dashboard
In your Ezoic dashboard, go to **Identity → Visitor Accounts**. The page shows account activity for the selected site:
- **Total accounts** and **verified accounts**
- **New signups** over the last 7 and 30 days
- **Logins** over the last 7 and 30 days
- A 30-day daily signups chart
- **Export accounts (CSV)** — download your site's visitor accounts, including account ID, email, status, verification state, and creation/update timestamps
The same page links to the login provider settings where you can enable Google sign-in, and shows the copy-paste integration snippets below pre-filled with your site's ID.
## Quick Start
### Step 1 — Add the SDK
Add this once, near the top of ``, on every page where visitors can sign in or need to be recognized. Replace `YOUR_DOMAIN_ID` with your Ezoic site ID (shown pre-filled on the Visitor Accounts dashboard page):
```html
```
The script publishes `window.ezAuth` and automatically restores any persisted login, so returning visitors are signed in on every page load.
You can also configure via a script-tag attribute instead of the global:
```html
```
Pages using the SDK must be served from the domain configured with Ezoic. Requests from other origins are rejected with an `origin_mismatch` error.
### Step 2 — Drop in the form
The fastest integration is Ezoic's built-in sign-in / create-account form. Add an element and mount the form into it:
```html
```
The form handles login, registration, forgotten passwords, password reset, and email verification. When a visitor lands from an emailed reset or verification link, the form automatically shows the right screen. If a visitor tries to register with an email that already has an account, the error message links them to the sign-in screen with that email prefilled: "An account with that email already exists. Try signing in instead."
`mount(target, options)` accepts an optional second argument:
- `mode` — initial view, `"login"` (default) or `"register"`
- `showToggle` — whether the form shows the link to switch between sign-in and create-account (default `true`)
- `onScreenChange` — callback fired whenever the active screen changes, including the initial paint. Receives one of `"login"`, `"register"`, `"forgot"`, `"reset"`, `"verify"`, `"signed-in"`, or `"notice"` — the same values as the `ezauth--` modifier class. Useful when your page renders its own chrome (headings, buttons) around the form and needs to keep it in sync. The callback fires before the new screen's DOM is painted, so don't read form elements inside it.
It returns a handle with an `unmount()` method.
#### Styling the form
The form ships **unstyled on purpose** — it emits semantic markup with a stable set of CSS classes so it inherits your site's look. Add your own stylesheet targeting those classes.
The mounted root is `
`, where the modifier class reflects the current screen (`ezauth--login`, `ezauth--register`, `ezauth--forgot`, `ezauth--reset`, `ezauth--verify`, `ezauth--signed-in`, `ezauth--notice`). Every button carries `ezauth__button` and every text input carries `ezauth__input`, plus a specific class per control (`ezauth__submit`, `ezauth__email`, `ezauth__password`, `ezauth__toggle`, `ezauth__forgot-link`, `ezauth__back`, `ezauth__logout`), with `ezauth__form`, `ezauth__heading`, `ezauth__field`, `ezauth__label`, and `ezauth__message` (inline errors) for structure. These class names are a stable contract and will not change.
A minimal starting point:
```css
.ezauth {
max-width: 360px;
margin: 0 auto;
padding: 24px;
border: 1px solid #d2d6dc;
border-radius: 8px;
}
.ezauth__form { display: flex; flex-direction: column; gap: 14px; }
.ezauth__input { width: 100%; padding: 10px 12px; border: 1px solid #d2d6dc; border-radius: 8px; }
.ezauth__button { border: none; border-radius: 8px; padding: 10px 14px; cursor: pointer; }
.ezauth__submit { background: #2b6cb0; color: #ffffff; font-weight: 600; }
.ezauth__toggle, .ezauth__forgot-link, .ezauth__back { background: transparent; color: #2b6cb0; padding: 4px 0; text-align: left; }
.ezauth__message { color: #c53030; font-size: 0.85rem; }
.ezauth__message:empty { display: none; }
```
Scope your rules to `.ezauth` so they cannot leak into the rest of your page. Mounting multiple forms on one page is supported — the classes are shared, so one stylesheet styles them all.
### Step 3 (optional) — Build a custom UI
For full control, skip the drop-in form and drive authentication from JavaScript:
```javascript
// React to sign-in / sign-out anywhere on your site.
window.ezAuth.onChange(function (state) {
if (state.status === "authenticated") {
console.log("signed in as account #" + state.user.accountId);
}
});
// Email + password.
window.ezAuth.register("visitor@example.com", "their-password");
window.ezAuth.login("visitor@example.com", "their-password");
window.ezAuth.logout(); // this device
window.ezAuth.logoutAll(); // every device
// Forgotten password (the SDK reads the emailed token off the landing URL).
window.ezAuth.forgotPassword("visitor@example.com");
```
## JavaScript API Reference
All methods live on `window.ezAuth`. The SDK initializes itself from your embed config; every action method rejects with a `not_initialized` error if called before configuration.
### Reading state
| Method | Returns | Description |
|---|---|---|
| `isAuthenticated()` | `boolean` | Whether a visitor is signed in. |
| `getUser()` | `{ accountId, emailVerified }` or `null` | The signed-in visitor. Token-free — no email address is exposed. |
| `getState()` | `{ status, user }` | `status` is `"authenticated"` or `"anonymous"`. |
| `onChange(listener)` | unsubscribe function | Subscribes to auth-state changes. Fires immediately with the current state, then on every change. Safe to call before the SDK initializes. |
| `isInitialized()` | `boolean` | Whether the SDK has been configured. |
| `getDomainId()` | `number` or `null` | The configured site ID. |
### Email + password
| Method | Returns | Description |
|---|---|---|
| `register(email, password)` | `Promise` | Creates an account and signs the visitor in. A verification email is sent automatically. |
| `login(email, password)` | `Promise` | Signs the visitor in. |
| `logout()` | `Promise` | Signs out on this device. |
| `logoutAll()` | `Promise` | Signs out everywhere; resolves with the number of sessions revoked. |
### Sessions
Sessions persist in the browser and are restored automatically on page load — you normally never call these directly:
| Method | Returns | Description |
|---|---|---|
| `restore()` | `Promise` | Restores a persisted session, if any. |
| `refresh()` | `Promise` | Forces a session refresh. |
| `getAccessToken()` | `Promise` | The visitor's current access token (refreshed automatically), or `null` when signed out. Useful when your own code needs to identify the visitor's session. |
### Google sign-in
Google sign-in is server-verified: your page obtains an ID token from [Google Identity Services](https://developers.google.com/identity/gsi/web) (GIS) and hands it to the SDK, which verifies it with Ezoic's servers before issuing a session.
| Method | Returns | Description |
|---|---|---|
| `getOAuthConfig()` | `Promise<{ google: { enabled, clientId } }>` | Which providers are configured for your site — use it to decide whether to render a Google button. `clientId` is the public Google client ID to pass to GIS. |
| `getOAuthNonce()` | `Promise` | A single-use, server-issued nonce to pass to GIS when requesting the ID token. |
| `loginWithGoogleCredential(idToken)` | `Promise` | Completes the login from a GIS ID token. |
```javascript
// Google sign-in (server-verified). Only offer it when configured.
window.ezAuth.getOAuthConfig().then(function (cfg) {
if (cfg.google && cfg.google.enabled) {
// Use cfg.google.clientId with Google Identity Services, request a nonce
// via window.ezAuth.getOAuthNonce(), then complete the login:
// window.ezAuth.loginWithGoogleCredential(googleIdToken);
}
});
```
### Password reset
| Method | Returns | Description |
|---|---|---|
| `forgotPassword(email)` | `Promise` | Emails a reset link. Resolves the same way whether or not the account exists. |
| `getResetToken()` | `string` or `null` | The reset token from the current page URL (`?ezauth_reset_token=...`), if present. |
| `resetPassword(token, newPassword)` | `Promise` | Sets a new password using the token from the emailed link. |
The emailed link points back to your site with an `ezauth_reset_token` query parameter. The drop-in form detects it and shows the "choose a new password" screen automatically; in a custom UI, check `getResetToken()` on page load. A successful reset revokes the account's existing sessions and does **not** sign the visitor in — they log in with the new password. Reset links expire after 1 hour.
### Email verification
| Method | Returns | Description |
|---|---|---|
| `verifyEmail(token)` | `Promise` | Completes verification using the token from the emailed link. Resolves `true` once the email is verified. |
| `resendVerification(email)` | `Promise` | Re-sends the verification email. Resolves the same way whether or not the account exists. |
| `getVerifyToken()` | `string` or `null` | The verification token from the current page URL (`?ezauth_verify_token=...`), if present. |
Registration sends a verification email automatically; the visitor is signed in right away with `emailVerified: false` until they click the link. The emailed link points back to your site with an `ezauth_verify_token` query parameter — the drop-in form handles it automatically, or check `getVerifyToken()` in a custom UI. Verification links expire after 24 hours.
### Errors
Every rejected promise carries an `EzAuthError` with a stable `code` (and the HTTP status when the failure came from the server). Messages never contain tokens, passwords, or emails.
| Code | Meaning |
|---|---|
| `not_initialized` | An action method was called before the SDK was configured. |
| `invalid_request` | The request was malformed (for example, a missing or invalid email). |
| `unauthorized` | Wrong credentials, or the session is no longer valid. |
| `account_exists` | Registration attempted with an email that already has an account. The drop-in form handles this itself, linking the visitor to sign in with the email prefilled. |
| `oauth_not_configured` | Google sign-in was attempted but is not enabled for the site. |
| `invalid_token` | The reset or verification token is invalid, expired, or already used. |
| `password_compromised` | The chosen password appears in known data breaches — ask the visitor to pick another. |
| `rate_limited` | Too many attempts; try again later. |
| `origin_mismatch` | The page's origin does not match the configured domain. |
| `upstream_error` / `internal_error` / `network_error` / `invalid_response` | A transient server or network problem. |
## Security
- Passwords are checked against known breach corpuses ([Have I Been Pwned](https://haveibeenpwned.com/Passwords)); compromised passwords are rejected on registration and reset.
- Repeated failed logins temporarily lock the account, and registration attempts are rate-limited per IP.
- Login and password-recovery responses never reveal whether an account exists for an email address.
- Sessions use short-lived access tokens refreshed automatically by the SDK; requests are only accepted from your configured domain over HTTPS.
- `getUser()` exposes only an account ID and verification status — visitor email addresses are available to you through the dashboard CSV export, not through the on-page API.
## Related
- [Subscriptions: Visitor Authentication](/docs/subscriptions/visitor-authentication/) — how Ezoic Subscriptions uses visitor accounts for checkout and content access.
- [Google One Tap](/docs/identity/google-one-tap/) — One Tap prompt for identity-based ad monetization (separate from Google sign-in for visitor accounts).
---
## BeeHiiv
Source: https://docs.ezoic.com/docs/identity/integrations/beehiiv/
For our BeeHiiv integration, you'll need an API key. You can find this by navigating to the settings section of your BeeHiiv dashboard and locating the integrations page, as shown in the screenshot below:
## Adding UTM Tagging to your links:
Once the integration is complete, you'll notice a new custom text field called `zid` has been added to each email subscriber on your list, as shown in the screenshot below:
Now that your subscriber list has been synced with these new Ezoic user IDs as custom fields in BeeHiiv, you'll need to insert them into your newsletter/email campaign links using whichever UTM parameter you prefer.
**For example:**
If your current link goes to `https://www.content-website.com/page`, you would add the BeeHiiv custom field dynamic parameter for `zid` into a UTM parameter, such as `utm_content`. This would make your URL look like this:
`https://www.content-website.com/page?utm_content=zid-{{zid}}`
To **automatically add the UTM** **parameter to each link**, you can add the `zid-{{zid}}` under the UTM analytics section of your BeeHiiv dashboard.
---
And that’s it! You’re all done and ready to start sending traffic that can be properly identified with rich first-party data, allowing advertisers to target your audience more effectively!
---
## ActiveCampaign
Source: https://docs.ezoic.com/docs/identity/integrations/activecampaign/
For our ActiveCampaign integration, you will need to provide us with your API URL, API Key and Account name. You can find your account name by navigating to the settings page and you will find it listed at the top. Once you have that, from the sidebar navigate to the “Developer” section where you will find your API “URL” and “KEY” (as shown in the screenshot below).
## Adding UTM Tagging to your links
Now that your subscriber list has been synced with these new Ezoic, we will need you to start inserting your Active Campaign Contact ID into your newsletter/email campaign links using whichever UTM parameter you prefer.
For example:
If your current link goes to “https://www.content-website.com/page” , you would add the Active Campaign unique_email_id parameter into a UTM parameter, such as `utm_content` along with the text “zid-” prepended to the id. This would make the URL look like this:
`https://www.content-website.com/page?utm_content=zid-%SUBSCRIBERID%`
And that’s it! You’re all done and ready to start sending traffic that can be properly identified with rich first-party data, allowing advertisers to target your audience more effectively!
---
## MailChimp
Source: https://docs.ezoic.com/docs/identity/integrations/mailchimp/
For our MailChimp integration we will need you to provide us with an API Key, which you can find by navigating to “Account and Billing” and the under the extras dropdown in your MailChimp selecting the “API Keys”.
## Adding UTM Tagging to your links
Now that your subscriber list has been synced with Ezoic, we will need you to start inserting your MailChimp subscriber unique_email_id into your newsletter/email campaign links using whichever UTM parameter you prefer.
**For example**:
If your current link goes to “https://www.content-website.com/page” , you would add the MailChimp unique_email_id parameter into a UTM parameter, such as `utm_content` along with the text “zid-” prepended to the id. This would make your URL look like this:
`https://www.content-website.com/page?utm_content=zid-*|EMAIL_UID|*`
And that’s it! You’re all done and ready to start sending traffic that can be properly identified with rich first-party data, allowing advertisers to target your audience more effectively!
---
## Kit
Source: https://docs.ezoic.com/docs/identity/integrations/kit/
For our Kit integration, you'll need an API key. Specifically, the Kit API V4. You can find this by navigating to the Developer settings section of your Kit dashboard, as shown in the screenshot below:
If you need to create a V4 Key:
1. Click on "Add a new key"
2. Give it an internal name
3. Copy and save the API key.
## Adding UTM Tagging to your links
Now that your subscriber list has been synced with Ezoic, we will need you to start inserting your Kit subscriber ID into your newsletter/email campaign links. This can be done automatically or manually.
### Automatically (recommended)
In Kit, go to Settings>Advanced. Under Advanced Tracking, toggle on all three settings as pictured below.
### Manually
Insert your Kit subscriber ID into your newsletter/email campaign links using whichever UTM parameter you prefer.
For example: If your current link goes to “https://www.content-website.com/page” , you would add the Kit subscriber id into a UTM parameter, such as `utm_content` along with the text “SubID-” prepended to the id. This would make your URL look like this:
`https://www.content-website.com/page?utm_content=zid-{{SUBID}}`
And that’s it! You’re all done and ready to start sending traffic that can be properly identified with rich first-party data, allowing advertisers to target your audience more effectively!
---
## Mailerlite
Source: https://docs.ezoic.com/docs/identity/integrations/mailerlite/
For our Mailerlite integration, You’ll need to provide us with your Mailerlite account name, and API key. You can find your Mailerlite account name in the top right of your Mailerlite dashboard.
To retrieve your API key, navigate to the integrations page and click ‘Use’ for API:
Then, if you haven’t already, click ‘Generate New Token’.
Once you have your API key, you’ll need to go to the Identity menu in your Ezoic dashboard. Click ‘Connect or upload your list’. Select Mailerlite from the Account integration options, the enter an your Mailerlite account name (this can just be your mailerlite username), and API key
Click save, and we’ll complete the integration process.
## Adding UTM Tagging to your links
Now that your subscriber list has been synced with Ezoic, we’ll create a new custom variable called ‘zid’ for each of your contacts. You’ll need to include these in your newsletter/email campaign links using whichever UTM parameter you prefer.
For example: If your current link goes to `https://www.content-website.com/page`, you would add the Mailerlite zid parameter into a UTM parameter, such as utm_content along with the text `zid-` prepended to the id. Done correctly, the url should look like this:
`https://www.content-website.com/page?utm_content=zid-{$zid}`
And that’s it! You’re all done and ready to start sending traffic that can be properly identified with rich first-party data, allowing advertisers to target your audience more effectively!
# Mobile Apps
---
## Android
Source: https://docs.ezoic.com/docs/mobileapps/android/
The Ezoic Ads SDK for Android lets you display banner, rewarded, interstitial, native, outstream video, and instream video ads in native Android apps. The SDK initializes Ezoic configuration, Prebid, Google Ad Manager, and consent handling from one integration point.
## Requirements
- Android SDK 24 or higher
- Android Gradle Plugin 8.0 or higher
- Kotlin 1.9 or higher
- Google Mobile Ads application ID
## Installation
Add the Ezoic Ads SDK dependency to your app module:
```kotlin
dependencies {
implementation("com.ezoic.sdk:ezoic-ads-sdk:1.5.0")
}
```
The SDK depends on Google Mobile Ads and Prebid Mobile. Make sure your project resolves dependencies from Google's Maven repository and Maven Central.
## Initialize the SDK
Initialize the SDK from your `Application` class or main `Activity` before loading ads.
```kotlin
import android.app.Application
import android.util.Log
import com.ezoic.ads.sdk.core.EzoicAds
import com.ezoic.ads.sdk.core.EzoicConfiguration
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
val config = EzoicConfiguration(
domain = "example.com",
debugEnabled = false,
testMode = false
)
EzoicAds.instance.initialize(this, config) { result ->
result.onSuccess {
Log.d("Ezoic", "SDK initialized")
}.onFailure { error ->
Log.e("Ezoic", "Initialization failed: $error")
}
}
}
}
```
`domain` must match the domain configured for your site in Ezoic.
## Add a Banner Ad
Create an `EzoicBannerView`, add it to your layout, and call `loadAd()` after the SDK is initialized.
```kotlin
import android.os.Bundle
import android.util.Log
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import com.ezoic.ads.sdk.adunits.EzoicBannerView
import com.ezoic.ads.sdk.adunits.EzoicBannerViewListener
import com.ezoic.ads.sdk.core.EzoicError
class MainActivity : AppCompatActivity(), EzoicBannerViewListener {
private lateinit var bannerView: EzoicBannerView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
bannerView = EzoicBannerView(this, 12345)
bannerView.listener = this
val container = findViewById(R.id.banner_container)
container.addView(bannerView)
bannerView.loadAd("300x250")
}
override fun onBannerLoaded(bannerView: EzoicBannerView) {
Log.d("Ezoic", "Banner loaded")
}
override fun onBannerLoadFailed(bannerView: EzoicBannerView, error: EzoicError) {
Log.e("Ezoic", "Banner failed: ${error.message}")
}
override fun onDestroy() {
bannerView.destroy()
super.onDestroy()
}
}
```
Replace `12345` with your Ezoic ad unit identifier. The SDK fetches the Google Ad Manager ad unit, Prebid configuration, supported sizes, targeting values, and refresh interval from Ezoic servers.
## Banner Sizes
You can load ads with an adaptive default size, a typed size, one size string, or a list of size strings.
```kotlin
bannerView.loadAd()
bannerView.loadAd(EzoicBannerSize.MediumRectangle)
bannerView.loadAd("300x250")
bannerView.loadAd(listOf("300x250", "320x50"))
```
Common sizes include:
- `EzoicBannerSize.Banner`: 320x50
- `EzoicBannerSize.LargeBanner`: 320x100
- `EzoicBannerSize.MediumRectangle`: 300x250
- `EzoicBannerSize.FullBanner`: 468x60
- `EzoicBannerSize.Leaderboard`: 728x90
- `EzoicBannerSize.Custom(width, height)`: custom dimensions
## Add a Rewarded Ad
Rewarded ads are full-screen ads that grant an in-app reward when the user finishes watching. They use a load-then-show lifecycle: load the ad ahead of time (for example, at the start of a level), then present it at a natural break and grant the reward when it is earned.
```kotlin
import com.ezoic.ads.sdk.adunits.EzoicRewardedAd
import com.ezoic.ads.sdk.adunits.EzoicRewardedAdListenerAdapter
EzoicRewardedAd.load(this, 12345) { result ->
result.onSuccess { ad ->
// Optional: observe lifecycle events
ad.listener = object : EzoicRewardedAdListenerAdapter() {
override fun onRewardedAdDismissed(rewardedAd: EzoicRewardedAd) {
Log.d("Ezoic", "Rewarded ad closed")
}
}
// Present and grant the reward when earned
ad.show(this) { reward ->
Log.d("Ezoic", "Earned ${reward.amount} ${reward.type}")
grantReward(reward.amount)
}
}.onFailure { error ->
Log.e("Ezoic", "Rewarded load failed: ${error.message}")
}
}
```
Replace `12345` with your Ezoic ad unit identifier. Call `show(activity)` only after the ad has loaded; showing before the load completes (or after the ad was already shown) reports `EzoicError.AdNotReady`. Rewarded ads are single-use — load a new ad for each opportunity.
The reward `type` and `amount` come from the reward configured on the Google Ad Manager rewarded ad unit. The `EzoicRewardedAdListener` also reports `onRewardedAdShown`, `onRewardedAdFailedToShow`, `onRewardedAdImpression`, `onRewardedAdClicked`, and `onUserEarnedReward`. Java apps load with an `EzoicRewardedAdLoadListener` and extend `EzoicRewardedAdListenerAdapter`.
## Add an Interstitial Ad
Interstitial ads are full-screen ads shown at natural transition points (for example, between levels or screens). Unlike rewarded ads, they grant no reward. They use the same load-then-show lifecycle: load the ad ahead of time, then present it at a natural break.
```kotlin
import com.ezoic.ads.sdk.adunits.EzoicInterstitialAd
import com.ezoic.ads.sdk.adunits.EzoicInterstitialAdListenerAdapter
EzoicInterstitialAd.load(this, 12345) { result ->
result.onSuccess { ad ->
// Optional: observe lifecycle events
ad.listener = object : EzoicInterstitialAdListenerAdapter() {
override fun onInterstitialAdDismissed(interstitialAd: EzoicInterstitialAd) {
Log.d("Ezoic", "Interstitial ad closed")
}
}
// Present at a natural transition point
ad.show(this)
}.onFailure { error ->
Log.e("Ezoic", "Interstitial load failed: ${error.message}")
}
}
```
Replace `12345` with your Ezoic ad unit identifier. Call `show(activity)` only after the ad has loaded; showing before the load completes (or after the ad was already shown) reports `EzoicError.AdNotReady`. Interstitial ads are single-use — load a new ad for each opportunity.
The `EzoicInterstitialAdListener` reports `onInterstitialAdShown`, `onInterstitialAdFailedToShow`, `onInterstitialAdImpression`, `onInterstitialAdClicked`, and `onInterstitialAdDismissed`. Java apps load with an `EzoicInterstitialAdLoadListener` and extend `EzoicInterstitialAdListenerAdapter`.
## Add an Outstream Video Ad
Outstream video ads render inline in your own layout, like a banner, but with a video creative in an SDK-built player. Create an `EzoicOutstreamAdView`, add it to your layout, and call `loadAd()`.
```kotlin
import com.ezoic.ads.sdk.adunits.EzoicOutstreamAdView
import com.ezoic.ads.sdk.adunits.EzoicOutstreamAdViewListener
import com.ezoic.ads.sdk.core.EzoicError
class MainActivity : AppCompatActivity(), EzoicOutstreamAdViewListener {
private lateinit var outstreamAdView: EzoicOutstreamAdView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
outstreamAdView = EzoicOutstreamAdView(this, 12345)
outstreamAdView.listener = this
val container = findViewById(R.id.outstream_container)
container.addView(outstreamAdView)
outstreamAdView.loadAd()
}
override fun onOutstreamLoaded(adView: EzoicOutstreamAdView) {
Log.d("Ezoic", "Outstream ad loaded")
}
override fun onOutstreamLoadFailed(adView: EzoicOutstreamAdView, error: EzoicError) {
Log.e("Ezoic", "Outstream failed: ${error.message}")
}
override fun onDestroy() {
outstreamAdView.destroy()
super.onDestroy()
}
}
```
Replace `12345` with your Ezoic ad unit identifier. `EzoicOutstreamAdView` is also XML-inflatable. Unlike `EzoicBannerView`, `loadAd()` takes no size — the SDK renders at the size configured on the server (640x360 by default). Call `destroy()` when the hosting `Activity` or `Fragment` is destroyed.
The `EzoicOutstreamAdViewListener` also reports `onOutstreamImpression`, `onOutstreamClicked`, `onOutstreamOpened`, and `onOutstreamClosed` — all optional.
## Add an Instream Video Ad
Instream video ads play inside your own video content. Your app owns the video player and the [Google IMA SDK](https://developers.google.com/interactive-media-ads/docs/sdks/android/client-side); the Ezoic SDK's job is to build the Google Ad Manager VAST ad-tag URL you hand to IMA. There is no Ezoic view — the deliverable is a URL string. `EzoicInstreamAd` is a view-less, multi-use controller: unlike rewarded and interstitial ads it is not single-use, so keep the instance and call `load()` again for the next video.
```kotlin
import com.ezoic.ads.sdk.adunits.EzoicInstreamAd
import com.ezoic.ads.sdk.adunits.EzoicInstreamAdListener
import com.ezoic.ads.sdk.core.EzoicError
import com.google.ads.interactivemedia.v3.api.ImaSdkFactory
private val instreamAd = EzoicInstreamAd(12345)
instreamAd.load(this, contentUrl = playingVideoUrl, listener = object : EzoicInstreamAdListener {
override fun onAdTagReady(adTagUrl: String) {
// Feed the tag to your IMA ads request.
val request = ImaSdkFactory.getInstance().createAdsRequest()
request.adTagUrl = adTagUrl
adsLoader.requestAds(request)
}
override fun onAdFailedToLoad(error: EzoicError) {
Log.e("Ezoic", "Instream load failed: ${error.message}")
// Play content without a preroll.
}
})
// In your AdErrorEvent.AdErrorListener, walk down the floor waterfall:
instreamAd.getNextAdTagUrl()?.let { nextTag ->
val request = ImaSdkFactory.getInstance().createAdsRequest()
request.adTagUrl = nextTag
adsLoader.requestAds(request)
}
// In your AdEvent.AdEventListener, on AdEventType.STARTED:
instreamAd.reportImpression()
```
Replace `12345` with your Ezoic ad unit identifier. `contentUrl` (the URL of the video being played) is optional and, when supplied, is added to the ad tag for contextual targeting. The `EzoicInstreamAdListener` reports `onAdTagReady(adTagUrl)` when the tag is ready and `onAdFailedToLoad(error)` on no fill. On an IMA ad error, call `getNextAdTagUrl()` to retry the next floor in the waterfall — it returns `null` once the waterfall is exhausted. On the IMA `STARTED` event, call `reportImpression()` (optionally passing a `revenueUsd` value) to record the Ezoic impression. Unlike rewarded and interstitial ads, `EzoicInstreamAd` is multi-use — call `destroy()` only when the ad unit is no longer needed, for example when the player is torn down.
## Add a Native Ad
Native ads deliver ad assets (headline, image, body text, call to action) that you render inside your own layout instead of a fixed banner or full-screen format, so the ad matches the look and feel of your content.
```kotlin
import com.ezoic.ads.sdk.adunits.EzoicNativeAd
import com.google.android.gms.ads.nativead.NativeAdView
private var ezoicNativeAd: EzoicNativeAd? = null
EzoicNativeAd.load(this, 12345) { result ->
result.onSuccess { ad ->
ezoicNativeAd = ad
val gmaAd = ad.nativeAd ?: return@onSuccess
// Inflate your own NativeAdView layout and populate its asset views
val adView = layoutInflater.inflate(R.layout.native_ad_view, null) as NativeAdView
val headline = adView.findViewById(R.id.ad_headline)
headline.text = gmaAd.headline
adView.headlineView = headline
adView.setNativeAd(gmaAd)
adContainer.removeAllViews()
adContainer.addView(adView)
}.onFailure { error ->
Log.e("Ezoic", "Native load failed: ${error.message}")
}
}
```
Replace `12345` with your Ezoic ad unit identifier. The SDK returns the underlying Google Mobile Ads `NativeAd` on `ad.nativeAd`; populate a `NativeAdView` with your own styling and assign it with `setNativeAd()`.
Release the ad when the hosting `Activity` or `Fragment` is destroyed:
```kotlin
override fun onDestroy() {
ezoicNativeAd?.destroy()
ezoicNativeAd = null
super.onDestroy()
}
```
The `EzoicNativeAdListener` reports `onNativeAdImpression`, `onNativeAdClicked`, `onNativeAdOpened`, and `onNativeAdClosed`. Java apps load with an `EzoicNativeAdLoadListener` and can extend `EzoicNativeAdListenerAdapter`.
## AndroidManifest.xml
Add your Google Mobile Ads application ID to `AndroidManifest.xml`:
In most Ezoic mobile app integrations, this app ID is provided by Ezoic and can be found in your Ezoic dashboard. Use the value assigned to your app unless your Ezoic representative gives you a different Google Mobile Ads application ID.
```xml
```
If your app targets Android 12 or higher and uses the advertising ID, add the advertising ID permission:
```xml
```
## app-ads.txt
Host an `app-ads.txt` file at the root of the developer website listed on your app's Play Store page (for example, `https://example.com/app-ads.txt`). It authorizes the buyers and exchanges that may sell your inventory; a missing or incomplete file causes most programmatic demand to be filtered out. Ezoic provides the required entries — confirm the file is published and current.
Unlike iOS, Android has no App Tracking Transparency or SKAdNetwork requirements. The advertising ID permission above is the main identifier-related step, and Google Mobile Ads merges its own required entries through the manifest automatically.
## Privacy and Consent
The SDK can automatically read consent signals from `SharedPreferences` when they are set by a consent management platform.
- TCF v2 consent is read from `IABTCF_*` keys.
- GPP consent is read from `IABGPP_*` keys.
- US Privacy consent is read from the standard IAB privacy key.
You can also set consent manually:
```kotlin
EzoicAds.instance.setGDPRConsent(
applies = true,
consentString = "TCF_CONSENT_STRING"
)
EzoicAds.instance.setGPPConsent(
gppString = "GPP_STRING",
sectionIds = "7"
)
EzoicAds.instance.setSubjectToCOPPA(true)
```
## Pageview Tracking
The SDK automatically tracks pageviews for standard `Activity`, `Fragment`, and view-hosted Navigation components after initialization.
If your app uses Compose Navigation, opt in with your `NavController`:
```kotlin
val navController = rememberNavController()
EzoicAds.instance.TrackNavigation(navController)
```
For WebView-heavy apps, use `EzoicWebViewClient` so page loads are tracked as pageviews:
```kotlin
webView.webViewClient = EzoicWebViewClient()
```
You can also track a pageview manually:
```kotlin
EzoicAds.instance.trackPageview()
```
## Java Compatibility
The Android SDK is written in Kotlin, but the public API is designed for Java interoperability. Java apps can use the `EzoicConfiguration.Builder`, callback interfaces, Java-friendly banner size constants, and `EzoicBannerViewAdapter`.
## Troubleshooting
### SDK Not Initializing
1. Confirm the configured domain matches your Ezoic dashboard.
2. Confirm the app has network access.
3. Enable `debugEnabled = true` and check Logcat for `EzoicAds` logs.
### Ads Not Loading
1. Initialize the SDK before calling `loadAd()`.
2. Confirm the Ezoic ad unit identifier is configured in Ezoic.
3. Confirm the Google Mobile Ads application ID is present in `AndroidManifest.xml`.
4. Check consent configuration if traffic is subject to privacy regulations.
---
## iOS
Source: https://docs.ezoic.com/docs/mobileapps/ios/
The Ezoic Ads SDK for iOS lets you display banner, rewarded, interstitial, native, outstream video, and instream video ads in native iOS apps. The SDK initializes Ezoic configuration, Prebid header bidding, Google Ad Manager, and consent handling from one integration point.
## Requirements
- iOS 14.0 or higher
- Xcode 15.0 or higher
- Swift 5.9 or higher
- Google Mobile Ads application ID
## Installation
Add the SDK with Swift Package Manager.
In Xcode, go to **File > Add Package Dependencies** and enter:
```
https://github.com/ezoic/ezoic-swift-sdk-dist.git
```
Select **Exact Version** and choose `1.5.0` (or the release version provided by Ezoic). Pinning an exact version is recommended so SDK updates are always intentional.
Or add the package to `Package.swift` using the release version provided for your app:
```swift
dependencies: [
.package(url: "https://github.com/ezoic/ezoic-swift-sdk-dist.git", exact: "1.5.0")
]
```
The SDK is distributed as a pre-built `EzoicAdsSDK.xcframework` attached to each GitHub release. When you add the package, Swift Package Manager also resolves the Prebid Mobile and Google Mobile Ads SDKs as transitive dependencies — you do not need to add them yourself.
## Initialize the SDK
Initialize the SDK in your `AppDelegate` or app startup flow before loading ads.
```swift
import EzoicAdsSDK
let config = EzoicConfiguration(
domain: "example.com",
debugEnabled: false,
testMode: false
)
EzoicAds.shared.initialize(with: config) { result in
switch result {
case .success:
print("Ezoic SDK initialized")
case .failure(let error):
print("Ezoic initialization failed: \(error)")
}
}
```
`domain` must match the domain configured for your site in Ezoic. Authentication is handled by the app's bundle identifier and the configured domain — there is no client-side API key.
For apps using async/await on iOS 15 or higher:
```swift
try await EzoicAds.shared.initialize(with: config)
```
## Add a Banner Ad
Create an `EzoicBannerView`, add it to your view hierarchy, and call `loadAd()` after the SDK is initialized.
```swift
import EzoicAdsSDK
import UIKit
class ViewController: UIViewController {
private var bannerView: EzoicBannerView?
override func viewDidLoad() {
super.viewDidLoad()
let adView = EzoicBannerView(adUnitIdentifier: 12345)
adView.delegate = self
adView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(adView)
NSLayoutConstraint.activate([
adView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
adView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
adView.widthAnchor.constraint(equalToConstant: EzoicBannerSize.banner.width),
adView.heightAnchor.constraint(equalToConstant: EzoicBannerSize.banner.height)
])
bannerView = adView
adView.loadAd()
}
}
extension ViewController: EzoicBannerViewDelegate {
func bannerViewDidLoad(_ bannerView: EzoicBannerView) {
print("Banner loaded")
}
func bannerView(_ bannerView: EzoicBannerView, didFailToLoadWithError error: EzoicError) {
print("Banner failed: \(error)")
}
}
```
Replace `12345` with your numeric Ezoic ad unit identifier. The SDK fetches the Google Ad Manager ad unit, Prebid configuration, supported sizes, targeting values, and refresh interval from Ezoic servers.
## Banner Sizes
You can load ads with an adaptive default size, a typed size, one size string, or a list of size strings.
```swift
adView.loadAd()
adView.loadAd(size: EzoicBannerSize.mediumRectangle)
adView.loadAd(size: "300x250")
adView.loadAd(sizes: ["300x250", "320x50"])
```
Common sizes include:
- `EzoicBannerSize.banner`: 320x50
- `EzoicBannerSize.largeBanner`: 320x100
- `EzoicBannerSize.mediumRectangle`: 300x250
- `EzoicBannerSize.fullBanner`: 468x60
- `EzoicBannerSize.leaderboard`: 728x90
- `EzoicBannerSize.custom(width:height:)`: custom dimensions
You can also use the adaptive size for the current screen width:
```swift
let adaptiveSize = EzoicBannerSize.adaptiveSize
```
## Add a Rewarded Ad
Rewarded ads are full-screen ads that grant an in-app reward when the user finishes watching. They use a load-then-show lifecycle: load the ad ahead of time (for example, at the start of a level), then present it at a natural break and grant the reward when it is earned.
```swift
import EzoicAdsSDK
var rewardedAd: EzoicRewardedAd?
func loadRewardedAd() {
EzoicRewardedAd.load(adUnitIdentifier: 12345) { [weak self] result in
switch result {
case .success(let ad):
self?.rewardedAd = ad
ad.delegate = self
case .failure(let error):
print("Rewarded load failed: \(error.localizedDescription)")
}
}
}
func showRewardedAd() {
rewardedAd?.show(from: self) { reward in
print("Earned \(reward.amount) \(reward.type)")
grantReward(reward.amount)
}
}
```
`async`/`await` is also supported on iOS 15 or higher:
```swift
rewardedAd = try await EzoicRewardedAd.load(adUnitIdentifier: 12345)
```
Replace `12345` with your numeric Ezoic ad unit identifier. Call `show(from:)` only after the ad has loaded; showing before the load completes (or after the ad was already shown) reports `EzoicError.adNotReady`. If you pass `nil` (or omit `from:`), the SDK presents from the app's top view controller. Rewarded ads are single-use — load a new ad for each opportunity.
Lifecycle events are delivered through `EzoicRewardedAdDelegate`. The reward `type` and `amount` come from the reward configured on the Google Ad Manager rewarded ad unit.
```swift
extension ViewController: EzoicRewardedAdDelegate {
func rewardedAd(_ rewardedAd: EzoicRewardedAd, userDidEarn reward: EzoicReward) {
grantReward(reward.amount)
}
func rewardedAdDidDismiss(_ rewardedAd: EzoicRewardedAd) {
print("Rewarded ad closed")
}
}
```
The delegate also reports `rewardedAdDidPresent`, `rewardedAd(_:didFailToPresentWithError:)`, `rewardedAdDidRecordImpression`, and `rewardedAdDidRecordClick`. All delegate methods are optional.
## Add an Interstitial Ad
Interstitial ads are full-screen ads shown at natural transition points (for example, between levels or screens). Unlike rewarded ads, they grant no reward. They use the same load-then-show lifecycle: load the ad ahead of time, then present it at a natural break.
```swift
import EzoicAdsSDK
var interstitialAd: EzoicInterstitialAd?
func loadInterstitialAd() {
EzoicInterstitialAd.load(adUnitIdentifier: 12345) { [weak self] result in
switch result {
case .success(let ad):
self?.interstitialAd = ad
ad.delegate = self
case .failure(let error):
print("Interstitial load failed: \(error.localizedDescription)")
}
}
}
func showInterstitialAd() {
interstitialAd?.show(from: self)
}
```
`async`/`await` is also supported on iOS 15 or higher:
```swift
interstitialAd = try await EzoicInterstitialAd.load(adUnitIdentifier: 12345)
```
Replace `12345` with your numeric Ezoic ad unit identifier. Call `show(from:)` only after the ad has loaded; showing before the load completes (or after the ad was already shown) reports `EzoicError.adNotReady`. If you pass `nil` (or omit `from:`), the SDK presents from the app's top view controller. Interstitial ads are single-use — load a new ad for each opportunity.
Lifecycle events are delivered through `EzoicInterstitialAdDelegate`.
```swift
extension ViewController: EzoicInterstitialAdDelegate {
func interstitialAdDidDismiss(_ interstitialAd: EzoicInterstitialAd) {
print("Interstitial ad closed")
}
}
```
The delegate also reports `interstitialAdDidPresent`, `interstitialAd(_:didFailToPresentWithError:)`, and `interstitialAdDidRecordImpression`/`interstitialAdDidRecordClick`. All delegate methods are optional.
## Add an Outstream Video Ad
Outstream video ads render inline in your own view hierarchy, like a banner, but with a video creative in an SDK-built player. Create an `EzoicOutstreamAdView`, set its delegate, add it to your view hierarchy, and call `loadAd()`.
```swift
import EzoicAdsSDK
var outstreamAd: EzoicOutstreamAdView?
func loadOutstreamAd() {
let adView = EzoicOutstreamAdView(adUnitIdentifier: 12345)
adView.delegate = self
adContainer.addSubview(adView)
outstreamAd = adView
adView.loadAd()
}
```
Replace `12345` with your numeric Ezoic ad unit identifier. Set `delegate` before calling `loadAd()` so the load callback isn't missed. Unlike `EzoicBannerView`, `loadAd()` takes no size — the SDK renders at the size configured on the server (640×360 by default). Call `destroy()` when the hosting view is torn down (e.g. in `deinit`, or when a cell is recycled) so the underlying ad is released.
Lifecycle events are delivered through `EzoicOutstreamAdViewDelegate`.
```swift
extension ViewController: EzoicOutstreamAdViewDelegate {
func outstreamViewDidLoad(_ outstreamView: EzoicOutstreamAdView) {
print("Outstream ad loaded")
}
func outstreamView(_ outstreamView: EzoicOutstreamAdView, didFailToLoadWithError error: EzoicError) {
print("Outstream failed: \(error.localizedDescription)")
}
}
```
The delegate also reports `outstreamViewDidRecordImpression`, `outstreamViewDidRecordClick`, `outstreamViewWillPresentScreen`, and `outstreamViewDidDismissScreen` — all optional.
## Add an Instream Video Ad
Instream video ads play inside your own video content. Your app owns the video player and the [Google IMA SDK](https://developers.google.com/interactive-media-ads/docs/sdks/ios); the Ezoic SDK's job is to build the Google Ad Manager VAST ad-tag URL you hand to IMA. There is no Ezoic view — the deliverable is a URL string, so you retain the `EzoicInstreamAd` instance yourself. `EzoicInstreamAd` is a view-less, multi-use controller: unlike rewarded and interstitial ads it is not single-use, so keep the instance and call `load(contentUrl:delegate:)` again for the next video.
```swift
import EzoicAdsSDK
import GoogleInteractiveMediaAds
let instreamAd = EzoicInstreamAd(adUnitId: 12345)
func loadInstreamAd() {
instreamAd.load(contentUrl: playingVideoUrl, delegate: self)
}
```
Tag delivery and failures arrive through `EzoicInstreamAdDelegate`:
```swift
extension ViewController: EzoicInstreamAdDelegate {
func instreamAd(_ instreamAd: EzoicInstreamAd, didReceiveAdTag adTagUrl: String) {
// Feed the tag to your own IMA ads request.
let request = IMAAdsRequest(
adTagUrl: adTagUrl,
adDisplayContainer: adDisplayContainer,
contentPlayhead: contentPlayhead,
userContext: nil
)
adsLoader.requestAds(with: request)
}
func instreamAd(_ instreamAd: EzoicInstreamAd, didFailToLoadWithError error: EzoicError) {
print("Instream load failed: \(error.localizedDescription)")
// Play content without a preroll.
}
}
```
Replace `12345` with your numeric Ezoic ad unit identifier. `contentUrl` (the URL of the video being played) is optional and, when supplied, is added to the ad tag for contextual targeting. On an IMA ad error, call `getNextAdTagUrl()` to retry the next floor in the waterfall — it returns `nil` once exhausted. On the IMA `STARTED` event, call `reportImpression()` (optionally passing a `revenueUsd` value) to record the Ezoic impression. Call `destroy()` when the player is torn down (e.g. in `deinit`) to cancel any in-flight request; the controller is otherwise reusable across loads.
## Add a Native Ad
Native ads deliver ad assets (headline, image, body text, call to action) that you render inside your own view hierarchy instead of a fixed banner or full-screen format, so the ad matches the look and feel of your content.
```swift
import EzoicAdsSDK
import GoogleMobileAds
var nativeAd: EzoicNativeAd?
func loadNativeAd() {
EzoicNativeAd.load(adUnitIdentifier: 12345) { [weak self] result in
switch result {
case .success(let ad):
self?.nativeAd = ad
ad.delegate = self
guard let gmaAd = ad.nativeAd else { return }
// Build a NativeAdView, populate its asset views, then attach the ad.
let adView = NativeAdView()
let headline = UILabel()
headline.text = gmaAd.headline
adView.headlineView = headline
adView.addSubview(headline)
adView.nativeAd = gmaAd
self?.adContainer.addSubview(adView)
case .failure(let error):
print("Native load failed: \(error.localizedDescription)")
}
}
}
```
`async`/`await` is also supported on iOS 15 or higher:
```swift
nativeAd = try await EzoicNativeAd.load(adUnitIdentifier: 12345)
```
Replace `12345` with your numeric Ezoic ad unit identifier. The SDK returns the underlying Google Mobile Ads `NativeAd` on `ad.nativeAd`; populate a `NativeAdView` with your own styling and assign the ad to its `nativeAd` property.
Call `destroy()` when the hosting view is torn down (for example in `deinit`, or when a table or collection view cell is recycled) so the underlying ad is released.
Lifecycle events are delivered through `EzoicNativeAdDelegate`. All delegate methods are optional.
```swift
extension ViewController: EzoicNativeAdDelegate {
func nativeAdDidRecordImpression(_ nativeAd: EzoicNativeAd) {
print("Native ad impression")
}
func nativeAdDidRecordClick(_ nativeAd: EzoicNativeAd) {
print("Native ad clicked")
}
}
```
The delegate also reports `nativeAdWillPresentScreen` and `nativeAdDidDismissScreen`, which fire when a click opens and closes an overlay such as the ad's destination.
## Required App Setup for Ads to Serve
A few configuration steps live in your app project and on your website, not in the SDK. Apple and Google require them, and they directly affect whether — and how well — ads fill. The SDK cannot add them for you (see [What the SDK does vs. what you must add](#what-the-sdk-does-vs-what-you-must-add)), so complete all of the following before testing fill.
### 1. Google Mobile Ads application ID
Add your Google Mobile Ads application ID to `Info.plist`. This ID is provided by Ezoic and can be found in your Ezoic dashboard. Use the value assigned to your app unless your Ezoic representative gives you a different one. Also set `GADIsAdManagerApp` so Google Mobile Ads runs in Ad Manager mode.
```xml
GADApplicationIdentifierca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXXGADIsAdManagerApp
```
Without a valid `GADApplicationIdentifier`, the Google Mobile Ads SDK will not initialize and no ads will serve.
### 2. App Tracking Transparency (ATT)
ATT has the single biggest impact on fill and CPM. Without it, the IDFA is unavailable (all zeros) and a large share of programmatic demand will not bid or will bid much lower.
Add the tracking usage description to `Info.plist`. Apple requires this string to live in the app's `Info.plist` — it cannot be supplied by the SDK because it is shown in the App Store privacy label and the system prompt.
```xml
NSUserTrackingUsageDescriptionThis identifier will be used to deliver personalized ads to you.
```
**The SDK presents the ATT prompt for you.** During `initialize`, when the status is still undetermined, the SDK shows the prompt and waits for the user's decision before starting the ad stack, so the IDFA (if granted) is attached to the first ad request. You only need to supply the `NSUserTrackingUsageDescription` string above — no extra code.
```swift
// The SDK requests ATT (if undetermined) before loading ads.
EzoicAds.shared.initialize(with: config) { _ in
// SDK ready — banners can load with the IDFA available.
}
```
If you prefer to drive the ATT flow yourself (for example, to show a pre-prompt or to control its timing), set `requestATTBeforeAds: false` on `EzoicConfiguration` and request authorization **before** initializing the SDK so the IDFA is available on the first ad request:
```swift
import AppTrackingTransparency
let config = EzoicConfiguration(domain: "example.com", requestATTBeforeAds: false)
ATTrackingManager.requestTrackingAuthorization { _ in
EzoicAds.shared.initialize(with: config) { _ in }
}
```
If you disable the SDK's ATT handling, resolve the prompt before initializing the ad SDK. Initializing first means the first ad requests go out without the IDFA even when the user later grants permission, which lowers fill on the initial screen.
### 3. SKAdNetwork identifiers
`SKAdNetworkItems` lets buyers attribute installs when the IDFA is unavailable; missing identifiers suppress demand from buyers that require SKAdNetwork. Apple reads this key only from the app's main `Info.plist` — it is **not** aggregated from frameworks or SDKs, so it must be added to your app even though the SDK depends on Google Mobile Ads.
Add the `SKAdNetworkItems` array with Google's published identifiers (Google's own `cstr6suwn9.skadnetwork` plus the participating third-party buyers). Copy the current, complete list from Google's [Prepare privacy strategies](https://developers.google.com/ad-manager/mobile-ads-sdk/ios/privacy/strategies#enable-skadnetwork-to-track-conversions) page — Google updates it as buyers are added.
```xml
SKAdNetworkItemsSKAdNetworkIdentifiercstr6suwn9.skadnetwork
```
Identifiers must be lowercase. If you add mediation partners, also include any identifiers those partners require.
### 4. app-ads.txt on your website
Host an `app-ads.txt` file at the root of the developer/website domain listed on your app's App Store page (for example, `https://example.com/app-ads.txt`). It authorizes the buyers and exchanges that may sell your inventory; a missing or incomplete file causes most programmatic demand to be filtered out. Ezoic provides the required entries — confirm the file is published and current.
### What the SDK does vs. what you must add
| Item | Provided automatically by the SDK | You must add |
| --- | --- | --- |
| Google Mobile Ads + Prebid SDKs | Yes (transitive dependencies) | — |
| Ad unit, sizes, targeting, Prebid config | Yes (fetched from Ezoic) | — |
| `GADApplicationIdentifier` / `GADIsAdManagerApp` | No (per-app value) | Yes, in `Info.plist` |
| ATT prompt (request + wait before ads) | Yes (SDK presents it during `initialize`) | — |
| `NSUserTrackingUsageDescription` string | No (App Store privacy requirement) | Yes, in `Info.plist` |
| `SKAdNetworkItems` | No (Apple reads app `Info.plist` only) | Yes, in `Info.plist` |
| `app-ads.txt` | No (hosted on your domain) | Yes, on your website |
Apple and Google deliberately require these to live in the app bundle or on the publisher domain, so they cannot be bundled inside the SDK. Keep them in mind whenever you ship a new app.
## Privacy and Consent
The SDK can automatically read consent signals from `UserDefaults` when they are set by a consent management platform.
- TCF v2 consent is read from `IABTCF_*` keys.
- GPP consent is read from `IABGPP_*` keys.
- US Privacy consent is read from the standard IAB privacy key.
You can also set consent manually:
```swift
EzoicAds.shared.setGDPRConsent(
applies: true,
consentString: "TCF_CONSENT_STRING"
)
EzoicAds.shared.setGPPConsent(
gppString: "GPP_STRING",
sectionIds: "7"
)
EzoicAds.shared.setSubjectToCOPPA(true)
```
## Pageview Tracking
The SDK automatically tracks view controller navigation after initialization.
You can also track a pageview manually when a user navigates to a new screen or content view:
```swift
EzoicAds.shared.trackPageview { success in
print("Pageview tracked: \(success)")
}
```
For apps using async/await on iOS 15 or higher:
```swift
let success = await EzoicAds.shared.trackPageview()
```
## Troubleshooting
### SDK Not Initializing
1. Confirm the configured `domain` matches your Ezoic dashboard.
2. Confirm the app has network access.
3. Enable `debugEnabled: true` and review SDK logs in the Xcode console.
### Ads Not Loading
1. Initialize the SDK before calling `loadAd()`.
2. Confirm the Ezoic ad unit identifier is configured in Ezoic.
3. Confirm the Google Mobile Ads application ID is present in `Info.plist`.
4. Check consent configuration if traffic is subject to privacy regulations.
---
## Flutter
Source: https://docs.ezoic.com/docs/mobileapps/flutter/
The Ezoic Flutter SDK lets you use Ezoic mobile app ads (banner, native, outstream video, instream video, rewarded, and interstitial) from Flutter while relying on the native Android and iOS SDKs for ad loading, Prebid, Google Ad Manager, consent handling, pageview tracking, and remote configuration.
The Flutter package is a plugin bridge over the native SDKs. It does not reimplement the ad stack in Dart.
## Requirements
- Flutter 3.19 or higher
- Dart 3.3 or higher
- Android SDK 24 or higher for Android apps
- iOS 14.0 or higher for iOS apps
- Google Mobile Ads application ID provided by Ezoic
- Native Android and iOS Ezoic SDK access for the platforms you support
## Installation
Add the Flutter package version provided by Ezoic:
```yaml
dependencies:
ezoic_flutter_sdk:
git:
url: https://github.com/ezoic/flutter-sdk.git
```
Then install iOS pods if your app supports iOS:
```sh
cd ios
pod install
```
The Flutter plugin expects the native iOS SDK to be available to CocoaPods as `EzoicAdsSDK`. If Ezoic provides the iOS SDK through a private spec repo or direct pod declaration, add that source to your app's `Podfile` before running `pod install`.
## Platform Setup
Flutter apps still need the native platform setup required by Android and iOS.
### Android
Follow the Android setup requirements in the [Android guide](/docs/mobileapps/android/), including:
- Making sure Gradle can resolve the Ezoic Android SDK dependency
- Adding the Google Mobile Ads application ID to `AndroidManifest.xml`
- Adding the advertising ID permission if your app targets Android 12 or higher and uses the advertising ID
The Google Mobile Ads application ID is usually provided by Ezoic and can be found in your Ezoic dashboard.
### iOS
Follow the [Required App Setup for Ads to Serve](/docs/mobileapps/ios/#required-app-setup-for-ads-to-serve) in the iOS guide. These steps live in your app's `Info.plist` (and on your website) and directly affect fill, so complete all of them:
- Installing the Ezoic iOS SDK version provided for your app
- Adding the Google Mobile Ads application ID (`GADApplicationIdentifier`) and `GADIsAdManagerApp` to `Info.plist`
- Adding `NSUserTrackingUsageDescription` to `Info.plist` (the SDK presents the App Tracking Transparency prompt for you before loading ads — biggest impact on fill)
- Adding the `SKAdNetworkItems` identifiers to the app's `Info.plist` (Apple does not read these from frameworks/SDKs)
The Google Mobile Ads application ID is usually provided by Ezoic and can be found in your Ezoic dashboard.
## app-ads.txt
For both platforms, host an `app-ads.txt` file at the root of the developer website listed on your app's store page (for example, `https://example.com/app-ads.txt`). It authorizes the buyers that may sell your inventory; a missing or incomplete file causes most programmatic demand to be filtered out. Ezoic provides the required entries — confirm the file is published and current.
## Initialize the SDK
Initialize Ezoic before rendering ads.
```dart
import 'package:ezoic_flutter_sdk/ezoic_flutter_sdk.dart';
await EzoicAds.initialize(
const EzoicConfiguration(
domain: 'example.com',
debugEnabled: false,
testMode: false
),
);
```
`domain` must match the domain configured for your site in Ezoic. The native Android and iOS SDKs both authenticate using the configured domain plus the app's bundle/package identifier — there is no client-side API key.
## Add a Banner Ad
Render `EzoicBannerView` where the banner should appear.
```dart
EzoicBannerView(
adUnitIdentifier: '12345',
size: EzoicBannerSize.mediumRectangle,
onLoad: () => print('Ezoic banner loaded'),
onError: (error) => print('Ezoic banner failed: ${error.message}'),
)
```
Replace `12345` with your Ezoic ad unit identifier. The native SDKs fetch the Google Ad Manager ad unit, Prebid configuration, targeting values, supported sizes, and refresh interval from Ezoic servers.
On Android, the current native SDK expects a numeric Ezoic ad unit identifier. Pass it as a string in Flutter, for example `'12345'`.
## Banner Sizes
Common banner sizes include:
- `EzoicBannerSize.banner`: 320x50
- `EzoicBannerSize.largeBanner`: 320x100
- `EzoicBannerSize.mediumRectangle`: 300x250
- `EzoicBannerSize.fullBanner`: 468x60
- `EzoicBannerSize.leaderboard`: 728x90
On Android, the selected size is passed into the native SDK's ad request. On iOS, the native SDK selects the configured ad size from Ezoic remote configuration, and the Flutter size controls the platform view dimensions.
## Banner Events
`EzoicBannerView` supports these event callbacks:
- `onLoad`
- `onError`
- `onImpression`
- `onClick`
- `onOpen`
- `onClose`
## Add a Rewarded Ad
Rewarded ads are full-screen ads that grant an in-app reward when the user finishes watching. Unlike banners, they are not widgets: load one imperatively ahead of time (for example, at the start of a level), then present it at a natural break. `show()` resolves with the earned reward, or `null` if the user dismissed the ad early.
```dart
import 'package:ezoic_flutter_sdk/ezoic_flutter_sdk.dart';
Future runRewardedAd() async {
try {
final ad = await EzoicRewardedAd.load('12345');
// Optional: observe lifecycle events
ad.onDismissed = () => print('Rewarded ad closed');
ad.onFailedToShow = (error) => print('Show failed: ${error.message}');
final reward = await ad.show();
if (reward != null) {
print('Earned ${reward.amount} ${reward.type}');
grantReward(reward.amount);
}
} catch (error) {
print('Rewarded ad failed: $error');
}
}
```
Replace `12345` with your Ezoic ad unit identifier (passed as a string). Load and `show` are separate steps; calling `show()` before the ad has loaded throws. Rewarded ads are single-use — load a new `EzoicRewardedAd` for each reward opportunity.
The reward `type` and `amount` come from the reward configured on the Google Ad Manager rewarded ad unit. The other lifecycle callbacks are `onShown`, `onImpression`, `onClicked`, and `onUserEarnedReward` — all optional.
## Add an Interstitial Ad
Interstitial ads are full-screen ads shown at natural transition points (for example, between levels or screens). Unlike rewarded ads, they grant no reward. Like rewarded ads, they are not widgets: load one imperatively ahead of time, then present it at a natural break. `show()` resolves when the ad is dismissed, or throws an `EzoicInterstitialAdError` if it fails to present.
```dart
import 'package:ezoic_flutter_sdk/ezoic_flutter_sdk.dart';
Future runInterstitialAd() async {
try {
final ad = await EzoicInterstitialAd.load('12345');
// Optional: observe lifecycle events
ad.onShown = () => print('Interstitial ad shown');
ad.onFailedToShow = (error) => print('Show failed: ${error.message}');
await ad.show();
print('Interstitial ad closed');
} catch (error) {
print('Interstitial ad failed: $error');
}
}
```
Replace `12345` with your Ezoic ad unit identifier (passed as a string). Load and `show` are separate steps; calling `show()` before the ad has loaded throws. Interstitial ads are single-use and auto-destroy once dismissed — load a new `EzoicInterstitialAd` for each opportunity.
The other lifecycle callbacks are `onImpression`, `onClicked`, and `onDismissed` — all optional.
## Add an Outstream Video Ad
Render `EzoicOutstreamAdView` where the video should appear. Like `EzoicNativeAdView`, it is a platform view that fills its parent's constraints, so wrap it in a `SizedBox` (or another constrained parent) to size it.
```dart
SizedBox(
height: 200,
child: EzoicOutstreamAdView(
adUnitIdentifier: '12345',
onLoad: () => print('Ezoic outstream ad loaded'),
onError: (error) => print('Ezoic outstream ad failed: ${error.message}'),
onImpression: () => print('Ezoic outstream ad impression'),
onClick: () => print('Ezoic outstream ad clicked'),
onOpen: () => print('Ezoic outstream ad opened an overlay'),
onClose: () => print('Ezoic outstream ad overlay closed'),
),
)
```
Replace `12345` with your Ezoic ad unit identifier (passed as a string). The native SDKs load the ad and render it inline through Google Ad Manager at the size configured on the server.
`EzoicOutstreamAdView` fills its parent's constraints and destroys its underlying ad with the platform view's lifecycle — no manual `destroy()` call is required.
`EzoicOutstreamAdView` supports these event callbacks:
- `onLoad`
- `onError` — receives an `EzoicOutstreamAdError` with `message` and `code`
- `onImpression`
- `onClick`
- `onOpen`
- `onClose`
On Android, the current native SDK expects a numeric Ezoic ad unit identifier. Pass it as a string in Flutter, for example `'12345'`.
## Add an Instream Video Ad
`EzoicInstreamAd` is a view-less controller for instream video. Instream video runs inside your app's OWN video content: your app owns the video player and the Google IMA SDK. The controller renders nothing — its deliverable is a Google Ad Manager VAST ad-tag URL you feed to your IMA `AdsRequest`. Unlike `EzoicInterstitialAd`, it is multi-use — it is not auto-destroyed, so keep the instance and call `load` again for the next video.
```dart
import 'package:ezoic_flutter_sdk/ezoic_flutter_sdk.dart';
final instream = EzoicInstreamAd('12345');
Future runInstreamAd() async {
try {
final tagUrl = await instream.load(contentUrl: playingVideoUrl);
// Feed tagUrl to your IMA AdsRequest.adTagUrl and request the preroll.
} catch (error) {
print('Instream ad failed: $error');
}
}
// When IMA reports an ad error, walk the floor waterfall to the next tag.
// A null result means the waterfall is exhausted — give up on the preroll.
final next = await instream.getNextAdTagUrl();
// When IMA reports the ad STARTED, fire the Ezoic impression pixel.
await instream.reportImpression();
// Release the controller when the ad unit is no longer needed.
await instream.destroy();
```
Replace `12345` with your Ezoic ad unit identifier (passed as a string). `load({String? contentUrl})` resolves with the tag URL or throws an `EzoicInstreamAdError`; pass the URL of the video you're playing so it can be folded into the tag for contextual targeting. `getNextAdTagUrl()` resolves to `null` once the waterfall is exhausted. `reportImpression({double? revenueUsd})` records the impression on the IMA `STARTED` event; `revenueUsd` is optional. Call `destroy()` only when you're done with the ad unit — the controller keeps its state across loads so you can reuse it for the next video.
## Add a Native Ad
Native ads deliver ad assets (headline, icon, advertiser, media, body text, and call to action) rendered in a template designed to match the look and feel of your app content, rather than in a fixed banner or full-screen format. Unlike interstitial and rewarded ads, native ads are widgets: render `EzoicNativeAdView` where the ad should appear in your layout.
```dart
SizedBox(
height: 320,
child: EzoicNativeAdView(
adUnitIdentifier: '12345',
onLoad: () => print('Ezoic native ad loaded'),
onError: (error) => print('Ezoic native ad failed: ${error.message}'),
onImpression: () => print('Ezoic native ad impression'),
onClick: () => print('Ezoic native ad clicked'),
onOpen: () => print('Ezoic native ad opened an overlay'),
onClose: () => print('Ezoic native ad overlay closed'),
),
)
```
Replace `12345` with your Ezoic ad unit identifier (passed as a string). The native SDKs load the ad and render it in an SDK-built template through a platform view.
`EzoicNativeAdView` is a platform view and fills its parent's constraints, so wrap it in a `SizedBox` (or another constrained parent) to set its size.
The plugin destroys the underlying native ad with the platform view's lifecycle — no manual `destroy()` call is required.
`EzoicNativeAdView` supports these event callbacks:
- `onLoad`
- `onError` — receives an `EzoicNativeAdError` with `message` and `code`
- `onImpression`
- `onClick`
- `onOpen`
- `onClose`
On Android, the current native SDK expects a numeric Ezoic ad unit identifier. Pass it as a string in Flutter, for example `'12345'`.
## Privacy and Consent
The native SDKs can automatically read platform consent signals when they are set by a consent management platform. You can also set consent manually from Flutter:
```dart
await EzoicAds.setGDPRConsent(true, 'TCF_CONSENT_STRING');
await EzoicAds.setGPPConsent('GPP_STRING', '7');
await EzoicAds.setSubjectToCOPPA(false);
```
For platform-specific consent behavior, see the [Android privacy section](/docs/mobileapps/android/#privacy-and-consent) and [iOS privacy section](/docs/mobileapps/ios/#privacy-and-consent).
## Pageview Tracking
The native SDKs handle their platform-specific automatic pageview tracking. If your Flutter app uses Dart navigation, you can also track a pageview manually when the active route changes:
```dart
await EzoicAds.trackPageview();
```
## Troubleshooting
### SDK Not Initializing
1. Confirm the configured `domain` matches your Ezoic dashboard.
2. Confirm the native Android/iOS Ezoic SDK dependencies are installed for the platform you are building.
3. Enable `debugEnabled: true` and review platform logs.
### Ads Not Loading
1. Initialize Ezoic before rendering `EzoicBannerView` or `EzoicNativeAdView`.
2. Confirm the Ezoic ad unit identifier is configured in Ezoic.
3. Confirm the Google Mobile Ads application ID is present in `AndroidManifest.xml` or `Info.plist`.
4. Check the platform-specific setup in the [Android guide](/docs/mobileapps/android/) or [iOS guide](/docs/mobileapps/ios/).
---
## React Native
Source: https://docs.ezoic.com/docs/mobileapps/react-native/
The Ezoic React Native SDK lets you display banner, native, outstream video, instream video, rewarded, and interstitial ads in React Native apps with Prebid header bidding, Google Ad Manager, remote configuration, consent handling, and pageview tracking, all from a single JavaScript API.
The package is a thin bridge over Ezoic's native Android and iOS ad stacks, so ad loading and auctions run natively while you write only React Native code. This guide is self-contained: it covers everything you need to ship on both platforms.
## Requirements
- React Native 0.72 or higher
- Node.js 18 or higher
- For Android apps: Android SDK 24 or higher, Android Gradle Plugin 8.0 or higher
- For iOS apps: iOS 14.0 or higher, Xcode 15.0 or higher, CocoaPods 1.12 or higher
- A Google Mobile Ads application ID provided by Ezoic
- A domain configured for your site in Ezoic
Authentication is handled by the app's bundle/package identifier plus the configured domain. There is no client-side API key.
## Installation
Install the package version provided by Ezoic:
```sh
npm install @ezoic/react-native-sdk
```
The package ships the native Android and iOS Ezoic SDKs as transitive dependencies and registers them through React Native autolinking, so you do not add the native ad SDKs yourself.
### iOS pods
For apps that support iOS, install pods after adding the package:
```sh
cd ios
pod install
```
This resolves the native `EzoicAdsSDK` framework along with its Prebid Mobile and Google Mobile Ads dependencies. If your project uses frameworks, make sure `use_frameworks!` in your `Podfile` is compatible with Google Mobile Ads (static linkage is recommended).
## Android Setup
The package's Gradle module declares the Ezoic Android SDK dependency automatically. Your app project must be able to resolve it, which standard React Native apps already do because they include Google's Maven repository and Maven Central.
If your `android/build.gradle` or `android/settings.gradle` customizes repositories, confirm both are present:
```groovy
repositories {
google()
mavenCentral()
}
```
### Google Mobile Ads application ID
Add your Google Mobile Ads application ID to `android/app/src/main/AndroidManifest.xml`. This ID is provided by Ezoic and can be found in your Ezoic dashboard. Use the value assigned to your app unless your Ezoic representative gives you a different one.
```xml
```
### Advertising ID permission
If your app targets Android 12 (API 31) or higher and uses the advertising ID, add the permission to `AndroidManifest.xml`:
```xml
```
## iOS Setup
### Google Mobile Ads application ID
Add your Google Mobile Ads application ID to `ios//Info.plist`. This ID is provided by Ezoic and can be found in your Ezoic dashboard. Use the value assigned to your app unless your Ezoic representative gives you a different one.
```xml
GADApplicationIdentifierca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX
```
Also set `GADIsAdManagerApp` to `true` in `Info.plist` so Google Mobile Ads runs in Ad Manager mode.
### App Tracking Transparency
ATT has the single biggest impact on fill and CPM. Without it the IDFA is unavailable and much of programmatic demand will not bid or bids far lower.
Add the tracking usage description to `ios//Info.plist`. Apple requires this string in the app's `Info.plist` (it appears in the App Store privacy label and system prompt) — it cannot come from the SDK.
```xml
NSUserTrackingUsageDescriptionThis identifier will be used to deliver personalized ads to you.
```
**The SDK presents the ATT prompt for you on iOS.** When you call `EzoicAds.initialize`, the native iOS SDK shows the prompt (if undetermined) and waits for the decision before starting the ad stack, so the IDFA (if granted) is attached to the first ad request. You only need to add the `NSUserTrackingUsageDescription` string above.
```tsx
import { EzoicAds } from '@ezoic/react-native-sdk';
// The SDK requests ATT (if undetermined) before loading ads on iOS.
await EzoicAds.initialize({ domain: 'example.com' });
```
To drive ATT yourself instead (for a pre-prompt or custom timing), pass `requestATTBeforeAds: false` and request authorization before initializing — for example with `react-native-tracking-transparency`:
```tsx
import { requestTrackingPermission } from 'react-native-tracking-transparency';
import { EzoicAds } from '@ezoic/react-native-sdk';
await requestTrackingPermission();
await EzoicAds.initialize({ domain: 'example.com', requestATTBeforeAds: false });
```
### SKAdNetwork
`SKAdNetworkItems` lets buyers attribute installs when the IDFA is unavailable; missing identifiers suppress demand. Apple reads this key only from the app's main `Info.plist` — it is **not** aggregated from frameworks or SDKs.
Add the `SKAdNetworkItems` array with Google's published identifiers (`cstr6suwn9.skadnetwork` plus participating third-party buyers). Copy the current, complete list from Google's [Prepare privacy strategies](https://developers.google.com/ad-manager/mobile-ads-sdk/ios/privacy/strategies#enable-skadnetwork-to-track-conversions) page; identifiers must be lowercase.
```xml
SKAdNetworkItemsSKAdNetworkIdentifiercstr6suwn9.skadnetwork
```
## app-ads.txt
For both platforms, host an `app-ads.txt` file at the root of the developer website listed on your app's store page (for example, `https://example.com/app-ads.txt`). It authorizes the buyers that may sell your inventory; a missing or incomplete file causes most programmatic demand to be filtered out. Ezoic provides the required entries — confirm the file is published and current.
## Initialize the SDK
Initialize Ezoic once, early in your app lifecycle, before rendering any ads.
```tsx
import { EzoicAds } from '@ezoic/react-native-sdk';
import { useEffect } from 'react';
export function App() {
useEffect(() => {
EzoicAds.initialize({
domain: 'example.com',
debugEnabled: false,
testMode: false
});
}, []);
return null;
}
```
`domain` must match the domain configured for your site in Ezoic. Set `debugEnabled: true` during development to surface verbose native logs in Logcat (Android) and the Xcode console (iOS). Use `testMode: true` only while integrating; remove it for production traffic.
`initialize` resolves once the native SDK has finished bootstrapping. You can await it before rendering ads:
```tsx
await EzoicAds.initialize({ domain: 'example.com' });
```
## Add a Banner Ad
Render `EzoicBannerView` where the banner should appear. Give it explicit dimensions through `style` so the platform view is laid out correctly.
```tsx
import { EzoicBannerView } from '@ezoic/react-native-sdk';
import { SafeAreaView } from 'react-native';
export function ArticleScreen() {
return (
console.log('Ezoic banner loaded')}
onError={(error) => console.warn('Ezoic banner failed', error)}
/>
);
}
```
Replace `12345` with your Ezoic ad unit identifier. The native SDKs fetch the Google Ad Manager ad unit, Prebid configuration, targeting values, supported sizes, and refresh interval from Ezoic servers, so you do not configure those in the app.
The native Ezoic ad unit identifier is numeric. Pass it as a string in React Native, for example `"12345"`.
## Banner Sizes
Pass `size` as a `widthxheight` string. Common sizes are:
- `"320x50"`: Banner
- `"320x100"`: Large Banner
- `"300x250"`: Medium Rectangle
- `"468x60"`: Full Banner
- `"728x90"`: Leaderboard
You can also pass a comma-separated list to let the auction choose among several sizes, for example `size="300x250,320x50"`. Always size the `style` so the view can hold the largest size you request.
## Banner Events
`EzoicBannerView` supports these event props:
- `onLoad`: the banner received an ad
- `onError`: the banner failed to load; receives an error object
- `onImpression`: an impression was recorded
- `onClick`: the user tapped the ad
- `onOpen`: the ad opened a full-screen overlay
- `onClose`: the full-screen overlay was dismissed
## Add a Native Ad
Native ads deliver ad assets (headline, icon, media, body text, and call to action) rendered in a template designed to match the look and feel of your app content, rather than in a fixed banner or full-screen format. Like `EzoicBannerView`, `EzoicNativeAdView` is a React component: render it where the ad should appear, sized through `style`.
```tsx
import { EzoicNativeAdView } from '@ezoic/react-native-sdk';
export function ArticleScreen() {
return (
console.log('Ezoic native ad loaded')}
onError={(error) => console.warn('Ezoic native ad failed', error)}
onImpression={() => console.log('Ezoic native ad impression')}
onClick={() => console.log('Ezoic native ad clicked')}
onOpen={() => console.log('Ezoic native ad opened an overlay')}
onClose={() => console.log('Ezoic native ad overlay closed')}
/>
);
}
```
Replace `12345` with your Ezoic ad unit identifier. The native SDKs render the ad in a Google-built native ad template (`NativeAdView`) that lays out the headline, icon, media, body, and call-to-action for you; unlike `EzoicBannerView`, `EzoicNativeAdView` has no `size` prop, so the template lays out its assets inside the bounds you give it through `style`.
`EzoicNativeAdView` supports these event props:
- `onLoad`: the native ad received an ad
- `onError`: the native ad failed to load; receives an error object
- `onImpression`: an impression was recorded
- `onClick`: the user tapped the ad
- `onOpen`: the ad opened a full-screen overlay
- `onClose`: the full-screen overlay was dismissed
Unlike `EzoicBannerView`, `EzoicNativeAdView` accepts the Ezoic ad unit identifier as either a string or a number, for example `"12345"` or `12345`.
## Add a Rewarded Ad
Rewarded ads are full-screen ads that grant an in-app reward when the user finishes watching. Unlike banners, they are not React components: load one imperatively ahead of time (for example, at the start of a level), then present it at a natural break. `show()` resolves with the earned reward, or `null` if the user dismissed the ad early.
```tsx
import { EzoicRewardedAd } from '@ezoic/react-native-sdk';
async function runRewardedAd() {
try {
const ad = await EzoicRewardedAd.load('12345');
// Optional: observe lifecycle events
ad.setListeners({
onDismissed: () => console.log('Rewarded ad closed'),
onFailedToShow: (error) => console.warn('Show failed', error.message),
});
const reward = await ad.show();
if (reward) {
console.log(`Earned ${reward.amount} ${reward.type}`);
grantReward(reward.amount);
}
// Rewarded ads are single-use — release the handle when done.
ad.destroy();
} catch (error) {
console.warn('Rewarded ad failed to load', error);
}
}
```
Replace `12345` with your Ezoic ad unit identifier (passed as a string). Load and `show` are separate steps; calling `show()` before the ad has loaded rejects with an error. Load a new `EzoicRewardedAd` for each reward opportunity.
The reward `type` and `amount` come from the reward configured on the Google Ad Manager rewarded ad unit. `setListeners` accepts `onShown`, `onFailedToShow`, `onImpression`, `onClicked`, `onUserEarnedReward`, and `onDismissed` — all optional.
## Add an Interstitial Ad
Interstitial ads are full-screen ads shown at natural transition points (for example, between levels or screens). Unlike rewarded ads, they grant no reward. Like rewarded ads, they are not React components: load one imperatively ahead of time, then present it at a natural break. `show()` resolves when the ad is dismissed, or rejects if it fails to present.
```tsx
import { EzoicInterstitialAd } from '@ezoic/react-native-sdk';
async function runInterstitialAd() {
try {
const ad = await EzoicInterstitialAd.load('12345');
// Optional: observe lifecycle events
ad.setListeners({
onShown: () => console.log('Interstitial ad shown'),
onFailedToShow: (error) => console.warn('Show failed', error.message),
});
await ad.show();
console.log('Interstitial ad closed');
} catch (error) {
console.warn('Interstitial ad failed to load', error);
}
}
```
Replace `12345` with your Ezoic ad unit identifier (passed as a string). Load and `show` are separate steps; calling `show()` before the ad has loaded rejects with an error. Interstitial ads are single-use and auto-destroy once dismissed — load a new `EzoicInterstitialAd` for each opportunity. Call `ad.destroy()` yourself only if you loaded an ad and never showed it.
`setListeners` accepts `onShown`, `onFailedToShow`, `onImpression`, `onClicked`, and `onDismissed` — all optional.
## Add an Outstream Video Ad
Render `EzoicOutstreamAdView` where the video should appear. Like `EzoicNativeAdView`, it has no `size` prop — size it with `style` and the native view lays the player out inside those bounds. It is view-managed: mounting the component loads the ad, unmounting destroys it.
```tsx
import { EzoicOutstreamAdView } from '@ezoic/react-native-sdk';
export function ArticleScreen() {
return (
console.log('Ezoic outstream ad loaded')}
onError={(error) => console.warn('Ezoic outstream ad failed', error)}
onImpression={() => console.log('Ezoic outstream ad impression')}
onClick={() => console.log('Ezoic outstream ad clicked')}
onOpen={() => console.log('Ezoic outstream ad opened an overlay')}
onClose={() => console.log('Ezoic outstream ad overlay closed')}
/>
);
}
```
Replace `12345` with your Ezoic ad unit identifier. The native SDKs render the ad inline through Google Ad Manager at the size configured on the server.
`EzoicOutstreamAdView` supports these event props:
- `onLoad`: the outstream ad received an ad
- `onError`: the outstream ad failed to load; receives an error object
- `onImpression`: an impression was recorded
- `onClick`: the user tapped the ad
- `onOpen`: the ad opened a full-screen overlay
- `onClose`: the full-screen overlay was dismissed
Like `EzoicNativeAdView`, `EzoicOutstreamAdView` accepts the Ezoic ad unit identifier as either a string or a number, for example `"12345"` or `12345`.
## Add an Instream Video Ad
`EzoicInstreamAd` is a view-less controller for instream (pre/mid/post-roll) video. Unlike the banner, native, and outstream components, it renders nothing: your app owns the video player and the Google IMA SDK, and its sole deliverable is a Google Ad Manager VAST ad-tag URL string you feed to your own IMA `AdsRequest`. Unlike rewarded and interstitial ads, a controller is multi-use — it is not auto-destroyed, so you `load()` it repeatedly and `destroy()` it yourself.
```tsx
import { EzoicInstreamAd } from '@ezoic/react-native-sdk';
async function runInstreamAd() {
const instream = new EzoicInstreamAd('12345');
try {
const adTagUrl = await instream.load({ contentUrl: playingVideoUrl });
adsLoader.requestAds({ adTagUrl });
// On an IMA ad error, walk down the floor waterfall to the next tag.
const next = await instream.getNextAdTagUrl(); // null once exhausted
if (next) adsLoader.requestAds({ adTagUrl: next });
// On the IMA STARTED event, fire the Ezoic impression pixel.
await instream.reportImpression({ revenueUsd: 0.42 });
} catch (error) {
console.warn('Instream ad failed to load', error);
} finally {
await instream.destroy();
}
}
```
Replace `12345` with your Ezoic ad unit identifier (passed as a string). `load({ contentUrl })` resolves with the tag URL, or rejects on no fill, an uninitialized SDK, or an overlapping load already in flight for this id; `contentUrl` is optional and, when supplied, is added to the tag for contextual targeting. `getNextAdTagUrl()` resolves to `null` once the waterfall is exhausted. `reportImpression({ revenueUsd })` records the Ezoic impression on the IMA `STARTED` event; `revenueUsd` is optional. Call `destroy()` when the ad unit is no longer needed — the controller otherwise stays alive and reusable across loads.
## Privacy and Consent
The native SDKs automatically read standard IAB consent signals from platform storage when a consent management platform sets them — `UserDefaults` on iOS and `SharedPreferences` on Android:
- TCF v2 consent from `IABTCF_*` keys
- GPP consent from `IABGPP_*` keys
- US Privacy consent from the standard IAB US Privacy key
If you manage consent yourself, set it from React Native before ads load:
```ts
await EzoicAds.setGDPRConsent(true, 'TCF_CONSENT_STRING');
await EzoicAds.setGPPConsent('GPP_STRING', '7');
await EzoicAds.setSubjectToCOPPA(false);
```
- `setGDPRConsent(applies, consentString)`: whether GDPR applies and the TCF consent string
- `setGPPConsent(gppString, sectionIds)`: the GPP string and applicable section IDs
- `setSubjectToCOPPA(subject)`: whether the user is subject to COPPA
Manual values take precedence over automatically read signals.
## Pageview Tracking
After initialization, the native SDKs automatically track platform navigation: view controller transitions on iOS, and Activity, Fragment, and view-hosted Navigation on Android.
Because React Native apps drive screens from JavaScript, the native navigation signals do not see your in-app routes. Track a pageview manually whenever the active route changes so ad pacing and reporting stay accurate:
```ts
await EzoicAds.trackPageview();
```
If you use React Navigation, call it from a navigation state listener:
```tsx
import { NavigationContainer } from '@react-navigation/native';
import { EzoicAds } from '@ezoic/react-native-sdk';
export function Root() {
return (
{
EzoicAds.trackPageview();
}}
>
{/* navigators */}
);
}
```
## Troubleshooting
### SDK Not Initializing
1. Confirm the configured `domain` matches your Ezoic dashboard.
2. Confirm the device or simulator has network access.
3. Enable `debugEnabled: true` and review native logs: Logcat for `EzoicAds` on Android, the Xcode console on iOS.
4. On iOS, confirm `pod install` ran after installing the package and that the build uses the generated workspace.
### Ads Not Loading
1. Make sure `EzoicAds.initialize` runs and resolves before `EzoicBannerView` or `EzoicNativeAdView` mounts.
2. Confirm the Ezoic ad unit identifier is configured in Ezoic and passed as a string.
3. Confirm the Google Mobile Ads application ID is present in `AndroidManifest.xml` (Android) and `Info.plist` (iOS).
4. Give the banner explicit `style` dimensions large enough for the requested size.
5. Check consent configuration if your traffic is subject to privacy regulations.
### Build Failures
1. Android: confirm `google()` and `mavenCentral()` are available to your app's Gradle repositories.
2. iOS: delete `ios/Pods` and `ios/Podfile.lock`, then run `pod install` again.
3. Confirm your React Native version meets the minimum requirement and that autolinking is enabled.
---
## Unity
Source: https://docs.ezoic.com/docs/mobileapps/unity/
The Ezoic Unity SDK lets you show Ezoic banner, rewarded, and interstitial ads in Unity games while relying on the native Android and iOS SDKs for ad loading, Prebid, Google Ad Manager, consent handling, pageview tracking, and remote configuration.
The Unity package is a C# bridge over the native SDKs. It does not reimplement the ad stack in C#, so your game code stays platform-agnostic and the same calls run as safe no-ops in the editor.
## Requirements
- Unity 2021.3 LTS or higher
- Android SDK 24 or higher for Android apps
- iOS 14.0 or higher for iOS apps
- External Dependency Manager for Unity (EDM4U) recommended to resolve the native libraries
- Google Mobile Ads application ID provided by Ezoic
## Installation
In Unity, open **Window → Package Manager → + → Add package from git URL…** and enter the package git URL pinned to a released version:
```
https://github.com/ezoic/ezoic-unity-sdk.git#v1.0.1
```
Or add it to your project's `Packages/manifest.json` dependencies:
```json
{
"dependencies": {
"com.ezoic.ads": "https://github.com/ezoic/ezoic-unity-sdk.git#v1.0.1"
}
}
```
## Platform Setup
Unity games still need the native platform setup required by Android and iOS. The package ships an `Editor/EzoicDependencies.xml` manifest so the External Dependency Manager for Unity (EDM4U) can resolve the native libraries automatically. Install EDM4U from the [Unity Jar Resolver](https://github.com/googlesamples/unity-jar-resolver) project.
### Android
With EDM4U installed, the Android Resolver adds the native `com.ezoic.sdk:ezoic-ads-sdk:1.5.0` library (from Maven Central) to your generated Gradle build — no manual dependency needed.
The native SDK serves through Google Ad Manager, which requires your application ID in the Android manifest. Enable **Custom Main Manifest** under **Project Settings → Player → Android → Publishing Settings** and add the `meta-data` entry inside the `` tag of `Assets/Plugins/Android/AndroidManifest.xml`:
```xml
```
Replace the value with the Google Mobile Ads application ID provided for your account. A missing or incorrect app ID causes the app to crash on start. The application ID is usually provided by Ezoic and can be found in your Ezoic dashboard.
### iOS
With EDM4U installed, the iOS Resolver adds `pod 'EzoicAdsSDK', '~> 1.5.0'` to the generated Xcode project's `Podfile`. After Unity builds the Xcode project, run `pod install` in the build output directory if EDM4U has not already done so. iOS requires a deployment target of **14.0 or higher**, set under **Project Settings → Player → iOS → Other Settings → Target minimum iOS Version**.
Follow the [Required App Setup for Ads to Serve](/docs/mobileapps/ios/#required-app-setup-for-ads-to-serve) in the iOS guide. These steps live in your app's `Info.plist` and directly affect fill, so complete all of them:
- Adding the Google Mobile Ads application ID (`GADApplicationIdentifier`) and `GADIsAdManagerApp` to `Info.plist`
- Adding `NSUserTrackingUsageDescription` to `Info.plist` (the SDK presents the App Tracking Transparency prompt for you before loading ads — biggest impact on fill)
- Adding the `SKAdNetworkItems` identifiers to `Info.plist` (Apple does not read these from frameworks/SDKs)
## app-ads.txt
For both platforms, host an `app-ads.txt` file at the root of the developer website listed on your app's store page (for example, `https://example.com/app-ads.txt`). It authorizes the buyers that may sell your inventory; a missing or incomplete file causes most programmatic demand to be filtered out. Ezoic provides the required entries — confirm the file is published and current.
## Initialize the SDK
Initialize Ezoic once, early in your game's lifecycle, with your Ezoic-registered domain. Make the call from the Unity main thread (for example from a `MonoBehaviour`'s `Start`). The optional callback reports `(success, error)`.
```csharp
using Ezoic.Ads;
using UnityEngine;
public class AdsBootstrap : MonoBehaviour
{
void Start()
{
EzoicAds.Initialize("example.com", (success, error) =>
{
if (success)
{
Debug.Log("Ezoic Ads initialized. SDK version: " + EzoicAds.Version);
}
else
{
Debug.LogWarning("Ezoic Ads init failed: " + error);
}
});
}
}
```
`domain` must match the domain configured for your site in Ezoic. The native Android and iOS SDKs both authenticate using the configured domain plus the app's bundle/package identifier — there is no client-side API key. `EzoicAds.IsInitialized` becomes `true` once initialization succeeds.
## Add a Banner Ad
Construct an `EzoicBannerAd` with your ad unit id and a screen position, subscribe to its events, then call `Load()`. Show it once it loads.
```csharp
using Ezoic.Ads;
// Adaptive banner anchored to the bottom of the screen.
var banner = new EzoicBannerAd(adUnitId: 12345, position: BannerPosition.Bottom);
banner.OnLoaded += () => banner.Show();
banner.OnLoadFailed += error => Debug.LogWarning("Banner failed: " + error);
banner.OnClicked += () => Debug.Log("Banner clicked");
banner.OnImpression += () => Debug.Log("Banner impression");
banner.Load();
// later: banner.Hide(); banner.Show();
// when done: banner.Destroy();
```
Replace `12345` with your Ezoic ad unit id. Use `Show()` / `Hide()` to toggle visibility without reloading, and always call `Destroy()` when you are done with a banner to release the native ad.
## Banner Sizes
The banner constructor takes an optional `size` string. Pass `null` (the default) to request an adaptive banner sized by the native SDK, or a fixed size string such as `"320x50"`:
```csharp
// Fixed 320x50 banner at the top of the screen.
var banner = new EzoicBannerAd(adUnitId: 12345, position: BannerPosition.Top, size: "320x50");
```
Common fixed size strings include:
- `"320x50"`: standard banner
- `"320x100"`: large banner
- `"300x250"`: medium rectangle
- `"468x60"`: full banner
- `"728x90"`: leaderboard
`BannerPosition` supports `Top`, `Bottom`, `TopLeft`, `TopRight`, `BottomLeft`, `BottomRight`, and `Center`.
## Banner Events
`EzoicBannerAd` raises these events, all delivered on the Unity main thread:
- `OnLoaded`
- `OnLoadFailed(string error)`
- `OnClicked`
- `OnImpression`
## Add a Rewarded Ad
Rewarded ads are full-screen ads that grant an in-app reward when the user finishes watching. Load one imperatively ahead of time (for example, at the start of a level), subscribe to its events, then present it at a natural break. The static `Load` hands you a ready instance through its `onLoaded` callback.
```csharp
using Ezoic.Ads;
EzoicRewardedAd.Load(
adUnitId: 34567,
onLoaded: ad =>
{
ad.OnUserEarnedReward += (type, amount) =>
Debug.Log($"Reward earned: {amount} {type}");
ad.OnShown += () => Debug.Log("Rewarded shown");
ad.OnDismissed += () => Debug.Log("Rewarded dismissed");
ad.OnFailedToShow += error => Debug.LogWarning("Show failed: " + error);
ad.OnImpression += () => Debug.Log("Rewarded impression");
ad.OnClicked += () => Debug.Log("Rewarded clicked");
if (ad.IsLoaded)
{
ad.Show();
}
// when finished with it: ad.Destroy();
},
onFailed: error => Debug.LogWarning("Rewarded load failed: " + error));
```
Replace `34567` with your Ezoic ad unit id. The reward `type` and `amount` come from the reward configured on the Google Ad Manager rewarded ad unit. Load and `Show()` are separate steps; guard `Show()` behind `IsLoaded`. Load a new `EzoicRewardedAd` for each reward opportunity, and call `Destroy()` when you are done with an instance.
## Add an Interstitial Ad
Interstitial ads are full-screen ads shown at natural transition points (for example, between levels or screens). They grant no reward. Like rewarded ads, load one ahead of time and present it at a break.
```csharp
using Ezoic.Ads;
EzoicInterstitialAd.Load(
adUnitId: 23456,
onLoaded: ad =>
{
ad.OnShown += () => Debug.Log("Interstitial shown");
ad.OnDismissed += () => Debug.Log("Interstitial dismissed");
ad.OnFailedToShow += error => Debug.LogWarning("Show failed: " + error);
ad.OnImpression += () => Debug.Log("Interstitial impression");
ad.OnClicked += () => Debug.Log("Interstitial clicked");
if (ad.IsLoaded)
{
ad.Show();
}
// when finished with it: ad.Destroy();
},
onFailed: error => Debug.LogWarning("Interstitial load failed: " + error));
```
Replace `23456` with your Ezoic ad unit id. Load and `Show()` are separate steps; guard `Show()` behind `IsLoaded`. Load a new `EzoicInterstitialAd` for each opportunity, and call `Destroy()` when you are done with an instance.
## Try the Sample
The package ships a **Basic Integration** sample: a single `MonoBehaviour` with an on-screen control panel that initializes the SDK and loads, shows, hides, and destroys every ad type while logging each ad event.
To import it, open **Window → Package Manager**, select **Ezoic Ads** in the package list, open the **Samples** tab, and click **Import** next to *Basic Integration*. Then attach the imported `EzoicAdsDemo` script to a `GameObject` in an empty scene, set your domain and ad unit ids in the Inspector, and build to a device.
## Example Project
For a complete, ready-to-open project, see the [Ezoic Unity SDK example](https://github.com/ezoic/ezoic-unity-sdk-example) on GitHub. It is a minimal Unity project that installs this SDK through the UPM git URL and shows how to test your ad code without a device:
- `Assets/App/AdsBootstrap.cs`: an app-style integration that initializes the SDK, then loads a banner, an interstitial, and a rewarded ad, recording every outcome.
- `Assets/Tests/EditMode/`: tests for the synchronous API surface (nothing throws, `IsInitialized` and `Version` defaults are correct).
- `Assets/Tests/PlayMode/`: tests that initialization and load callbacks are always delivered on the Unity main thread, never inline.
- `.github/workflows/ci.yml`: runs both test suites headlessly on every push with [GameCI](https://game.ci).
Open the folder in Unity Hub (Unity 6000.3 LTS; any 2021.3 or newer editor works if you adjust `ProjectSettings/ProjectVersion.txt`). Unity resolves the SDK from GitHub automatically. Run the tests from **Window → General → Test Runner**.
## Privacy and Consent
The native SDKs can automatically read platform consent signals when they are set by a consent management platform. You can also set consent manually, any time after initialization:
```csharp
EzoicAds.SetGDPRConsent(true, "TCF_CONSENT_STRING");
EzoicAds.SetGPPConsent("GPP_STRING", "7");
EzoicAds.SetSubjectToCOPPA(false);
```
For platform-specific consent behavior, see the [Android privacy section](/docs/mobileapps/android/#privacy-and-consent) and [iOS privacy section](/docs/mobileapps/ios/#privacy-and-consent).
## Pageview Tracking
The native SDKs handle their platform-specific automatic pageview tracking. If your game changes screens, you can also track a pageview manually:
```csharp
EzoicAds.TrackPageview();
```
## Troubleshooting
### Native library unresolved
If the build fails to find the native Ezoic library, confirm the External Dependency Manager for Unity (EDM4U) is installed. It reads the package's dependency manifest and resolves the native Android library and iOS CocoaPod for you; without it you must add the Gradle dependency and Podfile line manually.
### No ads in the editor
Ads only serve on Android and iOS devices. In the Unity editor (and any other platform) the SDK runs as a safe no-op stub: calls never throw, load callbacks report failure, `EzoicAds.IsInitialized` is `false`, and `EzoicAds.Version` is an empty string. Build to a device to see ads.
### iOS pods not installed
After Unity generates the Xcode project, EDM4U writes the `EzoicAdsSDK` pod into the `Podfile`. If pods are missing, run `pod install` in the Xcode build output directory. Also confirm the Google Mobile Ads application ID and the App Tracking Transparency description are present in `Info.plist`.
# Web Games
---
## Getting Started
Source: https://docs.ezoic.com/docs/webgames/getting-started/
The Game SDK has two sides:
1. **The host page** — the ads.txt-approved website that embeds the game. It runs the standard Ezoic JavaScript integration. All auctions run here, and all ads render here (confined to the game's play area), because this is the domain advertisers bid on.
2. **The game** — an HTML5 game running inside an iframe, usually served from a different origin (a game CDN or portal). It loads `gamesdk.js` and calls the `EzGameSDK` API. Every call is proxied to the host page over a `postMessage` bridge.
The game never runs an auction itself and needs no ads.txt, no dashboard setup, and no placeholders of its own.
## Host page setup
The host page needs the standard [Ezoic JavaScript integration](/docs/ezoicads/integration/) and at least one `showAds()` call, which loads the ad pipeline the game bridge delegates to:
```html
```
Then embed the game with the `data-ez-game` attribute. The bridge only answers frames that carry this attribute — handshakes from unmarked iframes are rejected:
```html
```
That is the entire host-page integration. If the host page ever needs to turn the bridge off, it can call `ezstandalone.config({ gameSdk: false })`.
## Game-side setup
Inside the game, load the SDK and queue your startup code on `EzGameSDK.cmd`. The queue pattern means your code runs whether it executes before or after `gamesdk.js` finishes loading (the same pattern as `ezstandalone.cmd`):
```html
```
`init()` detects the environment and, when embedded on an Ezoic page, performs the handshake with the host bridge. It resolves with one of three environments:
| Environment | Meaning | Ad behavior |
|---|---|---|
| `ezoic` | Embedded on a page whose Ezoic bridge answered the handshake | Real ads, rendered by the host page |
| `local` | Running as the top window (opened directly / local development) | Simulated ads, so the game is fully testable without Ezoic |
| `unavailable` | Embedded in a parent frame but no bridge responded | Ad calls resolve quickly with `{success: false, error: 'noParent'}` so the game keeps running |
Your game logic does not need to branch on the environment — the same API surface and the same success/failure shapes apply in all three.
## Your first ad break
Call `commercialBreak()` at natural pauses. Use the `onStart` callback to pause gameplay and mute audio — it fires right before the ad actually displays:
```javascript
// Before gameplay starts
EzGameSDK.commercialBreak({ position: 'preroll', onStart: pauseGame })
.then(function (result) {
resumeGame();
});
// Between levels — display format keeps it quick
function onLevelComplete() {
EzGameSDK.commercialBreak({ position: 'midgame', format: 'display', onStart: pauseGame })
.then(function (result) {
resumeGame();
});
}
```
Rewarded ads must always be the user's choice — offer them through a button or prompt, never trigger them automatically. Grant the reward only when `rewarded` is `true`:
```javascript
function onWatchAdForExtraLife() {
EzGameSDK.rewardedBreak({ rewardName: 'extra_life', onStart: pauseGame })
.then(function (result) {
if (result.rewarded) {
grantExtraLife();
}
resumeGame();
});
}
```
And a banner anchored to the edge of the game frame:
```javascript
EzGameSDK.showBanner({ width: 320, height: 50, anchor: 'bottom' });
```
See the [API Reference](../api-reference) for every method, parameter, result shape, and error code.
## Testing
- **Local development**: open the game directly (not in an iframe). The SDK runs in `local` mode and every ad call shows a simulated ad that resolves with the same result shapes as production, so you can develop the full ad flow offline.
- **Live example**: a working cross-origin integration — real game, real host page, live bridge log — is at [examples.ezoic.com/game-sdk](https://examples.ezoic.com/game-sdk/), with copyable source.
---
## API Reference
Source: https://docs.ezoic.com/docs/webgames/api-reference/
All methods live on the global `window.EzGameSDK`, installed by `gamesdk.js`. Every method returns a Promise and never rejects — failures resolve with `{success: false, error, message}` so a missing ad can never break the game. Call `init()` before any other method; calls made before `init()` resolves return `{success: false, error: 'internal'}` (except `hasAdBlock()`, which resolves `false`).
Set `EzGameSDK.debug = true` to log SDK activity to the console.
## init(config)
Initializes the SDK: detects the environment and, when embedded on an Ezoic page, performs the `postMessage` handshake with the host bridge. Safe to call multiple times — subsequent calls return the same promise, and the config is read only on the first call.
```javascript
EzGameSDK.init({ breakCooldownSec: 5 }).then(function (info) {
// info.environment: 'ezoic' | 'local' | 'unavailable'
});
```
| Parameter | Type | Default | Description |
|---|---|---|---|
| `breakCooldownSec` | number | 5 | Minimum seconds between commercial breaks. `0` disables the cooldown; values above `3600` are clamped to `3600`. |
When the game does not set `breakCooldownSec`, the host page's bridge can supply its own default during the handshake. A value set by the game always wins.
**Resolves:** `{environment: string}`
## commercialBreak(config)
Shows a full-frame ad break covering the game's play area (not the whole host page). Use `position: 'preroll'` before gameplay starts and `'midgame'` at natural pauses such as level transitions or game over.
```javascript
EzGameSDK.commercialBreak({
position: 'midgame',
format: 'auto',
onStart: function () { pauseGame(); muteAudio(); },
}).then(function (result) {
// result: {success, watched} or {success: false, error, message}
resumeGame();
});
```
| Parameter | Type | Default | Description |
|---|---|---|---|
| `position` | string | `'midgame'` | `'preroll'` or `'midgame'`. |
| `format` | string | `'auto'` | `'auto'` — video ad with display fallback. `'display'` — display ad only (quickest; good between levels). `'video'` — video ad only; resolves `{success: false, error: 'unfilled'}` when no video demand fills. |
| `onStart` | function | — | Called right before the ad displays. Pause gameplay and mute audio here — an auction runs first, so there can be a delay between the call and the ad appearing. |
**Resolves:** `{success: true, watched: boolean}` on success — `watched` is `true` when the user saw the ad through rather than skipping. On failure: `{success: false, error, message}`.
Only one break (commercial or rewarded) can run at a time; a second call while one is in progress resolves with `error: 'busy'`. Calls inside the cooldown window resolve with `error: 'adCooldown'` and a message stating the seconds remaining.
## rewardedBreak(config)
Shows a rewarded ad. Rewarded ads must be **user-opted-in**: present them as a choice (a button or prompt that states the reward), never trigger them automatically, and never mislead the user about what they get.
```javascript
EzGameSDK.rewardedBreak({ rewardName: 'extra_life', onStart: pauseGame })
.then(function (result) {
if (result.rewarded) {
grantExtraLife();
}
resumeGame();
});
```
| Parameter | Type | Default | Description |
|---|---|---|---|
| `rewardName` | string | `''` | Label for the reward, used for analytics. |
| `onStart` | function | — | Called right before the ad displays. |
**Resolves:** `{success: true, rewarded: boolean}` — `rewarded` is `true` only when the user watched the ad to completion. Grant the in-game reward only in that case. On failure: `{success: false, rewarded: false, error, message}`.
Rewarded breaks are not subject to the commercial-break cooldown, but they share the one-break-at-a-time rule (`error: 'busy'`).
## showBanner(config)
Shows a banner ad anchored to the top or bottom edge of the game frame.
`width` and `height` describe the **maximum box** the game reserves — not one exact size. The bridge offers every standard ad size that fits inside the box, and the ad server picks one supported on the current device. The promise resolves only after the ad server confirms a fill; when nothing fills the box (for example a mobile-only 320x50 box on desktop) it resolves `{success: false, error: 'unfilled'}` and no empty container is left behind.
```javascript
EzGameSDK.showBanner({ width: 728, height: 90, anchor: 'bottom' })
.then(function (result) {
if (result.success) {
shrinkPlayAreaForBanner();
}
});
```
| Parameter | Type | Default | Description |
|---|---|---|---|
| `width` | number | 320 | Maximum banner width in pixels. Capped at 970. |
| `height` | number | 50 | Maximum banner height in pixels. Capped at 250. |
| `anchor` | string | `'bottom'` | `'top'` or `'bottom'` edge of the game frame. |
Requested dimensions are capped at 970x250 — the largest standard banner size.
**Resolves:** `{success: boolean, error?, message?}`
## hideBanner()
Hides the banner shown by `showBanner()`.
```javascript
EzGameSDK.hideBanner();
```
**Resolves:** `{success: boolean, error?, message?}`
## gameplayStart() / gameplayStop()
Signal when active gameplay starts and stops (pause, level end, game over). While gameplay is active, the host page suppresses its own page-level interstitials so they never interrupt play, and the signals are recorded for analytics. Fire-and-forget — both always resolve.
```javascript
EzGameSDK.gameplayStart();
// ... player is playing ...
EzGameSDK.gameplayStop();
```
**Resolves:** `{success: boolean}`
## hasAdBlock()
Detects whether the user has an ad blocker. On Ezoic pages the check runs on the host page; elsewhere the SDK falls back to a local bait-element check inside the game frame.
```javascript
EzGameSDK.hasAdBlock().then(function (blocked) {
if (blocked) {
showAdblockMessage();
}
});
```
**Resolves:** `boolean` — `true` when an ad blocker is detected. Never rejects; detection failures resolve `false`.
## Error codes
Failed calls resolve (never reject) with `{success: false, error, message}`:
| Code | Meaning |
|---|---|
| `adCooldown` | A commercial break was requested inside the cooldown window. The message states the seconds remaining. |
| `busy` | Another ad break — or another ad flow on the host page — is already in progress. |
| `unfilled` | The auction ran but no ad filled (for example `format: 'video'` with no video demand, or a banner box no standard size fits). |
| `noParent` | The game is embedded but no Ezoic bridge responded (environment `unavailable`). |
| `timeout` | The host page did not answer within the time limit. |
| `internal` | Unexpected failure, including calls made before `init()` resolved. |
When the host page disables the bridge with `ezstandalone.config({gameSdk: false})` — or the game iframe is missing the `data-ez-game` attribute — the handshake is rejected, `init()` resolves with environment `unavailable`, and ad calls return `error: 'noParent'`.
## Cooldown behavior
The cooldown is the minimum interval between commercial breaks (default 5 seconds). It is enforced on both sides: the SDK gates calls client-side, and the host bridge enforces it independently. Rejected calls resolve with `error: 'adCooldown'` without showing anything. `rewardedBreak` is exempt — a user who chose to watch an ad should never be blocked by a timer.
Configure it once in `init({breakCooldownSec})`; it cannot be changed per call.
# Analytics MCP
---
## ChatGPT
Source: https://docs.ezoic.com/docs/analytics-mcp/chatgpt/
Connect [ChatGPT](https://chatgpt.com) to Ezoic and ask about your site's traffic, ad revenue, video performance, and more from a chat.
Ezoic Analytics is available as an official plugin in the [ChatGPT plugin directory](https://chatgpt.com/plugins).
## Connect
[Open in ChatGPT](https://chatgpt.com/plugins/plugin_asdk_app_6a515f15215c819196d35d1462b6c542)
Or install from inside ChatGPT:
1. Open **Plugins**.
2. Search for **Ezoic Analytics**, or open the [plugin directory page](https://chatgpt.com/plugins/plugin_asdk_app_6a515f15215c819196d35d1462b6c542).
3. Click **Install plugin**.
4. When prompted, sign in with your Ezoic account and click **Allow**.
5. Start a new chat and ask about your analytics — or type `@` and choose **Ezoic Analytics** to invoke the plugin directly.
## Next steps
- See [example questions and available data](/docs/analytics-mcp/) on the Analytics MCP overview.
- Auth, permissions, and [troubleshooting](/docs/analytics-mcp/#troubleshooting) live on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## Claude
Source: https://docs.ezoic.com/docs/analytics-mcp/claude/
Connect [Claude.ai](https://claude.ai) or Claude Desktop to Ezoic and ask questions about your site's traffic, ad revenue, video performance, and more — right from a chat.
Ezoic Analytics is available as an official connector in the [Claude connectors directory](https://claude.ai/directory/connectors/ezoic-analytics).
## Connect
[Open in Claude](https://claude.ai/directory/connectors/ezoic-analytics)
Or install from inside Claude:
1. In a chat, click the **+** button next to the message box.
2. Choose **Add connector** → **Browse connectors**.
3. Search for **Ezoic Analytics** and open it.
4. Click **Connect**, then sign in with your Ezoic account and click **Allow**.
5. Start a new chat and ask about your analytics.
## Next steps
- Using the terminal? See [Claude Code](/docs/analytics-mcp/claude-code/).
- See [example questions and available data](/docs/analytics-mcp/) on the Analytics MCP overview.
- Auth, permissions, and [troubleshooting](/docs/analytics-mcp/#troubleshooting) live on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## Claude Code
Source: https://docs.ezoic.com/docs/analytics-mcp/claude-code/
Connect [Claude Code](https://docs.anthropic.com/en/docs/claude-code) to Ezoic analytics from the terminal.
## Install
```bash
claude mcp add --transport http ezoic-analytics https://analytics-mcp.ezoic.com/mcp
```
When the tools first connect, complete the Ezoic OAuth sign-in in the browser.
## Next steps
- See [example questions and available data](/docs/analytics-mcp/) on the Analytics MCP overview.
- Auth, permissions, and [troubleshooting](/docs/analytics-mcp/#troubleshooting) live on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## Cursor
Source: https://docs.ezoic.com/docs/analytics-mcp/cursor/
Connect [Cursor](https://cursor.com) to Ezoic and ask about your site's traffic, ad revenue, video performance, and more from the agent chat.
## Connect
[Add to Cursor](https://cursor.com/install-mcp?name=ezoic-analytics&config=eyJ1cmwiOiJodHRwczovL2FuYWx5dGljcy1tY3AuZXpvaWMuY29tL21jcCJ9)
Or add to `~/.cursor/mcp.json` (or your project's `.cursor/mcp.json`):
```json
{"mcpServers": {"ezoic-analytics": {"url": "https://analytics-mcp.ezoic.com/mcp"}}}
```
Reload MCP servers / restart Cursor if needed, then complete the Ezoic OAuth sign-in when prompted.
## Next steps
- See [example questions and available data](/docs/analytics-mcp/) on the Analytics MCP overview.
- Auth, permissions, and [troubleshooting](/docs/analytics-mcp/#troubleshooting) live on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## VS Code
Source: https://docs.ezoic.com/docs/analytics-mcp/vscode/
Connect [VS Code](https://code.visualstudio.com/) to Ezoic analytics from the editor.
## Connect
[Add to VS Code](https://insiders.vscode.dev/redirect/mcp/install?name=ezoic-analytics&config=eyJ0eXBlIjoiaHR0cCIsInVybCI6Imh0dHBzOi8vYW5hbHl0aWNzLW1jcC5lem9pYy5jb20vbWNwIn0=)
Or from the command line:
```bash
code --add-mcp '{"name":"ezoic-analytics","type":"http","url":"https://analytics-mcp.ezoic.com/mcp"}'
```
Complete the Ezoic OAuth sign-in when prompted.
## Next steps
- See [example questions and available data](/docs/analytics-mcp/) on the Analytics MCP overview.
- Auth, permissions, and [troubleshooting](/docs/analytics-mcp/#troubleshooting) live on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
---
## Codex
Source: https://docs.ezoic.com/docs/analytics-mcp/codex/
Connect [Codex CLI](https://github.com/openai/codex) to Ezoic analytics.
## Configure
Add this to `~/.codex/config.toml`:
```toml
[mcp_servers.ezoic-analytics]
url = "https://analytics-mcp.ezoic.com/mcp"
```
Restart Codex if it is already running, then complete the Ezoic OAuth sign-in when the server connects.
## Next steps
- See [example questions and available data](/docs/analytics-mcp/) on the Analytics MCP overview.
- Auth, permissions, and [troubleshooting](/docs/analytics-mcp/#troubleshooting) live on the overview page.
---
Need help? Reach us through [Ezoic Support](https://support.ezoic.com).
# Custom Events
---
## Getting Started
Source: https://docs.ezoic.com/docs/customevents/getting-started/
## Overview
Custom Events let publishers send their own events to Ezoic. These events can be used in Ezoic's Big Data Analytics (BDA) platform for dynamic reporting and analysis.
## Usage
To send a custom event:
```javascript
window.ezoicEvent = window.ezoicEvent || {};
window.ezoicEvent.queue = window.ezoicEvent.queue || [];
window.ezoicEvent.queue.push(function(){
window.ezoicEvent.add("event_name", "event_value");
});
```
Replace event_name and event_value with your own values. Your events will be available in BDA and can be viewed in the Custom Event report.
# Ezoic Subscriptions
---
## Getting Started
Source: https://docs.ezoic.com/docs/subscriptions/getting-started/
Ezoic Subscriptions helps you launch paid access without building checkout, subscriber sessions, payment handling, or account management from scratch. The implementation is centered on the onsite script: your site checks a product handle, then delivers the paid benefit when access is allowed — most commonly removing ads, and also unlocking premium content — and opens Ezoic checkout when access is needed.
The most common setup is selling an ad-free experience, so this guide uses a `remove-ads` product as its running example. Gating premium article content works the same way — swap in a `premium` product where it fits your site.
## Before You Start
You need:
1. The ability to edit your site HTML, templates, theme, or application code.
2. Access to Ezoic Subscriptions setup in your Ezoic account.
3. Completed payment onboarding so checkout can sell access.
4. A product with a product handle (such as `remove-ads`) and at least one price.
5. A clear decision about the benefit subscribers receive — an ad-free experience, unlocked content, or another feature.
If someone else maintains your site, share the integration guides with them before launch.
## Key Concepts
**Product:** The paid benefit you sell — such as removing ads or unlocking premium content — bought as a subscription or a one-time purchase. A product like `Remove Ads` is what your site code checks and sells. You can create more than one product per site.
**Product handle:** The developer-facing key your site checks with `hasAccess(...)` and passes to `showPaywall({ product })`. For example, a product named `Remove Ads` might use the product handle `remove-ads`.
**Price:** A way to buy a product, such as `Monthly` or `Annual`. Each price has a price handle you can pass to `openCheckout({ price })` for custom checkout buttons.
**Donation:** Optional reader support that does not unlock pages or grant a product. Donations are pay-what-you-want above a minimum amount you set — in USD by default, with optional per-currency minimums and suggested amounts.
**Subscriber portal:** The account area at `https://subscriber.ezoic.com` where subscribers can manage subscriptions, receipts, invoices, and payment methods.
## Setup Checklist
1. Complete payment onboarding in your Ezoic account.
2. Create a product. It gets a **product handle** (such as `remove-ads`) that your site code uses. Add at least one **price**, which gets its own **price handle**.
3. Add the onsite script to every page where subscriptions or donations should work:
```html
```
4. Check access with `ezsubscriptions.hasAccess("remove-ads")` to decide whether to deliver the paid benefit — for example, removing ads or revealing content.
5. When access is not allowed and you're ready to sell it, open the paywall with `ezsubscriptions.showPaywall({ product: "remove-ads" })`.
6. If you gate article or page content for readers, add paywalled-content structured data before launch (not needed for ad removal, tools, or other non-article products).
7. Optionally collect donations with a `data-ezoic-donate` button or `ezsubscriptions.openDonation()`.
8. [Test your checkout in test mode](/docs/subscriptions/testing-sandbox/) with Stripe test cards — no real charges.
9. Verify checkout, login, and the subscriber portal before announcing paid access.
## Next Steps
- Complete the prerequisites: [Payment Setup](/docs/subscriptions/payment-setup/) and [Products, Prices, and Paid Access](/docs/subscriptions/products/).
- Install the widget with [Onsite Script Integration](/docs/subscriptions/site-integration/).
- Gate content and features with the [Publisher-Managed Access API](/docs/subscriptions/publisher-managed-access/).
- Choose how visitors sign in with [Visitor Authentication](/docs/subscriptions/visitor-authentication/).
- Match your brand with [Paywall Appearance and Styling](/docs/subscriptions/paywall-appearance/).
- For gated articles, follow [SEO and Paywalling Best Practices](/docs/subscriptions/seo-paywalling-best-practices/).
- Collect reader support with [Donations](/docs/subscriptions/donations/).
- Understand what buyers see in the [Subscriber Experience](/docs/subscriptions/subscriber-experience/).
- Verify your integration in [Testing with Test Mode](/docs/subscriptions/testing-sandbox/) before launch.
- Make access decisions on your own server with the [Server-to-Server REST API](/docs/subscriptions/rest-api/).
- Debug integration issues with [Troubleshooting](/docs/subscriptions/troubleshooting/).
---
## Payment Setup
Source: https://docs.ezoic.com/docs/subscriptions/payment-setup/
Payment setup is a prerequisite for checkout. The onsite script can be installed before this is complete, but visitors cannot buy paid access until your payment setup can take live payments — an approved Ezoic-managed configuration, or a connected Stripe account with charges enabled.
Complete payment onboarding before creating paid products.
## Payment Options
### Ezoic-Managed Payments
Ezoic-managed payments use Ezoic's Stripe account to process subscriber payments, and Ezoic pays you out. There's no Stripe Connect onboarding for your own account, but Ezoic reviews your site before it can take live payments, which can take several days.
### Connect Your Stripe Account
Stripe Connect lets you use your own Stripe account for Ezoic Subscriptions. This is the recommended option in the dashboard when you want payments to flow directly through your Stripe account.
After you choose this option, the dashboard sends you to Stripe to complete onboarding. Stripe may take a few days to review your account before charges and payouts are fully enabled.
## Stripe Account Status
For connected Stripe accounts, the dashboard may show:
- **Details submitted:** Whether Stripe has received the required account information.
- **Charges enabled:** Whether the account can accept payments.
- **Payouts enabled:** Whether the account can receive payouts.
- **Requirements due:** Whether Stripe needs more information before the account is ready.
- **Disabled reason:** Why Stripe has disabled the account, shown when it has.
If Stripe asks for more information, complete the requested steps in Stripe and then refresh the status in Ezoic.
## Apple Pay and Google Pay
Apple Pay and Google Pay are **on by default**. As soon as your payment setup is active, Ezoic registers your site with Stripe so the wallet buttons appear at checkout for buyers on supported devices and browsers (for example, Apple Pay in Safari on iPhone). This works with both Ezoic-managed payments and your own connected Stripe account.
There is nothing to install or host — you don't need to add any file to your site. Stripe verifies your domain automatically.
Wallet payments are a per-domain setting in your Ezoic dashboard, in the Subscriptions area under **Settings → Ways buyers pay**, as **Wallet payments**. Leave it on to offer Apple Pay and Google Pay, or turn it off to hide the wallet buttons at checkout.
## Adaptive Pricing
Adaptive Pricing presents international visitors with a local-currency equivalent of your prices at checkout, converted at the payment provider's exchange rate, with a small conversion fee for the buyer. It is **on by default**, in the same **Settings → Ways buyers pay** card as wallet payments.
**Recommended:** set your own amounts per market with [additional currencies](/docs/subscriptions/products/#additional-currencies) on your prices — a visitor whose region matches one of your currencies pays that exact amount in that currency, with no conversion fee.
## After Payment Setup
Once your payment setup can take live payments, create your [products and prices](/docs/subscriptions/products/), then add the [onsite script](/docs/subscriptions/site-integration/) to your site.
---
## 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: once checkout knows their identity or card, it continues at the regular price. Until then the paywall can still show the trial call to action, so a returning visitor may see the trial offer briefly before checkout converts it to the paid price.
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.
---
## Promo Codes
Source: https://docs.ezoic.com/docs/subscriptions/promo-codes/
A promo code is a code your visitors type at checkout — such as `SAVE20` — to get a percentage or fixed-amount discount. You create and manage promo codes in the Ezoic dashboard; the onsite checkout picks them up automatically, so there is no code to write on your site.
## Create a Promo Code
Open the **Subscriptions → Products** page in your Ezoic dashboard and create a promo code. Each code has:
- **Code** — what the visitor types at checkout (letters, numbers, and hyphens, matched case-insensitively). You can enter your own or generate one.
- **Name** — an optional internal label shown on receipts, such as `Spring sale`.
- **Discount** — either a **percentage** off or a **fixed amount** off in a chosen currency.
- **Duration** — how long the discount lasts on a subscription:
- **Once** — the first payment only.
- **Repeating** — a set number of months, then billing returns to full price.
- **Forever** — every payment for as long as the subscription stays active.
- **Products** — restrict the code to specific products, or leave it empty to discount every product on the domain.
- **First-time customers only** — restrict the code to visitors who have never bought from you.
## Limits
Each of these is optional:
- **Max redemptions** — a cap on how many times the code can be used in total. Unlimited by default.
- **Expires** — a date after which the code stops working. Never expires by default.
- **Minimum order amount** — a minimum subtotal required to redeem the code.
## At Checkout
When your domain has at least one active promo code, the checkout shows a **Have a promo code?** link that opens a code entry field. The visitor enters a code and the discounted total appears in the order summary before they pay — validated by Stripe for a paid checkout, and by Ezoic Subscriptions for a trial start. There is nothing to add to your onsite integration — the field appears and disappears based on whether active codes exist.
A checkout accepts one promo code at a time.
## Trials and One-Time Purchases
- **Free trials** — **Repeating** and **Forever** codes work with a [free trial](/docs/subscriptions/products/#free-trials); the discount applies from the subscription start. **Once** codes **cannot** be used with a trial and are rejected at checkout — Stripe would spend the discount on the $0 trial invoice, leaving the first real charge undiscounted. Use a repeating or forever code to discount a trial plan.
- **One-time purchases** — a code discounts the single payment.
- **Donations** — promo codes do not apply to [donations](/docs/subscriptions/donations/).
## Managing Promo Codes
From the promo code list on the Products page you can:
- **Enable or disable** a code without deleting it — a disabled code stops working immediately and can be turned back on later.
- **Delete** a code permanently. Deletion prevents future redemptions but does not remove a discount already applied to an existing subscription.
- **Duplicate** an existing code's settings into a new code.
Product restrictions are fixed once a code is created. To change which products a code applies to, create a new code.
## Related
- [Products, Prices, and Paid Access](/docs/subscriptions/products/) — the products a promo code can discount.
- [Subscriber Experience](/docs/subscriptions/subscriber-experience/) — what a buyer sees at checkout.
---
## 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
```
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
```
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
```
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
```
`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
```
Or call the API from your own handler:
```html
```
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
```
```html
```
`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
```
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
```
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
```
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.
---
## Testing with Test Mode
Source: https://docs.ezoic.com/docs/subscriptions/testing-sandbox/
Test mode lets you experience Ezoic Subscriptions exactly as a subscriber would — checkout, login, access, and the subscriber portal — using Stripe test payments. Nothing in test mode touches your real business: no real charges, no live subscribers, and no impact on your revenue, payout estimates, or analytics.
Use it to verify your integration before launch, and to reproduce checkout or access issues any time after.
## Before You Start
Test mode needs two things in place:
1. **A product or donation** — a test checkout buys one of your products (or makes a test donation), so create at least one on the **Products** page first. See [Products and Prices](/docs/subscriptions/products/).
2. **The onsite script** — test mode opens on **your own site**, so the Ezoic Subscriptions script must be installed and detected there. See [Onsite Script Integration](/docs/subscriptions/site-integration/).
The preview unlocks automatically once the script is detected — there is nothing to enable.
## Entering Test Mode
1. In your Ezoic account, open **Subscriptions Manager → Products**.
2. Click **Preview in test mode**.
3. Your site opens in a new tab with test mode active, marked by a **Test mode — no real charges** pill at the bottom-left of every page.
From there, walk the full subscriber flow: open the paywall, check access with `hasAccess(...)`, and complete checkout. At checkout, use any [Stripe test card](https://docs.stripe.com/testing) — for example `4242 4242 4242 4242` with any future expiry, any CVC, and any ZIP.
## Test Data in Your Dashboard
Test purchases and subscribers appear in your dashboard, clearly separated from live data:
- The **Subscriptions** and **Payments** pages have a **Live mode | Test mode** toggle, and the **Access** page follows the same selection. Switch to Test mode to inspect the test records your preview created.
- While Test mode is on, a banner reminds you that you're viewing test data — it has no impact on your live data.
- Test payments are labeled, and their amounts and fees are excluded from your real revenue and payout estimates.
Pages outside these three always show live data, regardless of the toggle.
## Test Data Retention
Test-mode subscriptions are automatically canceled after 90 days and permanently deleted 30 days later. Test records are for validation, not long-term reference.
## Exiting Test Mode
Click **Exit** on the test-mode pill at the bottom-left of your site. Test mode stays active while you browse — it does not reset on page reloads, and the session refreshes as long as you keep browsing. It ends when you exit, or after about 12 hours of inactivity.
---
## Publisher-Managed Access API
Source: https://docs.ezoic.com/docs/subscriptions/publisher-managed-access/
Publisher-managed access lets your site decide exactly what subscribers can see. Ezoic Subscriptions handles checkout, payment infrastructure, subscriber sessions, and access verification. Your code checks a product handle and shows the right content.
## Product Handles
A product handle is the stable identifier your site checks with `hasAccess(...)` and sells with `showPaywall({ product })`. You author it on the product in your Ezoic dashboard. See [Product Handles](/docs/subscriptions/products/#product-handles) for the naming rules and examples.
## Basic Access Check
Every integration follows the same shape: check a product handle, then deliver the paid benefit when the decision is `allowed`. The most common benefit is an ad-free experience.
```html
```
Gating content works the same way — reveal a subscriber-only element instead of removing ads. See [Gate Premium Content](/docs/subscriptions/site-integration/#gate-premium-content) for that variant.
`hasAccess(...)` checks whether the current visitor has an active entitlement for the given product. `showPaywall({ product })` opens Ezoic's pre-built paywall and checkout experience for that product's prices. The same product handle is what you check and what you sell. For the ad-removal specifics (`ezsubscriptions.disableAds()` / `allowAds()`), see [Onsite Script Integration](/docs/subscriptions/site-integration/#removing-ads-for-subscribers).
## Access Decisions
`hasAccess(...)` returns an access decision:
- `allowed`: The visitor has active access.
- `login_required`: The visitor is not signed in to Ezoic Subscriptions on this site.
- `denied`: The visitor is signed in but does not have access.
- `expired`: The visitor had access, but it is no longer active.
- `revoked`: Access was removed.
- `unknown_product`: The product handle is not recognized for this site — usually a typo or an inactive product.
For most integrations, show subscriber-only content only when the decision is `allowed`. Treat every other decision as no current access, then call `showPaywall({ product: "your-product-handle" })` or show your own message before opening checkout.
A non-`allowed` decision is never a reason to sign the visitor out. `denied` in particular means the visitor is signed in and ready to buy — calling [`logout()`](/docs/subscriptions/api-reference/#logout) there destroys the session their checkout depends on.
## Anonymous Visitors
Anonymous visitors do not require a network request for access checks:
- `hasAccess(...)` returns `login_required`.
- `getProducts()` returns an empty list.
- `getPurchases()` returns an empty list.
This keeps pages responsive — there is no network round trip for a visitor who has no session to check yet. When an anonymous visitor decides to subscribe, `showPaywall(...)` handles identity and checkout. With [expedited checkout](/docs/subscriptions/visitor-authentication/#expedited-checkout) on (the default), a guest pays on a single screen with no separate sign-in step.
## Checking Multiple Features
Use `getProducts()` when your site has several subscriber-only features:
```javascript
const products = await ezsubscriptions.getProducts();
if (products.includes("pro")) {
enableProTools();
}
if (products.includes("premium")) {
showPremiumNavigation();
}
```
## Listing a Visitor's Purchases
`getProducts()` lists the whole-product access a visitor holds, but not individual item purchases. To build a "your purchases", downloads, or library page that includes per-item purchases, use `getPurchases()`:
```javascript
const purchases = await ezsubscriptions.getPurchases();
for (const purchase of purchases) {
if (purchase.status !== "active") continue;
// purchase.productKey, purchase.item, purchase.expiresAt
}
```
Each entry is `{ productKey?, item?, status, expiresAt? }`. `item` is set for a per-item purchase; `expiresAt` is set only for time-limited access. Anonymous visitors return an empty list.
## One-Time Purchases
For per-item purchases — unlocking a single article, download, or other one-off item rather than granting a recurring product — check `hasPurchased({ item })` instead of `hasAccess`:
```javascript
const access = await ezsubscriptions.hasPurchased({
item: "article-12345",
});
if (access.decision === "allowed") {
document.querySelector("[data-premium-content]").hidden = false;
}
```
Sell the item with `ezsubscriptions.openCheckout({ price: "article-unlock", item: "article-12345" })`, or let Ezoic's paywall sell it with `ezsubscriptions.showPaywall({ product: "premium", item: "article-12345" })`. See [Products, Prices, and Paid Access](/docs/subscriptions/products/) for how one-time prices and items work.
### Subscribe or Buy This Article
To let a subscription unlock everything *or* a one-time purchase unlock a single article, check both and open a paywall that offers the article price alongside the subscription:
```javascript
const item = "article-12345";
const [sub, bought] = await Promise.all([
ezsubscriptions.hasAccess("premium"),
ezsubscriptions.hasPurchased({ item }),
]);
if (sub.decision === "allowed" || bought.decision === "allowed") {
document.querySelector("[data-premium-content]").hidden = false;
} else {
ezsubscriptions.showPaywall({ product: "premium", item });
}
```
### Selling the Current Article Automatically
If you'd rather not assign each article an `item` key, configure the one-time price to unlock *the current article automatically* (see [Products, Prices, and Paid Access](/docs/subscriptions/products/)). Then `showPaywall({ product })` sells access to whatever page it runs on, and you reveal an already-bought article with `hasPurchased({ page: true })`:
```javascript
const access = await ezsubscriptions.hasPurchased({ page: true });
if (access.decision === "allowed") {
document.querySelector("[data-premium-content]").hidden = false;
}
```
`{ page: true }` checks the current page using the same page key Ezoic stamped at checkout, so you never compute or pass the item yourself.
## Granting Access Manually
You can comp a reader access to a product without a payment — for support make-goods, sponsors, or staff. In the Ezoic dashboard, open **Subscriptions → Access**, click **+ Grant access**, and enter:
- **Email** — the reader's email address. It identifies (or creates) the subscriber.
- **Product** — the product to grant.
- **Access days (optional)** — leave blank for access that does not expire, or enter a number of days for a time-limited grant.
A manual grant charges nothing and creates no subscription — the reader simply passes `hasAccess(...)` for that product until the grant expires or is revoked. If the reader already holds active access, the grant is a no-op.
## Reacting to Access Changes
When the visitor logs in, logs out, or completes checkout, subscribe to `access:change` so gated UI re-renders without a page reload:
```javascript
ezsubscriptions.on("access:change", async function () {
const access = await ezsubscriptions.hasAccess("premium");
document.querySelector("[data-premium-content]").hidden = access.decision !== "allowed";
});
```
This event is the only re-check trigger you need — do not poll `hasAccess(...)` on a `setInterval`. Login, logout, account switches, and completed checkouts all fire `access:change`, so a timer only adds requests without catching anything the event misses.
## Implementation Notes
- Keep product and price handles stable after launch.
- Show teaser content to everyone, then reveal or fetch the protected body only after access is allowed.
For every method's options and callbacks, see [JavaScript API Reference](/docs/subscriptions/api-reference/).
---
## Visitor Authentication
Source: https://docs.ezoic.com/docs/subscriptions/visitor-authentication/
Visitor authentication determines how a visitor proves who they are before Ezoic Subscriptions grants access or starts checkout. There are two modes:
- **Ezoic visitor accounts** — the default. Ezoic provides sign-in, account creation, and recovery. No integration code.
- **Bring your own login** — connect your existing login system to the widget through an `AuthAdapter`.
You select the mode in your Ezoic dashboard. The default is Ezoic visitor accounts.
## How Sign-In Works
In both modes the widget resolves a visitor identity (an email address) and uses it for access checks (`hasAccess`) and checkout. What differs is who handles sign-in and account creation:
- **Ezoic visitor accounts** — Ezoic's built-in login and create-account screens, shown in the widget.
- **Bring your own login** — your own login and sign-up pages, reached through the callbacks you register during widget setup.
At checkout, a signed-in visitor goes straight to payment. What an anonymous visitor sees depends on [expedited checkout](#expedited-checkout): with it on (the default), a guest goes straight to a single payment screen and enters their email there, with a **Have an account? Log in** link for returning subscribers; with it off, the visitor sees an identity gate offering **Log in**, **Create account**, and **Continue as a guest** when guest checkout is enabled. Where **Log in** and **Create account** send the visitor depends on the active mode: in Ezoic visitor accounts mode they open Ezoic's built-in login and create-account screens inside the widget; in bring-your-own-login mode they call the callbacks you supply, sending the visitor to your own login and sign-up pages.
Returning visitors sign in through the active mode's provider. On Ezoic visitor accounts, sign-in is passwordless: the visitor enters their email and Ezoic emails them a one-time sign-in link (single-use, expiring after 15 minutes), with a password sign-in available to visitors who set one. The emailed link is also how a guest buyer gets back in on a new device: access is tied to the email, so the link restores it with no account required. On bring your own login, your own system handles sign-in — Ezoic issues no sign-in link in that mode.
To let returning subscribers sign in from your own navigation — without first reaching a paywall — add a login entry point with [`ezsubscriptions.login()` or the `data-ezoic-login` attribute](/docs/subscriptions/site-integration/#add-a-log-in-link). It opens the same provider as checkout for your mode, and on Ezoic visitor accounts the page unlocks in place once they sign in.
Subscribers manage their billing in the subscriber portal at `https://subscriber.ezoic.com`.
## Signing Out
Signing out ends the visitor's Subscriptions access session, and gated content re-locks in place. How you trigger it depends on your mode.
If you granted the subscriber an ad-free experience with `ezsubscriptions.disableAds()`, signing out restores ads too. Because ad suppression is applied on Ezoic's servers, a page refresh may be needed before ads begin showing again.
### Ezoic visitor accounts
Call `ezsubscriptions.logout()`, or add the [`data-ezoic-logout` attribute](/docs/subscriptions/site-integration/#add-a-log-out-link) to a "Log out" link. Both sign the visitor out of their Ezoic visitor account, clear the access session, and re-lock gated content in place, with no extra code:
```html
```
or from your own code:
```javascript
await ezsubscriptions.logout();
```
### Bring your own login
Your login system owns the visitor's session, so sign them out of it first, then tell the widget to drop the access session:
```javascript
async function onLogout() {
await myAuth.signOut(); // end the visitor's session in your own system
await ezsubscriptions.logout(); // drop the Subscriptions access session
}
```
`ezsubscriptions.logout()` clears the access session, discards any parked checkout, and fires `access:change`. Calling [`ezsubscriptions.authChanged()`](#signal-authentication-changes) after your own sign-out also clears the access session and fires `access:change`, but it leaves ad state to you — so prefer `logout()` when the subscriber had ads removed. The widget cannot sign a visitor out of your own login system — that step is always yours.
### The subscriber portal signs out separately
The subscriber portal at `https://subscriber.ezoic.com` is a separate, Ezoic-hosted area with its own sign-in and sign-out. Signing out of your site does not sign a visitor out of the portal, and vice versa.
## Ezoic Visitor Accounts
This is the default mode and requires **no integration code**. Ezoic provides login, account creation, guest checkout (when you enable it), and passwordless recovery directly in the widget. You do not build or maintain any authentication on your site. If a visitor tries to create an account with an email that already has one, the widget links them to sign in with that email prefilled.
**Continue with Google** appears automatically on the login and account screens when it's available for the visitor, letting them sign in with their Google account. There's nothing to configure, and the option hides itself when it isn't available.
The widget places the Ezoic Accounts integration on the page for you. If your site already runs the Ezoic Accounts integration (`window.ezAuth`), leave it in place: the widget detects and reuses an existing instance and otherwise loads it itself. Either way, there is nothing to add or remove.
## Bring Your Own Login
Use this mode when your site already has its own login system. You connect it to the widget by registering an `AuthAdapter` — a small object that lets the widget read the signed-in visitor's email and send visitors to your login and sign-up pages.
### The `AuthAdapter` Contract
For the related SDK methods and their full signatures — `initialize`, `authChanged`, and the rest of the methods — see the [JavaScript API Reference](/docs/subscriptions/api-reference/).
```typescript
interface AuthAdapter {
getUserEmail?: () => string | null | Promise;
getIdentityToken?: () => string | null | Promise;
goToLogin: () => void | Promise;
goToCreateAccount: () => void | Promise;
logout?: () => void | Promise;
}
```
`goToLogin` and `goToCreateAccount` are always required. For identity, provide either `getIdentityToken()` (preferred — keeps the email off the page; see [Keeping the email off the page](#getidentitytoken)) or `getUserEmail()`. Provide both and the token is used when available, with `getUserEmail()` as the fallback.
`logout()` is optional. Provide it to show a **Not you?** switch next to the visitor's identity at checkout, so they can sign out of your auth and switch accounts. Without it the switch is hidden — the widget cannot sign visitors out of your system itself.
#### `getUserEmail()`
Returns the signed-in visitor's email address, or `null` when no one is signed in. May be synchronous or return a promise.
Returns:
```typescript
string | null | Promise
```
The widget calls this to detect a logged-in visitor and skip the checkout identity gate. Return a syntactically valid email when the visitor is authenticated in your system; return `null` (or an empty/invalid value) when the visitor is anonymous.
**This returns the email to your page's JavaScript.** On pages running third-party scripts, that exposes the address to anything else executing there. If that matters for your site, use [`getIdentityToken()`](#getidentitytoken) instead — it identifies the same visitor without putting the email on the page.
#### `getIdentityToken()`
**Optional.** Use this when you don't want the visitor's email exposed to your page's JavaScript.
`getUserEmail()` hands the email to the page as plain text, so any script running on the page can read it. `getIdentityToken()` avoids that: instead of the email, the widget gets a short-lived **signed token** that stands in for it. The email is encrypted inside the token, so nothing on the page can read it.
When `getIdentityToken()` returns a token, the widget uses it for sign-in and checkout **instead of** an email. When it returns `null` (visitor anonymous, or the token fetch failed), the widget falls back to `getUserEmail()` if you provided it — otherwise the visitor is treated as anonymous and sees the email step.
Returns:
```typescript
string | null | Promise
```
You don't build or sign the token yourself — Ezoic Subscriptions does. Your server calls the REST API's [`POST /subscriptions/v1/identity-token`](/docs/subscriptions/rest-api/#get-identity-token) with the signed-in reader's email, and the API returns a signed token. `getIdentityToken()` then retrieves that token from a small endpoint on your own server:
```javascript
auth: {
getIdentityToken: async function () {
// Your server-side endpoint calls POST /subscriptions/v1/identity-token
// with the signed-in reader's email and returns { token }.
const res = await fetch("/api/subscriptions-identity-token", { credentials: "same-origin" });
if (!res.ok) return null;
const { token } = await res.json();
return token ?? null;
},
getUserEmail: function () {
// Optional — used when getIdentityToken() returns null.
return myAuth.getCurrentUser()?.email ?? null;
},
goToLogin: function () { /* ... */ },
goToCreateAccount: function () { /* ... */ },
},
```
**Don't cache the token.** It expires after one hour, and the widget calls `getIdentityToken()` whenever it needs one. Retrieve a new token on every call — don't embed one in the page. A stale token is rejected and the visitor falls back to the email step.
If your pages don't run third-party scripts, skip this — `getUserEmail()` alone is fine.
#### `goToLogin()`
Sends the visitor to your login experience. The adapter decides how — a navigation, a modal, or a provider SDK call.
Returns:
```typescript
void | Promise
```
Powers the identity gate's **Log in** action, the paywall's "Already subscribed? Log in" link, and the **Have an account? Log in** link on the expedited checkout screen.
#### `goToCreateAccount()`
Sends the visitor to your account creation experience. The adapter decides how, exactly like `goToLogin`.
Returns:
```typescript
void | Promise
```
Powers the gate's **Create account** action. It can point at the same destination as `goToLogin` when your login page also registers new accounts.
### Register the Adapter
Register the adapter once with `ezsubscriptions.initialize({ auth })` inside the `cmd` queue, so it is set as soon as the script is ready. On a single-page app, run it once in your root layout or shell — not per route:
```html
```
Registration is idempotent: calling `initialize` again with a valid adapter replaces the previous one.
`initialize` only registers the adapter — it does not read your login state or start a session. That happens when you call [`authChanged()`](#signal-authentication-changes). Until then the widget treats the visitor as anonymous, even if they are signed in to your site.
### Signal Authentication Changes
Call `ezsubscriptions.authChanged()` when your auth system knows the visitor's login state — after a login, a logout, or after a session restore finishes. It reads the adapter, refreshes the access session, and resumes any checkout they left to log in:
```javascript
async function onLogin() {
await myAuth.signIn(/* ... */);
await ezsubscriptions.authChanged();
}
```
The widget does not watch your login system, and `initialize` does not read it. Page load does not either: the signed-in visitor is only applied when you call `authChanged()`.
If login happened on another page, send them back to the URL they started from first (the examples above pass a `return` parameter). Then call `authChanged()` on that page once `getUserEmail()` or `getIdentityToken()` would return the signed-in visitor:
- Already true on first paint (session cookie, server-rendered page): call it after `initialize`, for example in the same `cmd` callback.
- Not true yet (the page loads anonymous, then a request fills in the session): wait for that request. Calling `authChanged()` on the checkout page while the adapter still returns empty is treated as logged out and discards the parked checkout.
A parked checkout only resumes during `authChanged()`, and only on the page checkout started on.
**Return visitors to where they left off.** When `goToLogin()` or `goToCreateAccount()` sends a visitor to your pages, bring them back to the URL they started from once they sign in — the examples above pass a `return` parameter for exactly this. Call `authChanged()` on that page after your auth system has the signed-in visitor. A resumed checkout only re-opens on the page it started on, so a visitor who lands anywhere else has to find their way back themselves.
For signing a visitor out, see [Signing Out](#signing-out). For re-checking access on client-side route changes, see [Single-Page Apps and Frameworks](/docs/subscriptions/site-integration/#single-page-apps-and-frameworks-react-vue-nextjs).
### How Checkout Uses the Adapter
- A signed-in visitor (`getIdentityToken()` returns a token, or `getUserEmail()` returns an email) skips the identity gate and goes straight to payment.
- An anonymous visitor sees **Log in**, **Create account**, and **Continue as guest** when guest checkout is enabled. With [expedited checkout](#expedited-checkout) on (the default), there is no identity gate: a guest instead goes straight to the single payment screen and enters their email there, and **Log in** is a link on that screen. `goToLogin()` and `goToCreateAccount()` send the visitor to your login; when they are back on the checkout page and you call `authChanged()` after the adapter returns the signed-in visitor, the widget resumes the same checkout.
- The token or email from the adapter is the visitor's identity for access checks and checkout. The token is used when it returns one; otherwise the email.
**Register the adapter before going live.** On a domain set to bring your own login, the widget cannot resolve a visitor's identity until `ezsubscriptions.initialize({ auth })` has registered a complete adapter (`goToLogin`, `goToCreateAccount`, and an identity method). Until then, checkout logs an error and does not complete. Configure and test your adapter before switching the domain to this mode.
**Looking for a server-to-server integration?** See the [Server-to-Server REST API](/docs/subscriptions/rest-api/) to verify access and read subscriptions from your own backend.
## Guest Checkout
Guest checkout lets a visitor buy with just an email address instead of signing in or creating an account. With [expedited checkout](#expedited-checkout) on (the default), the guest enters their email directly on the single payment screen and pays — one tap with Apple Pay or Google Pay where available. With expedited checkout off, the visitor chooses **Continue as guest** at the identity gate, enters an email, and pays. Either way, access attaches to that email, so the visitor can sign in or recover access later using the same address.
If a guest later registers on your site (or already has an account) using that same email, their account — on your own login system or Ezoic visitor accounts, whichever mode you use — is linked to the purchase automatically, with no extra work for you or the visitor.
To make that next return a quick sign-in instead of another email link, the confirmation screen after a guest purchase offers an optional, skippable step to create an account. On Ezoic visitor accounts the visitor creates an Ezoic account right in the widget; on bring your own login it sends them to your own sign-up page. Either way, an account created with the same email keeps the purchase automatically — so encourage guests to reuse their checkout email.
Guest checkout is **on by default**. It is a per-domain setting in your Ezoic dashboard: in the Subscriptions area under **Settings → Visitor sign-in**, as **Allow guest checkout**. It applies to both authentication modes. Turn it off there to require every buyer to sign in or create an account before paying.
## Expedited Checkout
Expedited checkout removes the separate identity step for guest buyers. Instead of choosing **Continue as guest** and then moving on to a payment step, an anonymous visitor lands on a single payment screen: Apple Pay and Google Pay buttons where the visitor's device and browser support them, an email field, and the card form. Where a wallet is available, a buyer can check out in one tap — no email or card details to type. A **Have an account? Log in** link on the screen takes returning subscribers to sign-in instead.
Expedited checkout is **on by default** and recommended. It is a per-domain setting in your Ezoic dashboard, under **Settings → Visitor sign-in** as **Expedited checkout**, and it requires [guest checkout](#guest-checkout) — turning guest checkout off turns expedited off with it. It applies to both authentication modes. Turn it off to use the multi-step checkout, where an anonymous visitor sees the identity gate (**Log in** / **Create account** / **Continue as guest**) before the payment step.
## Next Steps
- Install the widget with [Onsite Script Integration](/docs/subscriptions/site-integration/).
- Gate content with the [Publisher-Managed Access API](/docs/subscriptions/publisher-managed-access/).
- Review method options in the [JavaScript API Reference](/docs/subscriptions/api-reference/).
---
## Paywall Appearance and Styling
Source: https://docs.ezoic.com/docs/subscriptions/paywall-appearance/
The paywall and checkout are pre-built, so they work with no styling on your part. When you want more control, there are three ways to get it, from least to most effort:
1. **Appearance settings** — pick a template, color mode, and accent per product in the dashboard. No code.
2. **CSS variables** — override the widget's colors, fonts, and spacing from your own stylesheet for exact brand matching.
3. **Build your own pricing UI** — skip Ezoic's paywall screen and render your own plan cards, handing off to `openCheckout({ price })` for payment only. See [Build Your Own Pricing UI](/docs/subscriptions/site-integration/#build-your-own-pricing-ui).
Most sites only need Appearance settings. Add CSS variables when the built-in template and accent aren't close enough. Reach for your own pricing UI when you want full control of how plans are presented — see the caveat at the end of this page if you combine it with Appearance settings.
## Appearance Settings
Each product has Appearance settings you choose in the Ezoic dashboard, on that product's page. They apply everywhere that product's paywall and checkout appear:
- **Template** — `Classic` (compact, neutral) or `Modern` (rounder corners, a colored top bar, and carded price rows).
- **Color mode** — `Light`, `Dark`, or `Auto`. `Auto` follows the visitor's system preference.
- **Accent color** — an optional brand color used for the primary button and focus ring. Ezoic derives the matching hover and text-on-accent colors for you.
For most sites, picking a template, mode, and accent is all you need.
## Language
The paywall and checkout support over 50 languages. Each visitor sees them in their own browser's language — no setup required. To set one language for every visitor, use the **Paywall and checkout language** setting in your Subscriptions settings.
Text you customize in your Ezoic dashboard — product names, price labels, the paywall title and subtitle, button labels — always appears exactly as written. Both left-to-right and right-to-left languages are supported.
## Paywall and Checkout Text
Two levels of copy overrides, both optional — a blank field keeps the widget's default wording:
- **Per product** — the product builder lets you replace the paywall's eyebrow, title, and subtitle for that product.
- **Per domain** — under **Settings → Language** you can reword the standard text shown on every product's paywall and checkout: the **Trust line** (the secure-checkout note), the **Cancel-anytime link**, and the **Manage-subscription link**.
## Custom Styling With CSS Variables
The widget renders inside a [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM), which isolates it from your page's stylesheet. That means you can't target its internal elements with your own CSS selectors — but it exposes a set of **CSS custom properties** (variables) on its host element, `#ezoic-sm`, and those are the supported way to restyle it.
Set the variable you want on `#ezoic-sm` from your site's CSS:
```css
#ezoic-sm {
--sm-accent: #5fa624;
--sm-accent-hover: #4e8a1e;
--sm-radius: 12px;
--sm-font-family: "Source Sans 3", system-ui, sans-serif;
}
```
A value you set on `#ezoic-sm` wins over the product's template, color mode, and accent. It applies in every mode, so if you only want to change dark mode, scope the rule: `#ezoic-sm[data-sm-mode="dark"] { --sm-surface: #101828; }`.
### A Fuller Example
```css
#ezoic-sm {
/* Brand accent — primary button and focus ring */
--sm-accent: #5fa624;
--sm-accent-hover: #4e8a1e;
--sm-on-accent: #ffffff;
--sm-focus: rgba(95, 166, 36, 0.14);
/* Type and surfaces */
--sm-font-family: "Source Sans 3", system-ui, sans-serif;
--sm-text: #0f172a;
--sm-surface: #ffffff;
--sm-bg: #f8fafc;
--sm-border: #e2e8f0;
/* Shape and elevation */
--sm-radius: 12px;
--sm-radius-lg: 16px;
--sm-shadow: 0 24px 64px rgba(15, 23, 42, 0.28);
}
```
### Supported Variables
| Variable | Controls |
|---|---|
| `--sm-font-family` | UI typeface |
| `--sm-text` | Primary text |
| `--sm-text-secondary` | Secondary text |
| `--sm-text-muted` | Muted / hint text |
| `--sm-surface` | Card and panel fill |
| `--sm-surface-hover` | Hover fill for quiet controls |
| `--sm-bg` | Behind-panel background |
| `--sm-border` | Dividers and control borders |
| `--sm-border-strong` | Stronger borders |
| `--sm-accent` | Primary button / accent |
| `--sm-accent-hover` | Accent hover state |
| `--sm-on-accent` | Text/icon on the accent |
| `--sm-focus` | Focus ring |
| `--sm-danger` | Error text |
| `--sm-danger-bg` | Error background |
| `--sm-success-fg` | Success text |
| `--sm-success-bg` | Success background |
| `--sm-radius-sm` | Small corner radius |
| `--sm-radius` | Base corner radius |
| `--sm-radius-lg` | Large corner radius |
| `--sm-overlay-bg` | Paywall backdrop scrim |
| `--sm-shadow` | Panel elevation (Classic) |
| `--sm-shadow-lg` | Panel elevation (Modern) |
The card entry fields (powered by Stripe) pick up these same variables automatically, so your checkout form stays consistent with the rest of the paywall — with one exception: `--sm-font-family` does not reach Stripe's fields, which keep their own system font stack.
## Build Your Own Pricing UI
For full control of how plans are presented, skip `showPaywall()` and render your own plan cards, then call `openCheckout({ price })` to hand off payment. See [Build Your Own Pricing UI](/docs/subscriptions/site-integration/#build-your-own-pricing-ui) for the full pattern.
`openCheckout({ price })` inherits its Appearance (template, color mode, accent) from the price's parent product, so it matches your paywall by default. CSS variables still apply on top — set them on `#ezoic-sm` as shown above.
## What You Can't Customize
The [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM) boundary means the CSS variables above are the full supported surface. You can't:
- Style the widget's internal elements with your own CSS selectors.
- Inject arbitrary CSS or scripts into the widget.
- Change its layout, or hide, add, or reorder elements.
If your brand needs something the variables don't cover, [let us know through Ezoic support](https://support.ezoic.com/).
---
## SEO and Paywalling Best Practices
Source: https://docs.ezoic.com/docs/subscriptions/seo-paywalling-best-practices/
When you gate article content, search engines need to understand that the content is intentionally paywalled. Ezoic Subscriptions gives you the access and checkout layer, but your page markup should still follow Google's paywalled-content guidance.
Use Google's official documentation as the source of truth: [Paywalled content structured data](https://developers.google.com/search/docs/appearance/structured-data/paywalled-content).
**This page applies only when you gate readable article or page content.** If your paid product removes ads, unlocks a tool or feature, or sells a download, you do not need paywalled-content structured data — you can skip this page. Add this markup only for article or page content you hide from readers behind the paywall.
## Recommended Page Structure
Keep teaser content visible to everyone, then wrap subscriber-only content in a real CSS class such as `.paywalled-content`.
```html
```
In this example:
- `.paywalled-content` is the CSS selector your structured data points at.
- `data-premium-content` is the hook your own integration code reveals after `hasAccess(...)` returns `allowed` (see [Onsite Script Integration](/docs/subscriptions/site-integration/)). The widget verifies access and renders the paywall; it never reveals your gated content for you.
- `hidden` starts the subscriber-only section hidden for a soft gate.
If you use a hybrid or server-side approach, keep the same selector on the gated container and fetch or render the protected body only after access is allowed.
## Add Paywalled-Content Structured Data
Add `isAccessibleForFree: false` and `hasPart` to your `Article` or `NewsArticle` structured data. Replace the example values with the real values for your article.
```html
```
If your page already has `Article` or `NewsArticle` JSON-LD, merge the paywall fields into the existing object instead of adding a conflicting duplicate article record.
## `cssSelector` Requirements
The `cssSelector` value must point to the gated content container on the page.
Use:
```json
"cssSelector": ".paywalled-content"
```
when your HTML contains:
```html
```
Do not point `cssSelector` at a missing class. Avoid nested paywalled sections unless your structured data accurately describes each gated section.
**SEO safety checks:** Do not `noindex` or robots-block gated pages that should rank in Google. Do not serve different article HTML to Googlebot based only on the user agent. Do not cloak full content to crawlers while showing unrelated or lower-quality content to users. For hard server-side walls, allow crawler access only after verifying crawler identity; do not trust a `Googlebot` user-agent string by itself.
## Social Previews
Social previews should not depend on subscriber state. Use teaser-level metadata that is safe for every visitor:
- `og:title`
- `og:description`
- `og:image`
- `twitter:card`
Do not require authentication before social crawlers can read these tags.
## Caching
How caching affects gated pages depends on where you reveal content:
- **Client-side gating (the default).** When you ship the same teaser HTML to everyone and reveal subscriber-only content in the browser after `hasAccess(...)` allows it, the page HTML is identical for every visitor — so caching it on a CDN is safe. Access is resolved in the browser after the widget loads, not baked into the cached HTML.
- **Server-side or hybrid gating.** If your server injects the protected body based on the visitor's session, be careful with caches:
- Do not cache the subscriber version and serve it to anonymous visitors.
- Do not let an anonymous version overwrite the subscriber version for signed-in visitors.
- Keep any response that returns the protected body private to the visitor session (for example, `Cache-Control: private`).
## Validate Before Launch
Before you launch a gated article:
1. Test a live URL with Google's [Rich Results Test](https://search.google.com/test/rich-results).
2. Confirm the rendered HTML includes the JSON-LD.
3. Confirm `cssSelector` points to the gated content container.
4. Use URL Inspection in Google Search Console after the page is live.
Validation does not guarantee ranking or rich-result display, but it helps catch markup mistakes before they affect search visibility.
---
## Donation Integration
Source: https://docs.ezoic.com/docs/subscriptions/donations/
Donations let readers support your work without buying a subscription. A donation does not unlock pages, grant a product, or change what content a visitor can see.
## Open A Donation Dialog
There's no setup call. Add a trigger and the widget loads your donation settings the first time it's used. You can open the donation checkout two ways:
- a **declarative button** — any element with `data-ezoic-donate`, no JavaScript; or
- **`openDonation()`** from your own click handler.
`openDonation()` opens the donation checkout and `closeDonation()` closes it. Both work as soon as the widget script has loaded — no other call is required.
## Add A Support Button
The simplest scripted integration is a "Support our work" button that opens the donation picker, where the reader chooses how much to give. Call `openDonation()` with no preset amount.
```html
```
## Preset A Donation Amount
If you want a button to preselect an amount — for example a "Give $25" tier — pass `amountCents`. The reader can still change it in the picker.
```html
```
`amountCents` is a preset amount in cents. For example:
- `500` means `$5.00`.
- `2500` means `$25.00`.
- `10000` means `$100.00`.
Currency is not passed to `openDonation`. The picker presents the currency the visitor sees — see [Currencies](#currencies) — and `amountCents` is interpreted in that currency's smallest unit.
If `amountCents` is missing, invalid, or below the minimum configured in the dashboard, the widget falls back to the normal donation picker. The backend still enforces the minimum amount.
## Currencies
By default the donation picker presents your donation's currency (USD unless you change it), and checkout automatically presents a local-currency equivalent to international visitors. The converted amounts follow the exchange rate, so they are rarely round numbers, and the buyer pays a small conversion fee on top; visitors can always switch back to your listed currency at checkout. 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 price points per market with **additional currencies** in the donation settings — each currency gets its own minimum and suggested amounts (for example ₹199 / ₹499 / ₹999 for visitors in India instead of a converted $5 / $10 / $25). A visitor whose region matches one of your currencies sees and pays those amounts in that currency — no conversion, no conversion fee. Everyone else gets the default currency with automatic local-currency presentment at checkout.
## Declarative Button Option
You can also mark a button with attributes and let the widget handle the click:
```html
```
The `data-ezoic-amount-cents` value follows the same cents format as `openDonation({ amountCents })`. Omit it to open the picker with no preset amount. Add `data-ezoic-product-id` with a donation product ID to target a specific donation — the declarative equivalent of `openDonation({ productId })`; rarely needed, since a site has one active donation.
## React To A Completed Donation
Pass `onSuccess`, `onCancel`, or `onError` to `openDonation()` to react to a donation — for example to reveal a thank-you message:
```html
Thanks for your support!
```
Callbacks only work through `openDonation()`. A declarative `[data-ezoic-donate]` button can't carry `onSuccess`, `onCancel`, or `onError` — to run code after a donation, open it from your own click handler with `openDonation({ onSuccess })` as shown above.
## Dashboard Prerequisite
Before the onsite donation API can open checkout, donations must be enabled for the site:
1. Open the Ezoic Subscriptions area in the Ezoic dashboard.
2. Open the **Products** page.
3. Turn on **Enable donations**.
4. Set the donation label, such as `Support our work`.
5. Set the minimum donation amount and suggested amounts.
6. Optionally add additional currencies with their own minimums and suggested amounts (see [Currencies](#currencies)).
7. Save settings.
Donations are pay-what-you-want above the minimum amount you set. A site has one active donation at a time. To change the label or minimum amount later, edit the donation on the Products page and save — there's no need to turn donations off first.
## Closing The Dialog
Donation checkout is dismissible. If your UI needs to close it programmatically, call:
```javascript
ezsubscriptions.closeDonation();
```
This only affects the donation dialog. It does not close or bypass the paid-access paywall for subscriber content.
For full `openDonation()`, `closeDonation()`, and callback details, see [JavaScript API Reference](/docs/subscriptions/api-reference/).
---
## Subscriber Experience
Source: https://docs.ezoic.com/docs/subscriptions/subscriber-experience/
Ezoic Subscriptions covers checkout, subscriber login, access sessions, subscription management, receipts, invoices, and payment methods. To walk this entire flow yourself before launch, see [Testing with Test Mode](/docs/subscriptions/testing-sandbox/).
## Checkout
When a visitor buys access, they move through checkout. The exact steps depend on how you opened it and whether the visitor is already signed in:
1. **Price.** From `showPaywall({ product })` the visitor picks from the product's active prices; from `openCheckout({ price })` the price is already chosen, so this step is skipped.
2. **Payment.** A visitor who is already signed in goes straight to payment. So does an anonymous visitor when [expedited checkout](/docs/subscriptions/visitor-authentication/#expedited-checkout) is on (the default): they land on a single payment screen with an email field, Apple Pay and Google Pay buttons where available, and the card form, plus a **Have an account? Log in** link for returning subscribers. With expedited checkout off, an anonymous visitor first sees an identity step offering **Log in**, **Create account**, and **Continue as guest** when guest checkout is enabled (which only takes an email), then pays on a separate payment step. Either way, payment runs through Stripe-powered checkout, and when you have an active [promo code](/docs/subscriptions/promo-codes/) the checkout shows a **Have a promo code?** link that opens a code entry field.
3. **Verification.** Ezoic Subscriptions verifies the payment and establishes access.
4. **Access.** The visitor receives access for the purchased product, and your site can reveal subscriber-only content or remove ads once access is confirmed.
The verification step is important. Do not deliver the paid benefit just because the browser-side payment step appears complete — wait for Ezoic Subscriptions access verification. An [`access:change`](/docs/subscriptions/publisher-managed-access/#reacting-to-access-changes) listener with a fresh `hasAccess(...)` check fires only once access is confirmed.
When a visitor buys as a guest (with only an email), the confirmation screen offers an optional, skippable step to create an account so their next return is a quick sign-in rather than another email link. See [Guest Checkout](/docs/subscriptions/visitor-authentication/#guest-checkout).
## Free Trials
When a price has a [free trial](/docs/subscriptions/products/#free-trials), the subscriber enters a payment method at checkout but is not charged until the trial ends. They get full access right away, and the subscription converts to paid automatically when the trial ends. A subscriber who cancels from the [portal](#subscriber-portal) before the trial ends is not charged.
## Returning Subscribers
You can give subscribers a direct way back in by adding a "Log in" link to your own navigation — wire it to `ezsubscriptions.login()` or the `data-ezoic-login` attribute (see [Add a Log In Link](/docs/subscriptions/site-integration/#add-a-log-in-link)). On Ezoic visitor accounts, signing in there unlocks gated content in place, without a paywall.
If a returning subscriber needs to regain access — for example on a new device — Ezoic Subscriptions can also email them a one-time access link as a fallback, so a paying subscriber is never left locked out.
The link is single-use and expires after a short time (currently 15 minutes). It returns the subscriber to your site and lets the widget restore their access session, after which `ezsubscriptions.hasAccess(...)` reflects their access.
## Subscriber Portal
Subscribers can manage their account at:
```
https://subscriber.ezoic.com
```
The portal is global across publisher sites that use Ezoic Subscriptions. A subscriber can sign in once and view subscription activity by site.
Subscribers can:
- View active and past subscriptions.
- Cancel a subscription at the end of the current billing period.
- Reactivate a subscription that is pending cancellation.
- View one-time purchases.
- View invoices and receipts.
- Add or update payment methods.
- Remove saved cards when they are not required by active subscriptions.
To add a "Manage subscription" link to your own site, wire it to `ezsubscriptions.openAccountPortal()`, which opens the portal in a new tab. 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. See [JavaScript API Reference](/docs/subscriptions/api-reference/#openaccountportal).
## Cancellations
Subscriber cancellations are scheduled for the end of the current billing period. That means a subscriber who cancels usually keeps access until the paid period ends.
If a subscriber changes their mind before the period ends, they can reactivate the subscription from the subscriber portal.
## Payment Methods
Subscribers can update cards in the portal. A card may not be removable while it is still used by an active subscription. In that case, the subscriber should set a different card for the subscription or cancel the subscription before removing the card.
## What Publishers Should Tell Subscribers
If subscribers ask where to manage billing, direct them to `https://subscriber.ezoic.com`.
If subscribers ask how to regain access on a new device, tell them to use the "Log in" link on your site or the one-time access link from the paywall — not the subscriber portal. The portal manages billing only; signing in there does not restore access to your gated content.
---
## 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
```
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
```
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
```
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
```
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
```
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
```
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
```
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
```
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
```
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
```
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
```
## Login and Logout
### `login(options)`
```typescript
login(options?: { dismissible?: boolean }): Promise
```
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
```
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
```
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
```
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
```
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
```
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
```
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
```
```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
```
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`).
---
## Server-to-Server REST API
Source: https://docs.ezoic.com/docs/subscriptions/rest-api/
The Subscriptions REST API lets your origin server verify a reader's access and read their subscriptions directly, without relying on the onsite widget. It is the server-side counterpart to the [JavaScript API](/docs/subscriptions/api-reference/): the same access decisions and response shapes, reached over HTTPS from your backend.
For many sites, the onsite [JavaScript API](/docs/subscriptions/api-reference/) is all you need. The REST API is for implementations that make access decisions on their own server rather than in the browser.
## When to Use the REST API
The onsite JavaScript API runs in the reader's browser and is the right tool for most integrations. Reach for the REST API when the decision has to happen on your server:
- Your CMS or application server renders paid content and needs to decide access before sending HTML.
- You expose your own API and want to authorize a request against the reader's subscription.
- A backend job needs to read what a reader currently holds.
Both APIs read the same entitlements, so a reader who has access onsite has access through the REST API too.
## Enable API Access
The REST API is off until you turn it on:
1. In your Ezoic dashboard, open **Subscriptions → Settings**.
2. Open the **REST API** card and turn on API access.
3. Copy your API key from the card.
Your API key is unique to your account — the same key works for any other Ezoic API services you've enabled. You rotate it from your API settings in the Ezoic dashboard.
Turning API access on or off never changes what a reader can buy or see onsite. It only controls whether your server can call these endpoints.
## Base URL
All requests go through the Ezoic API:
```
https://api-gateway.ezoic.com/subscriptions/v1
```
## Authentication
Every request identifies **your site** with your API key and a `domain` query parameter, and identifies **the reader** with a header.
### Site Credentials
| Parameter | Description |
| --- | --- |
| `X-API-Key` header | Your Ezoic API key, from the REST API settings card. |
| `domain` query parameter | The domain you are querying, for example `example.com`. It must belong to your account. |
The API authenticates the key, confirms the domain is yours, and rejects the request otherwise — so a key can only ever read its own domains.
The legacy `?developerKey=YOUR_API_KEY` query parameter is still accepted so existing integrations keep working, but we recommend integrations use the `X-API-Key` header.
### Reader Identity
Each request asks about one reader. Identify that reader with **exactly one** of these headers:
| Header | When to use it |
| --- | --- |
| `X-Ezoic-Reader-Email` | The reader's email address. Use this whenever your server knows it — the common case, including sites that bring their own login. |
| `X-Ezoic-Reader-Token` | The reader's Ezoic Subscriptions session token (the widget session JWT), relayed from the page. Use this on Ezoic-visitor-account sites where your server has no email to send. |
Send exactly one. A request with neither, or with both, is rejected. Reader identity always travels in a header, never in the query string, so email addresses and tokens stay out of URLs and logs.
#### By email
If your server already knows the reader's email — you run your own login, or the reader gave it to you — send it as `X-Ezoic-Reader-Email`. This is the simplest path and works in every authentication mode.
#### By session token (Ezoic-visitor-account sites)
If your site uses **Ezoic visitor accounts**, Ezoic handles sign-in and your server never sees the reader's email. In that case, relay the reader's **session token** from the page instead.
Read the token in the browser with [`getSessionToken()`](/docs/subscriptions/api-reference/#getsessiontoken) and send it to your own backend. Your server then forwards it to the REST API as the `X-Ezoic-Reader-Token` header:
```bash
curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/access?domain=example.com&product=remove-ads" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Ezoic-Reader-Token: THE_RELAYED_TOKEN"
```
`getSessionToken()` returns `null` for a signed-out visitor. The token is scoped to the current domain and expires with the visitor's session, so read it per request rather than storing it. The REST API validates the token against the requested `domain`, so a token from one site can't be replayed against another. See [`getSessionToken()`](/docs/subscriptions/api-reference/#getsessiontoken) in the JavaScript API reference.
## Endpoints
### Check Access
```http
GET /subscriptions/v1/access HTTP/1.1
```
Checks whether the reader holds a **product** or has bought a one-time **item**. It is the server-side mirror of `hasAccess(product)` and `hasPurchased({ item })`.
Query parameters (in addition to `domain`):
| Parameter | Description |
| --- | --- |
| `product` | A [product handle](/docs/subscriptions/publisher-managed-access/#product-handles) to check whole-product access. |
| `item` | A one-time item key to check a per-item purchase. For a price that unlocks *the current article automatically*, send the article's URL or its page path. |
Send at least one of `product` or `item`. Both accept a comma-separated list (for example `product=premium,remove-ads`), and the check is an **OR** across every product and item sent: the reader is allowed if they hold any one of them. A single call can mix products and items, up to 50 keys total.
```bash
curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/access?domain=example.com&product=remove-ads" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Ezoic-Reader-Email: reader@example.com"
```
Response:
```json
{
"success": true,
"data": {
"decision": "allowed",
"reasonCode": "allowed"
}
}
```
Grant access only when `decision` is `allowed`. Treat every other decision as no current access. See [Access Decisions](#access-decisions) for every `decision` value.
To check a one-time per-item purchase instead, send `item` with the same key your site passed at checkout:
```bash
curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/access?domain=example.com&item=article-12345" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Ezoic-Reader-Email: reader@example.com"
```
The onsite `hasPurchased({ page: true })` shortcut has **no server-side equivalent**: the browser knows the reader's current page, but your server doesn't, so the REST API always takes an explicit `item` key — the same key your site passed at checkout.
For a price that unlocks *the current article automatically*, that key is the article's page path (for example `/my-article`). Send the article's URL or its path as `item`; a full URL is reduced to the page path before matching. A bare path is matched verbatim, so it must not carry a trailing slash — the widget trims it when stamping the key (`/my-article`, never `/my-article/`).
### List Products
```http
GET /subscriptions/v1/products HTTP/1.1
```
Returns the product handles the reader currently holds — the server-side mirror of `getProducts()`. One-time item purchases are not products and are not included here (use [List Purchases](#list-purchases)).
```bash
curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/products?domain=example.com" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Ezoic-Reader-Email: reader@example.com"
```
Response:
```json
{
"success": true,
"data": ["remove-ads", "premium"]
}
```
A reader with no active products returns an empty array.
### List Purchases
```http
GET /subscriptions/v1/purchases HTTP/1.1
```
Returns the reader's purchases and entitlements — the server-side mirror of `getPurchases()`. Unlike `/products`, this **includes** one-time per-item purchases, and each entry carries its status and expiry.
```bash
curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/purchases?domain=example.com" \
-H "X-API-Key: YOUR_API_KEY" \
-H "X-Ezoic-Reader-Email: reader@example.com"
```
Response:
```json
{
"success": true,
"data": [
{
"productKey": "premium",
"status": "active",
"expiresAt": "2026-12-31T23:59:59Z"
},
{
"item": "article-12345",
"status": "active"
}
]
}
```
Each entry has:
| Field | Description |
| --- | --- |
| `productKey` | The product the purchase belongs to. Omitted for a one-time item purchase, and omitted if the product was removed. |
| `item` | Set only for a one-time per-item purchase. |
| `status` | The entitlement status: `pending`, `active`, `expired`, `revoked`, or `suspended`. Filter on `active` for current access. |
| `expiresAt` | RFC 3339 timestamp, set only for time-limited access. Lifetime access omits it. |
A reader with no purchases returns an empty array.
### List Prices
```http
GET /subscriptions/v1/prices HTTP/1.1
```
Returns the display data for prices addressed by key — for rendering an offer on your own server when your content-to-price mapping already knows which price to show. This is the domain's active catalog, not reader state, so it takes **no reader-identity header**.
Query parameters (in addition to `domain`):
| Parameter | Description |
| --- | --- |
| `keys` | Comma-separated price handles, up to 50. |
```bash
curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/prices?domain=example.com&keys=premium-monthly,premium-annual" \
-H "X-API-Key: YOUR_API_KEY"
```
Response:
```json
{
"success": true,
"data": [
{
"name": "Premium Monthly",
"priceKey": "premium-monthly",
"type": "recurring",
"amountCents": 499,
"currency": "usd",
"billingInterval": "month",
"billingIntervalCount": 1,
"grantScope": "product",
"trialPeriodDays": 7,
"badgeLabel": "",
"ctaLabel": ""
}
]
}
```
| Field | Description |
| --- | --- |
| `name` | The price's display name. |
| `priceKey` | The price handle. |
| `type` | `recurring` or `one_time`. |
| `amountCents` | The amount in the smallest unit of `currency`. |
| `currency` | The price's default currency. |
| `billingInterval` | `day`, `week`, `month`, or `year` for recurring prices; `none` for one-time prices. |
| `billingIntervalCount` | Number of intervals between billings. |
| `grantScope` | What the price unlocks — the whole product or specific items. |
| `trialPeriodDays` | Free-trial length, when the price has one. |
| `badgeLabel` / `ctaLabel` | The price's display badge and call-to-action labels. |
| `accessDurationDays` | Access length for a time-limited one-time price. |
| `boundItems` | The item keys an item-scoped price grants. |
### Get Identity Token
```http
POST /subscriptions/v1/identity-token HTTP/1.1
```
Returns a short-lived, signed identity token for a signed-in reader.
**You only need this endpoint if you use [bring-your-own login](/docs/subscriptions/visitor-authentication/#bring-your-own-login) and want to keep the reader's email off the page.** It exists for sites whose pages run third-party scripts: instead of returning the reader's email to page JavaScript (where any script can read it), you return this token, so the page never sees the email. If that doesn't apply to you, skip it — the [JavaScript API](/docs/subscriptions/api-reference/) and the other endpoints here use a plain email.
The flow:
1. Your page's `AuthAdapter` implements [`getIdentityToken()`](/docs/subscriptions/visitor-authentication/#getidentitytoken), which calls an endpoint on **your** server.
2. Your server reads the signed-in reader's email from its own session and calls this endpoint with it.
3. This endpoint returns a signed token; the email is encrypted inside it.
4. Your server returns the token to the page, and `getIdentityToken()` resolves with it. The widget sends the token instead of the email.
Unlike the other endpoints, this is a **POST** with a JSON body, and it carries **no reader-identity header** — the email goes in the body because this endpoint creates a token for that address; it does not look a reader up.
```bash
curl -X POST "https://api-gateway.ezoic.com/subscriptions/v1/identity-token?domain=example.com" \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
--data '{ "email": "reader@example.com" }'
```
Response:
```json
{
"success": true,
"data": {
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresAt": "2026-08-13T18:00:00Z"
}
}
```
| Field | Description |
| --- | --- |
| `token` | The signed identity token. Hand it to the page and return it from `getIdentityToken()`. The reader's email is encrypted inside it — the token reveals nothing to page JavaScript. |
| `expiresAt` | RFC 3339 expiry. The token is valid for **one hour**. |
**Don't cache the token.** Call this endpoint whenever the page needs one — typically from a small endpoint on your server that `getIdentityToken()` hits. Don't fetch once and reuse. After `expiresAt` the token is rejected, and checkout falls back to asking for an email.
Errors: `400` for a missing or invalid email (the address must be one Stripe will accept at checkout); `503` if identity tokens are not configured for the service — an operations condition, not a caller bug.
## Access Decisions
`/access` returns a `decision` and a `reasonCode`. Grant access only for `allowed`.
| Decision | Meaning |
| --- | --- |
| `allowed` | The reader has active access. |
| `denied` | The reader is known but has no entitlement for this product or item. |
| `login_required` | No reader could be resolved from the identity you sent — treat as signed out. |
| `expired` | The reader had access, but it has ended. |
| `revoked` | Access was removed. |
The `reasonCode` gives more detail behind the decision. Possible values: `allowed`, `customer_required`, `no_entitlement`, `not_started`, `expired`, `revoked`, `suspended`, `pending`.
## Unknown Readers
If the identity you send doesn't resolve to a known reader — an email that has never subscribed, or a session token that has lapsed — the reader simply holds nothing:
- `/access` returns `decision: "login_required"` with `reasonCode: "customer_required"`.
- `/products` and `/purchases` return an empty array.
This is a normal outcome, not an error. Prompt the reader to sign in or subscribe.
## Response Format
Every response looks like this:
```json
{ "success": true, "data": ... }
```
On an error, `success` is `false` and `message` describes the problem:
```json
{ "success": false, "message": "A reader email or token is required" }
```
Common error statuses:
| Status | Cause |
| --- | --- |
| `400 Bad Request` | Missing `product` and `item` on `/access`, more than 50 principals on `/access`, missing/duplicate reader identity headers, or a missing/invalid `email` on `/identity-token`. |
| `403 Forbidden` | Subscriptions are not enabled for the domain, the API key lacks the Subscriptions permission, or a relayed reader token is invalid. |
| `429 Too Many Requests` | Rate limited. The API allows 600 requests per minute per API key and domain; the gateway may apply its own per-key limits on top. |
| `503 Service Unavailable` | Identity tokens are not configured (the `/identity-token` endpoint only). |
Authentication and authorization for the API key and `domain` are handled by the Ezoic API before the request reaches Subscriptions.
## Responses Are Not Cached
These reads reflect the reader's live entitlements and are served with `Cache-Control: no-store`. Call the API when you need a decision rather than caching results, so a new purchase, cancellation, or expiry takes effect immediately.
---
## 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, so a visitor (or their card) that already trialed the product is offered the regular price instead. 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
```
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
```
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 `
```
## Adding Placeholders dynamically
To add a placeholder to the page create a `div` element of where the ad should be. The element's id attribute should be the ID of the placeholder with prefix `ezoic-pub-ad-placeholder-`.
For example, to add placeholders with the id 103 and 104, the HTML would look similar to this.
```html
```
## Calling For Ads
Once the placeholders have been added to the HTML, call for the Ezoic ad code by using `ezstandalone.displayMore()`.
```html
```
## Exceptions
For integrated sites without static placeholders that want to load all placeholders dynamically using our client-side ad setup, please use the following guide instead: [Getting Started With EzoicAds](/docs/ezoicads/getting-started/).
`Static placeholders` are placeholders which are added to the html when passing through our proxy, rather than being added via javascript
---
## Controlling Form Factor Traffic
Controlling device traffic relies on the settings in Ezoic [dashboard](https://pubdash.ezoic.com/ezoicads/optimizationgoals)
If traffic to a specific form factor is disabled in the Ezoic dashboard, you can add a conditional statement using the `ezstandalone.isEzoicUser()` function to respect those settings.
```html
```
---
## Legacy Features
Source: https://docs.ezoic.com/docs/ezoic-legacy-features/legacy/
## Prerequisites
Make sure placeholders are created via the Ezoic dashboard. See our [support article](https://support.ezoic.com/kb/article/ezoicads-getting-started-guide) to learn more about placeholders and how to create them.
## Load Standalone library
Load the standalone library by adding this script to the `` of the page.
```html
```
This will load the `ezstandalone` global object and run any queued functions.
## Adding Placeholders
To add a placeholder to the page, create a `div` element where the ad should be. The element's id attribute should be the ID of the placeholder with prefix `ezoic-pub-ad-placeholder-`.
__DO NOT__ add any styling to the actual placeholder `div`.
Adding styles or reserving space for the ad may end with undesired results (e.g. if a placeholder is not chosen by our system, there may be empty space on the page).
For example, to add a placeholder with the id 103, the HTML would look similar to this.
```html
```
## Defining Placeholders
Defined placeholders must exist in the HTML.
Defining placeholders tells ezstandalone which placeholders are available to determine the optimal setup.
Pass all the placeholders that exist on the page to the `ezstandalone.define` function with the following code.
```html
```
Note: example is assuming placeholders `102`, `103`, and `104` were created through the dashboard and exist on the page.
Be wary for the correct scoping of `ezstandalone`.
## Calling For Ads
After defining the placeholders, enable standalone by calling `ezstandalone.enable()`. This will fetch optimizations from our servers.
The following is what the code should look like after defining and enabling the placeholders.
```html
```
Note: this will call for the ad code of the potential placeholders `102`, `103`, and `104`. In order to have the ads show, display must be called.
## Displaying ads
In order to display ads, call `ezstandalone.display()` after defining and enabling the placeholders.
```html
```
## Setup Complete
Once all steps are done, a basic version of standalone is setup, and pages are ready to show ads!
This section is for websites whose content loads or changes dynamically
## Dynamically Changing Pages
When switching between pages dynamically, it is important to call
```javascript
ezstandalone.refresh()
```
Calling `ezstandalone.refresh()` tells standalone that it is a new pageview and calls for new ad code on the already defined placeholders.
If the new page contains different set of placeholders from the previous page, you must call `ezstandalone.define()` on the set of placeholders prior to calling refresh
```html
```
__Caution:__ calling define will overwrite the previous set of placeholders, so make sure to add any placeholders that persist from the previous set
eg. If placeholder `103` and `104` exist on page `example.com/page1` and the user dynamically changes to page `example.com/page2` where placeholder `104` and `105` exist. Placeholder `104` must be included in the define call
## New Content
For content that loads after the initial content has loaded, and new placeholders are added, you'll want to call for those placeholders using the `ezstandalone.displayMore` function
eg. If a user scrolls down your page and new placeholders `104` and `105` are added to the page you would call the function like so
```html
```
## Changing content
If the content changes within the same pageview and a placeholder is no longer needed or visible, you need to properly clean up that placeholder using `ezstandalone.destroyPlaceholders`
```html
```
## Infinite Scroll
For sites which implement an infinite scroll, you'll have to use a combination of calling `ezstandalone.destroyPlaceholders` followed by `ezstandalone.displayMore` to reuse placeholders
For infinite scroll it is recommended create a set of `in-content` placeholders to use specifically for it
### Example
The example below shows the flow of an infinite scroll would look like on a site with multiple articles
Start off by calling the placeholder for the first article on page load
```html
```
User then scrolls to next article so you load the next set of ads
```html
```
User then scrolls to third article and you need to reuse the placeholders from the first article
```html
```