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.
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:
- In your Ezoic dashboard, open Subscriptions → Settings.
- Open the REST API card and turn on API access.
- 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.
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:
/accessreturnsdecision: "login_required"withreasonCode: "customer_required"./productsand/purchasesreturn 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.