View as Markdown

Server-to-Server 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: the same access decisions and response shapes, reached over HTTPS from your backend.

For many sites, the onsite JavaScript API is all you need. The REST API is for implementations that make access decisions on their own server — authorizing your own APIs, gating content before it leaves your backend, or reconciling entitlements in a job. It gives you a secure, tamper-resistant way to verify what a reader can access, with the decision made on your server rather than in the browser.

Use it when access decisions belong on your server — gating an API you own, rendering paid content server-side, or reconciling entitlements in a job — 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 your Ezoic API-gateway developerKey. It is shared with any other Ezoic API-gateway services you've enabled, and you rotate it from your API-gateway 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 gateway:

https://api-gateway.ezoic.com/subscriptions/v1

Authentication

Every request identifies your site with two query parameters and identifies the reader with a header.

Site Credentials

Parameter Description
developerKey Your Ezoic API key, from the REST API settings card.
domain The domain you are querying, for example example.com. It must belong to your account.

The gateway authenticates the developerKey, confirms the domain is yours, and rejects the request otherwise — so a key can only ever read its own domains.

Reader Identity

Each request asks about one reader. Assert who that reader is 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 the onsite JavaScript API, then send it to your backend:

ezsubscriptions.cmd.push(function (api) {
  const token = api.getSessionToken(); // the signed-in reader's JWT, or null
  if (token) {
    // Send it to your own server (e.g. as a header on your app's own request),
    // where you'll forward it to the REST API as X-Ezoic-Reader-Token.
    fetch("/my-backend/unlock", { headers: { "X-Reader-Token": token } });
  }
});

Your server then forwards that token to the REST API:

curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/access?developerKey=YOUR_API_KEY&domain=example.com&product=remove-ads" \
  -H "X-Ezoic-Reader-Token: THE_RELAYED_TOKEN"
import requests

url = "https://api-gateway.ezoic.com/subscriptions/v1/access?developerKey=YOUR_API_KEY&domain=example.com&product=remove-ads"
headers = {"X-Ezoic-Reader-Token": "THE_RELAYED_TOKEN"}

response = requests.get(url, headers=headers)
print(response.json())
<?php
$ch = curl_init("https://api-gateway.ezoic.com/subscriptions/v1/access?developerKey=YOUR_API_KEY&domain=example.com&product=remove-ads");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Ezoic-Reader-Token: THE_RELAYED_TOKEN"]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;

getSessionToken() returns null for a signed-out visitor, and the token is short-lived and scoped to the current domain — 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() in the JavaScript API reference.

Endpoints

Check Access

GET /subscriptions/v1/access

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 developerKey and domain):

Parameter Description
product A product handle to check whole-product access.
item A one-time item key to check a per-item purchase.

Provide one of product or item. If both are sent, product is used.

curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/access?developerKey=YOUR_API_KEY&domain=example.com&product=remove-ads" \
  -H "X-Ezoic-Reader-Email: reader@example.com"
import requests

url = "https://api-gateway.ezoic.com/subscriptions/v1/access?developerKey=YOUR_API_KEY&domain=example.com&product=remove-ads"
headers = {"X-Ezoic-Reader-Email": "reader@example.com"}

response = requests.get(url, headers=headers)
print(response.json())
<?php
$ch = curl_init("https://api-gateway.ezoic.com/subscriptions/v1/access?developerKey=YOUR_API_KEY&domain=example.com&product=remove-ads");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Ezoic-Reader-Email: reader@example.com"]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Response:

{
  "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 for the full vocabulary.

List Products

GET /subscriptions/v1/products

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

curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/products?developerKey=YOUR_API_KEY&domain=example.com" \
  -H "X-Ezoic-Reader-Email: reader@example.com"
import requests

url = "https://api-gateway.ezoic.com/subscriptions/v1/products?developerKey=YOUR_API_KEY&domain=example.com"
headers = {"X-Ezoic-Reader-Email": "reader@example.com"}

response = requests.get(url, headers=headers)
print(response.json())
<?php
$ch = curl_init("https://api-gateway.ezoic.com/subscriptions/v1/products?developerKey=YOUR_API_KEY&domain=example.com");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Ezoic-Reader-Email: reader@example.com"]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Response:

{
  "success": true,
  "data": ["remove-ads", "premium"]
}

A reader with no active products returns an empty array.

List Purchases

GET /subscriptions/v1/purchases

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.

curl -X GET "https://api-gateway.ezoic.com/subscriptions/v1/purchases?developerKey=YOUR_API_KEY&domain=example.com" \
  -H "X-Ezoic-Reader-Email: reader@example.com"
import requests

url = "https://api-gateway.ezoic.com/subscriptions/v1/purchases?developerKey=YOUR_API_KEY&domain=example.com"
headers = {"X-Ezoic-Reader-Email": "reader@example.com"}

response = requests.get(url, headers=headers)
print(response.json())
<?php
$ch = curl_init("https://api-gateway.ezoic.com/subscriptions/v1/purchases?developerKey=YOUR_API_KEY&domain=example.com");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Ezoic-Reader-Email: reader@example.com"]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Response:

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

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.
unknown_product The product handle is not recognized for this domain — usually a typo or an inactive product.

The reasonCode gives more detail behind the decision. Possible values: allowed, customer_required, unknown_product, 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 is a JSON envelope:

{ "success": true, "data": ... }

On an error, success is false and message describes the problem:

{ "success": false, "message": "A reader email or token is required" }

Common error statuses:

Status Cause
400 Bad Request Missing product and item on /access, or missing/duplicate reader identity headers.
403 Forbidden Subscriptions are not enabled for the domain, or a relayed reader token is invalid or was minted for a different domain.

Authentication and authorization for the developerKey and domain are handled by the Ezoic API gateway 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.