# 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 — 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:

```text
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:

```javascript
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:



```bash
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"
```



`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()`](/docs/subscriptions/api-reference/#getsessiontoken) in the JavaScript API reference.

## Endpoints

### Check Access

```text
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](/docs/subscriptions/publisher-managed-access/#product-handles) 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.



```bash
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"
```



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

### List Products

```text
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](#list-purchases)).



```bash
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"
```



Response:

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

A reader with no active products returns an empty array.

### List Purchases

```text
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.



```bash
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"
```



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

```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`, 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.

