---
title: "REST API overview"
description: "Base URL, the three authentication methods with a worked hash example, where your API key lives, what happens after repeated failed credentials, response shapes, pagination, URL conventions, error codes and where to find the interactive API docs."
type: "reference"
audience: [network-manager, admin]
keywords: [api, rest, authentication, hash, timestamp, apiKey, x-api-key, bearer, lockout, token, swagger, openapi, pagination, errors, jsonmodel]
---

Fetch the complete documentation index at: https://epom.com/ad-server/docs/llms.txt. Use this file to discover all available pages before exploring further. Append .md to any documentation page URL to get its markdown version.

# REST API overview

Nearly everything you can do in the control panel — managing advertisers, campaigns, creatives,
sites, placements, targeting, capping and reporting — is also available over a REST API.
This page covers the conventions shared by all endpoints. The other pages in this section
walk through each functional area with `curl` examples and link every call to its full entry
in the [generated reference](https://epom.com/ad-server/docs/api/endpoints.md).

## Base URL

The API is served from the same domain as your control panel:

```
https://<your-network-domain>/rest-api/...
```

Every endpoint path starts with `/rest-api/` and ends in `.do`, with no exceptions. Some
paths answer more than one HTTP method; the [per-tag reference](https://epom.com/ad-server/docs/api/endpoints.md) lists every
path and method and gives the current totals. If your network runs under a white-label domain, use that domain; API access,
permissions and data scope are identical.

## Authentication

Three methods are supported. Pick one per request; you don't combine them.

| Method | Best for | How it works |
|---|---|---|
| Hash (`username` + `timestamp` + `hash`) | Server-to-server integrations | Signed request: an MD5 signature built from your password and a timestamp |
| API key (`X-Api-Key` header) | Scripts and integrations | One header on any REST call that needs a signed-in user |
| Session cookie | Browser-based tools, quick tests | Log in to the control panel, reuse the session cookie |

### Hash authentication (recommended)

Add three query parameters to any request:

| Parameter | Value |
|---|---|
| `username` | Your control-panel login |
| `timestamp` | Current Unix time **in milliseconds** |
| `hash` | `MD5( MD5(password) + timestamp )` — hex digest, either letter case |

Worked example with password `MySecret42` and timestamp `1751712000000`:

1. `MD5("MySecret42")` → `307ec992a5c1e4622ae2f7c909a0992f`
2. Concatenate with the timestamp → `307ec992a5c1e4622ae2f7c909a0992f1751712000000`
3. `MD5` of that string → `9ece291919268a203f248e81a969253f` — this is your `hash`

```bash
TS=$(($(date +%s) * 1000))                                    # milliseconds!
PWD_MD5=$(printf '%s' 'MySecret42' | md5 -q)                  # Linux: md5sum | cut -d' ' -f1
HASH=$(printf '%s%s' "$PWD_MD5" "$TS" | md5 -q)               # Linux: md5sum | cut -d' ' -f1

curl "https://<your-network-domain>/rest-api/advertisers.do?username=john.doe&timestamp=$TS&hash=$HASH"
```

> **Warning:** the timestamp must be in **milliseconds**, and the server accepts it only
> within ±5 minutes of its own clock. A timestamp in seconds, or a machine with a skewed
> clock, produces `Request is obsolete.` A wrong password or a wrong concatenation order
> produces `Data fingerprint mismatch.`, and an unknown `username` produces
> `No matching credentials found.` — all three arrive as HTTP 401.

> **Corner case:** the signature is checked **before** the clock. A request whose hash is
> wrong reports the fingerprint mismatch even if the timestamp is also stale, so fix the
> hash first and re-test before blaming clock drift.

Because the timestamp is part of the signature, a long-running script must recompute
`timestamp` **and** `hash` per request, not once at start-up.

Three endpoint families take the same three values as **path segments** rather than query
parameters — the formula is identical:

```
/rest-api/analytics/{format}/{login}/{hash}/{timestamp}.do
/rest-api/analytics/reports-layout-settings/{login}/{hash}/{timestamp}.do
/rest-api/self-serv/dashboard/{login}/{hash}/{timestamp}.do
```

See [Analytics reports via API](https://epom.com/ad-server/docs/api/analytics-api.md).

> **Note:** because `hash` and `timestamp` are signature values rather than business
> parameters, the generated reference and Swagger UI omit them from query-parameter lists.
> They still have to be sent. They *are* listed where they are path segments, as above.
> `username` and `apiKey` are ordinary parameters and are shown normally.

#### The `$AUTH` shorthand

The `curl` examples on the other API pages abbreviate the three signature parameters as
`$AUTH` and append it to the URL. Build it from the `TS` and `HASH` above, fresh for each
request:

```bash
AUTH="username=john.doe&timestamp=$TS&hash=$HASH"

curl "https://<your-network-domain>/rest-api/sites.do?$AUTH"
```

To authenticate with an [API key](#api-key) instead, set the key once and send it as a header
in place of `$AUTH` — every example works the same either way:

```bash
APIKEY="<your API key>"

curl -H "X-Api-Key: $APIKEY" "https://<your-network-domain>/rest-api/sites.do"
```

### API key

Send your key in the `X-Api-Key` header (or as `Authorization: Bearer <key>`) on any REST call
that needs a signed-in user. The key is a 32-character value tied to your user account,
generated on your Preferences page; the call runs with exactly your permissions. Your role
needs the *API Key Access* permission, and **no stock role has it** — on a new network your
network administrator switches it on for the roles that should use the API. Without it the key is refused
with **401** and `API key access is not enabled for this account`; the key of a deactivated
account is refused with `Account is not active`. The analytics report (`/rest-api/analytics/{format}`)
and the MCP endpoint (`/mcp`) refuse it with **403** instead.

```bash
curl -H "X-Api-Key: $APIKEY" "https://<your-network-domain>/rest-api/sites.do"
```

A request that carries more than one key has them tried in order — `X-Api-Key`, then
`Authorization: Bearer` — and the first that belongs to an account allowed to use API keys
wins, so a proxy that adds its own `Bearer` header does not hide your `X-Api-Key`.

On general REST calls the key works only in a header: in the query string it would end up in
proxy logs and browser history, so a query-string `apiKey` is not read there — the call is
authenticated by hash or session, or answers 401. Send the key as `X-Api-Key` on the analytics
report too:

```bash
curl -H "X-Api-Key: $APIKEY" "https://<your-network-domain>/rest-api/analytics/JSON.do?range=YESTERDAY"
```

As a fallback for a client that cannot set a header, that one endpoint
(`/rest-api/analytics/{format}.do`, GET and POST) also accepts the key as an `apiKey` query
parameter; a header wins when both are sent.

Unlike a signed hash, which expires after five minutes, a key is valid until you generate a
new one — treat it like a password and rotate it if it leaks.

**Where your key lives.** It is your own, not the network's, and you mint it yourself:
*Account → Preferences → Change personal information → **API key***. Click **Generate** next
to the field: the server mints a fresh 32-character hex value, saves it at once and shows it
in the field — there is nothing to submit afterwards. The **AI assistant (MCP)** card on the
same page shows the same key and has its own **Generate API key** button while you have none;
see [Connect an AI assistant (MCP)](https://epom.com/ad-server/docs/api/mcp.md).

| What you see on the **Change personal information** card | Why |
|---|---|
| The **API key** field with a **Generate** button | *API Key Access* is enabled and *Manage Preferences* is enabled |
| The field, read-only, with no button | *API Key Access* is read-only; or *Manage Preferences* is read-only, and then the card is titled **Personal information** |
| No **API key** field at all | Your role has no *API Key Access*, or *Manage Preferences* is off and the whole card is hidden — ask your network administrator |

> **Warning:** **Generate** replaces the key permanently the moment you click it — there is no
> confirmation, no grace period and no second key slot. Every integration using the old key
> stops authenticating at that moment. Roll it only when you are ready to update the callers.

> **Corner case:** a wrong, revoked or unknown API key is refused with **401** on both doors,
> but the body differs. On a general REST call it is `{"success": false, "errors": {"error":
> "Not logged in"}}`. On the analytics endpoint it carries `rest_error`
> (`No matching credentials found.`) next to `errors.user` (`invalid user`) — the analytics
> endpoints use their own body shape for authentication failures, not `errors.error`.

### Failed credentials and the lockout

Repeated refused credentials — a wrong hash, an unknown login, a wrong API key — temporarily block
further attempts from the caller's address. While it is blocked, every request that presents a
credential is refused with **401** and `Too many failed attempts`, even one with a valid key or
hash. Wait a few minutes and try again; if it persists, ask your network manager. A console session
that is already signed in keeps working.

### Session cookie and token login

For browser-based access, log in to the control panel and the session cookie
(`APPSESSIONID`) authenticates subsequent API calls from the same browser.

For programmatic session creation there is a two-step token flow:

1. `POST /rest-api/auth/token.do` with `username` and `password`. Returns
   `{"success": true, "authToken": "...", "role": "..."}`.
2. `GET /rest-api/auth/{authToken}/login.do` — establishes the session.

> **Corner case:** step 2 **redirects to `/index.do` by default, even on failure**. Add
> `json=true` to get `{"success": …, "redirectUrl": …}` back instead, or a `callback`
> parameter for JSONP. Without one of those, a script that follows redirects cannot tell a
> good token from a bad one.

There is also `POST /rest-api/auth/forgot.do` to trigger a password-reset email. It
succeeds silently for an address that has no account, on purpose — that response tells you
nothing about whether the address exists.

**Signing a user in from another website: navigate, do not fetch.** Step 2 has three answer
modes. The default answers with a redirect to the control panel; `json=true` answers JSON and
`callback=<fn>` answers JSONP. Only the default mode works when the call comes from a page on
another site: open the URL as a top-level navigation (`window.location`, a link, or a form
`target="_top"`), and the session cookie is set while the browser loads it. Called from another
site through `<script>`, `fetch` or XHR — which is what the JSONP and `json=true` modes are for —
the response still reports `success: true`, but the browser discards the session cookie
(`SameSite=Lax`), so following `redirectUrl` lands on the login page and the token is already
spent. Use those two modes only from pages served by the control panel's own domain.

> **Corner case:** the token is valid for **one minute** and is invalidated after a single
> use — the server evicts it from the cache as it reads it. The token endpoint requires
> HTTPS unless the server was explicitly configured to allow HTTP; over plain HTTP it
> answers **403** with `{"success": false, "error": "Secure channel required"}`. Every refusal —
> unknown username, wrong password, deactivated account, an address outside the user's IP
> whitelist — answers **401** with the same `Username or password is incorrect`, so check each
> of those when a known-good password is refused. After repeated failures the answer is the
> lockout message instead (also 401), whatever the password; wait a few minutes.

**Writes over a session need the CSRF token.** A `POST`, `PUT` or `DELETE` authenticated only
by the session cookie must also send the session's CSRF token in the `X-CSRF-TOKEN` header
(or a `_csrf` parameter); without it the call is refused with **403** and nothing is changed.
The control panel and Swagger UI add the token for you. A script that logs in through the
token flow and then writes with a cookie jar should switch to hash or API-key authentication,
which need no token. Reads (`GET`) are unaffected.

> **Note:** cross-origin calls get no access to the session cookie: browser code on another
> site can call the API, but only with hash or API-key authentication. The session cookie is
> sent as `SameSite=Lax`, and the session id changes at login.

## Your first call

A good first call lists the sites your account can see: `GET /rest-api/sites.do` (an advertiser
account starts with `advertisers.do`, which answers in the same bare-array form). Every role can
sign a request with [hash authentication](#hash-authentication-recommended); no permission is
needed for it. The API key route needs *API Key Access* on your role, which **no stock role holds**
— on a publisher or advertiser account, ask your network administrator to switch it on for your role
before you look for a key (see [API key](#api-key)).

```bash
curl -H "X-Api-Key: $APIKEY" "https://<your-network-domain>/rest-api/sites.do"
```

The answer is a bare array — there is no `success` key — with one object per site you can view,
ordered by id. Deleted sites are left out, and an account that may view no inventory gets `403`:

```json
[
  {"id": 5001, "name": "Sports Daily", "enableTargeting": false},
  {"id": 5002, "name": "Northwind Recipes", "enableTargeting": true}
]
```

`enableTargeting` is the site's pre-targeting switch. Add `basicInfo=true` to get a `basicInfo`
object on every row (or `basicInfoIds=5001,5002` for only those sites):

```json
[
  {
    "id": 5001,
    "name": "Sports Daily",
    "enableTargeting": false,
    "basicInfo": {
      "isActive": true,
      "hasTraffic": true,
      "created": {"timestamp": 1751712000000, "formatted": "05/07/2025"},
      "url": "https://sportsdaily.example",
      "email": "ads@sportsdaily.example",
      "monthlyVisitors": "-",
      "monthlyImpressions": "-"
    }
  }
]
```

`hasTraffic` says whether the site has served recently. `created`, `url`, `email` and
`description` appear only when the site has a value; `monthlyVisitors` and `monthlyImpressions`
are the ranges picked on the site, or `-`. `isPrivate` and `categoryIds` (comma-separated) appear
only for a role that may see the *Private* flag and inventory categories. To trim rows, pass
`fields=name` (the `id` is always kept), and page with `offset` and `limit` — see
[Pagination and list conventions](#pagination-and-list-conventions).

## Response format

There is no single envelope. Which shape you get depends on the endpoint family, so read
the shape before you write the client.

| Shape | Looks like | Who returns it |
|---|---|---|
| Bare array | `[{…}, {…}]` | Most entity list and tree endpoints — `advertisers.do`, `campaigns.do`, `sites.do`, `banners.do`, `placements/all.do`, `advertisers-tree.do`, `sites-tree.do` |
| Bare object | `{"name": "…", "id": 4, …}` | Most single-entity reads — `advertisers/{id}.do`, `zones/{id}.do` |
| Grid page | `{"data": […], "total": 12, "overflowed": false}` | Grid-backed lists, e.g. `template/list.do` |
| Status envelope | `{"success": true, "id": 1001}` | Create, update, delete and action endpoints |
| Error envelope | `{"success": false, …}` | Every failure — see [Error codes](#error-codes) |

> **Warning:** `success` exists only on the last two shapes. A client that does
> `if (!response.success) fail()` treats every successful `GET /rest-api/advertisers.do` as
> a failure, because a bare array has no `success` key. Branch on the HTTP status first,
> then on `success` where the body is an object that has one.

On the status envelope:

- `success` — `false` on every failure. A failure is also answered with the 4xx status that
  describes it (see [Error codes](#error-codes)), so you can rely on either the status or the
  flag; the body keeps the same keys it always had.
- `msg` — human-readable message, usually only on errors.
- `errors` — map of field name → validation message on validation failures. A failure whose
  reason is only in `msg` (or in an `error` code) repeats it as `errors.error`, so one reader
  handles every refusal.
- Create endpoints typically add an `id` field with the new entity's ID.
- Empty collections are serialized, not dropped: an empty grid page is
  `{"data": [], "total": 0, "overflowed": false}` and an empty list endpoint is `[]`.
- `overflowed` on a grid page means the result was truncated by a server-side ceiling
  rather than by your `limit`.

### Parameters that were not applied

The create and update calls — advertisers, campaigns, banners, sites, zones and placements,
limits and pricing writes, saved reports, templates, and the site/placement piggyback and
own-profile updates — set only the fields they are made for and that your role may change. A parameter outside that set does not fail the call — the
rest of the update is still applied — but it is named back so a success is never mistaken for
"everything I sent was saved":

```json
{
  "success": true,
  "ignoredFields": ["active", "contactNmae"],
  "ignoredFieldReasons": {
    "active": "not permitted",
    "contactNmae": "not a field of this operation"
  }
}
```

- `not permitted` — a real field of the call that needs a permission your role lacks (the
  stored value is kept);
- `not a field of this operation` — a misspelt name, a field managed by another endpoint, or
  one the call never changes;
- a sentence naming the cause, where the call itself could not apply the value and says why — the
  permission a changed limit cap or a banner `status` needs (`Your role lacks the
  TOTAL_CLICKS_LIMIT permission.`), a `thirdTrackingCode[…]` key that is no country, a
  `browserVersion` the browser cannot be targeted by, a `linkToPlacements` with no auto-linking
  rules or no linking permission. The field is not applied and the rest of the call is saved.

Both keys are absent when everything was applied. Authentication parameters and parameters the
call reads itself (ids, `categoryId`, `labels`, …) are never listed.

## Pagination and list conventions

Three parameter styles exist, depending on the endpoint family:

| Style | Parameters | Used by |
|---|---|---|
| Entity lists | `offset` (0-based), `limit`, `fields` (comma-separated; `id` is always kept), `filter[…]` (on/off settings and `has…` relation flags) | `/rest-api/advertisers.do`, `campaigns.do`, `sites.do`, `banners.do`, `placements/summary.do`. Advertisers, campaigns and sites come ordered by id, and without `limit` they return every row. `/rest-api/sites/{siteId}/zones.do` takes `fields` and `filter` only |
| Trees | `start` (0-based), `limit` (omit it for every entry) | `/rest-api/advertisers-tree.do`, `/rest-api/sites-tree.do`; entries are advertisers or sites, ordered by id, each with everything beneath it. A page shorter than `limit` is the last one |
| Grid style | `start` (0-based offset), `limit`, plus `sort`, `dir` (`ASC`/`DESC`), `multisort` (JSON array of `{property, direction}`), `query` (name search), `activeOnly`, `onlyData`, `onlyTotal` | Grid-backed lists, e.g. `/rest-api/template/list.do` |

Analytics reports page with `offset` and `limit` too, but their `limit` defaults differently —
see [Analytics reports via API](https://epom.com/ad-server/docs/api/analytics-api.md#report-parameters). A requested `fields` name that no
returned row carries is listed in the `X-Ignored-Fields` response header, and an unknown
`filter` key is a `400` that lists the valid keys. `/rest-api/placements/all.do` is the one
list that is not paged and takes no parameters: it returns every accessible placement. The
per-endpoint rules (bounds on `limit`, the filter keys of each list) are in
[Entities: create, read, update, delete](https://epom.com/ad-server/docs/api/entities-crud.md).

## URL and parameter conventions

- **Kebab-case paths** are the convention for new endpoints (`/frequency-capping`,
  `/no-click-capping`, `/price-adjustment-rules`), and most paths already use it. A few
  older paths still carry a camelCase segment — `/advertiserShares.do`,
  `/siteShares.do`, `/floorCPMs.do`, `/banners/sendToApprove.do`,
  `/sites/{siteId}/changeAdServingDomain.do`, `/adsHosts.do`, `/invocationCodeHosts.do`,
  `/banner/dspPartners.do` and the rest. They work; expect new features in kebab-case.
- **One path is snake_case**: `/rest-api/placements/update/click_through.do`. Every
  neighbouring placement endpoint is kebab-case (`update/non-standard.do`), so this one is
  easy to guess wrong. Copy it exactly.
- **Parameters are form/query parameters**, not JSON bodies, for nearly all endpoints.
  Send POSTs as `application/x-www-form-urlencoded`. The exceptions (JSON or multipart
  bodies) are called out on the pages that document them.
- **Enum parameters** are per-parameter, not uniform. Most take the enum **name**
  (`priority=PREMIUM`, `bannerType=EXTERNAL_FILE`), several also accept the legacy numeric
  ID, and a few take the numeric ID only. The generated
  [per-tag reference](https://epom.com/ad-server/docs/api/endpoints.md) prints the accepted values for each parameter —
  when it shows a list of names, names are what that endpoint takes.
- **Dates** are `yyyy-MM-dd` on most endpoints; the limits endpoints use
  `yyyy-MM-dd-HH-mm`. Each page states the format it needs.
- Repeatable array parameters are passed by repeating the name:
  `countryCode=US&countryCode=DE`.

**Calls that change data do not answer `GET`.** Endpoints that delete, toggle, recalculate or
reload — for example `/rest-api/rules/{entity}/{entityId}/enable.do`, `…/remove.do` and
`/rest-api/template/delete.do` — accept `POST` only; a `GET` gets **405 Method Not Allowed**.

**Which method a delete takes.** Deletes are split between `DELETE` and `POST`, and the wrong
one gets **405**. The main families:

| Method | Delete endpoints |
|---|---|
| `DELETE` | Advertiser (`/advertisers/{id}/delete.do`) and banner (`/banner/{id}/delete.do`); targeting rules (`/{entity}/{id}/targeting/{targetId}/delete.do`, `/{entity}/{id}/targetings.do`) and preset domain, app-package and IP-range lists; capping by kind (`/{campaign\|banner}/{id}/frequency-capping.do` and the other kinds); entity limits (`/{entity}/{id}/limits.do`); country prices (`…/pricing/{countryCode}.do`); price adjustment rules; conversion actions; labels; shares; campaign piggybacks; VAST companions, files and events; saved reports; advertiser payments and publisher payouts |
| `POST` | Campaign, site, zone and placement (`…/{id}/delete.do`); rule groups (`/rule/{ruleId}/delete.do`) and detaching them (`/rules/{entity}/{id}/remove.do`); cappings by id (`/capping/delete.do`); per-action limits; pricing models (`…/pricing/{paymentModel}/remove.do`); bid floors; templates with their files and properties; placement and site piggybacks; server-side trackers; tracking codes; PMP deals; media library files; dashboard widgets; site CPM thresholds |

The [per-tag reference](https://epom.com/ad-server/docs/api/endpoints.md) gives the method of every endpoint.

**Where the full field list lives.** The task pages in this section name the fields a call
needs and the ones that change its behaviour. Every field of an operation — with its type,
description, accepted values and an example request — is in the generated reference, and each
task page links to the exact entry, e.g. [`POST /rest-api/sites/update.do`](https://epom.com/ad-server/docs/api/sites.md#post-rest-apisitesupdatedo).

## Error codes

| HTTP status | Meaning | Body |
|---|---|---|
| 200 | Request processed | Envelope with `success: true`. A batch that applied some of its items and refused others also answers 200: `success: false`, the saved ids in `updated` and every refused one in `failures` |
| 400 | Validation error, malformed parameter, missing required parameter, a value the database refuses | `errors` map and/or `msg`. A JSON body names the unknown field (with the accepted ones) or the field whose value is invalid (with accepted values for an enum). A value the database refuses — out of range, too long, a duplicate — is `A value in the request is out of range or too long for its field.` or `The request conflicts with stored data…`, without the SQL |
| 401 | Authentication failed: bad hash, expired timestamp, unknown or unauthorised API key, no credential at all, a locked address, wrong password on `auth/token` | `errors.error` on general REST calls and on `auth/token` (`error`): `Data fingerprint mismatch.`, `Request is obsolete.`, `No matching credentials found.`, `Not logged in`, `API key access is not enabled for this account`, `Account is not active`, `Too many failed attempts`, `Username or password is incorrect`. The analytics report endpoints put the text under `rest_error` instead |
| 403 | Authenticated but not permitted (missing role permission or share) | `errors.error` (and `msg` with `permissionDenied: true` where the endpoint always sent it); the missing grant is named — `Your role lacks the OPEN_RTB permission.` or `You have no UPDATE permission on Campaign 2001.` — so you know what to ask your network admin for |
| 404 | Entity or endpoint not found, including a delete or update of something that does not exist. A deleted entity outside your shares answers exactly as an id that never existed — it is not a 403 | `errors.error` and/or `msg` naming the missing entity and id, e.g. `Placement 4001 not found.` |
| 405 | Wrong HTTP verb for that path | `{"success": false, "message": "This method is not allowed for the requested URL."}` |
| 409 | The write clashes with an existing record (e.g. a PMP deal with the same deal ID, buyer and country), or a creative's file is missing on the server — nothing was written | `conflict: true` or `bannersFileMissingOnFS: true`, with the reason in `msg` / `message` (for a missing file, upload it again). Switching targeting on or off never checks files, so it is not refused this way |
| 415 | Unsupported media type (wrong `Content-Type` on body endpoints) | `message`, e.g. `Content-Type 'text/plain' is not supported. Expected: application/json` |
| 429 | The caller's daily analytics request limit is reached | `errors["Too Many Requests"]`: `Daily requests limit reached.` |
| 500 | Internal error | Generic `Internal server error` (as `message`, as `errors.error` on endpoints that answer that shape, or as `msg` on the analytics report endpoints) — details are logged server-side, never returned |
| 503 | A service the answer depends on did not respond (e.g. the geo service behind region, city and carrier lists) | `errors.error`; retry later rather than reading an empty list as "none" |

> **Note:** failures carry the HTTP status above, never a 200 with `success: false`. The body
> still explains the failure, so a client that treats any 4xx as a transport error should read
> the body of a 4xx before discarding it.

> **Warning:** **the error text is not always under the same key.** Framework-level
> failures (missing parameter, wrong verb, unreadable body, unsupported media type, no
> handler, internal error) use `message`. Application-level failures use an `errors`
> object keyed by field name — with `error` as the field name when the failure isn't
> attributable to one field. A handful of endpoints use a flat `msg` instead. Read all
> three before deciding a response carried no explanation.

> **Corner case:** a URL that does **not** end in `.do` never reaches the API at all — the
> container answers it, and the 404 comes back in a different shape:
> `{"success": false, "status": 404, "msg": "Not Found"}`. If you see a `status` key, your
> path is malformed, not your parameters.

Analytics has two ceilings of its own, both set per network: **Daily requests per user limit**
(default 100) and **Records per request limit** (default 10,000). Exceeding the
first answers **429**; asking for more rows than the second answers **400** with an
`errors.limit` message naming the ceiling. A report refused for an earlier reason —
permission, bad parameters, or the record-count ceiling — never counts against the daily quota;
see [Analytics reports via API](https://epom.com/ad-server/docs/api/analytics-api.md#limits-and-performance) for how the day is counted.

## Interactive documentation

- **Swagger UI**: `https://<your-network-domain>/swagger-ui.html`
- **Raw OpenAPI spec** (JSON): `https://<your-network-domain>/v3/api-docs`

![Swagger UI: the Authorize button, the tag filter, and endpoint groups that expand into their operations](https://epom.com/docs-images/ad-server/reference-api-index/swagger-ui.webp)

Both require an authenticated session — open them in a browser where you are logged in to
the control panel; anonymous requests get 401. The spec is the authoritative parameter
reference; the pages in this section
summarize the parts you'll use most. Groups are sorted alphabetically and each one renders
already expanded, so use the **Filter by tag** box to get to one.

> **Note:** Swagger UI itself is loaded from a public CDN (`unpkg.com`). On a network that
> blocks outbound CDN traffic the page renders blank while `/v3/api-docs` keeps working —
> fetch the raw spec and open it in your own viewer.

## Per-tag endpoint reference

For the exhaustive endpoint-by-endpoint reference — every path, parameter, request body
and response per OpenAPI tag — see the generated
[API endpoint reference by tag](https://epom.com/ad-server/docs/api/endpoints.md). One page per tag (Advertisers,
Campaigns, Banners, Targeting Rules, …), regenerated from the spec, so it always matches
what Swagger UI shows. Use the hand-written pages below for task-oriented walkthroughs
with `curl` examples, and the per-tag pages when you need the full parameter table of a
specific endpoint.

> **Note:** the spec declares two further groups — **Mobile Networks** and **Referral
> Program** — that carry no operations. The referral reads under `/rest-api/referral-program/`
> exist and are covered in [Network services and miscellaneous APIs](https://epom.com/ad-server/docs/api/network-and-misc.md),
> but they are not in the spec, so Swagger UI, the per-tag reference and the AI assistant's
> `api_search` / `api_read` tools do not list them.

## API quotas: how many requests can I make per day?

Only **analytics reports** are metered. Creating, editing, switching on/off, linking and
changing limits, targeting or pricing through the API are not counted against any quota.

| Quota | Default | Applies to | When exceeded |
|---|---|---|---|
| **Daily requests per user limit** | 100 reports per user per day | `/rest-api/analytics/...` report calls, counted per API user | `429` with `Daily requests limit reached.` until the server's next calendar day |
| **Records per request limit** | 10,000 rows | One report call; also the row count you get when `limit` is left out | A `limit` above it is a `400`; page with `offset` instead |

How to check where you stand: `GET /rest-api/network/settings.do` returns
`analyticsApi.dailyRequestLimit`, `analyticsApi.requestsUsedToday` (your own count) and
`analyticsApi.recordsPerRequestLimit`. Only a report that actually runs is counted — a call
refused with `400`, `401` or `403` costs nothing.

**When the count resets.** At midnight of the **ad server's own clock** — the time zone the
server runs in, which is neither your network's reporting time zone nor yours. `requestsUsedToday`
falls back to 0 at that moment, which shows you when it happens; Epom support can tell
you the server time zone.

**Raising the quota.** Both values (*Daily requests per user limit*, *Records per request limit*)
are network-wide settings; ask Epom support or your account manager to raise them.

**Staying under it.** Ask for one report grouped by the dimension you need
(`groupBy=PLACEMENT&groupRange=DAY`) instead of one call per entity, pull yesterday once a
day instead of polling, and use a separate API user per integration — the count is per user.
See [Automate reports](https://epom.com/ad-server/docs/guides/analytics/automate-reports.md).

## Statistics by my own parameter (creator id, sub-id) through the API

Reports can only group by what the ad request carried, so the value has to be in the tag
first — see [Ad request parameters](https://epom.com/ad-server/docs/reference/invocation-codes.md#ad-request-parameters-key-ch-cp-o-origin-requesturl-consent).
Two ways, depending on how many labels you need:

| You pass in the tag | Group the report by | Report type |
|---|---|---|
| `ch=<value>` (`data-channel`) — one label per request | `groupBy=CHANNEL` (filter: `eqStr[CHANNEL]=<value>`) | General (default) and Advanced |
| `cp.<name>=<value>` (`data-cp-<name>`) — any number of named parameters | `statisticType=HADOOP&groupBy=CUSTOM_PARAMETER&actionCustomParameterFragment=<name>` — rows per value of that one parameter | Advanced only |

```bash
# Yesterday's impressions per creator, per country, from the Advanced report
curl -G "https://<your-network-domain>/rest-api/analytics/JSON.do" -H "X-Api-Key: $APIKEY" \
  --data-urlencode "statisticType=HADOOP" \
  --data-urlencode "range=YESTERDAY" --data-urlencode "groupBy=CUSTOM_PARAMETER" \
  --data-urlencode "groupBy=COUNTRY" --data-urlencode "actionCustomParameterFragment=creator_id"
```

A grouping that comes back empty almost always means the requests did not carry the
parameter (a VAST URL or a passback that drops it, a different tag version on the page) —
check one live request URL before suspecting the report. Full parameter list:
[Analytics reports via API](https://epom.com/ad-server/docs/api/analytics-api.md).

## Usage expectations

- Reference data (targeting values, categories, ad units) changes rarely — fetch it once
  and cache it rather than per request.
- The one-time login token expires in 60 seconds; request it immediately before use.

## Related

- [Entities: create, read, update, delete](https://epom.com/ad-server/docs/api/entities-crud.md)
  - [Advertisers and campaigns via API](https://epom.com/ad-server/docs/api/entities-advertisers-campaigns.md)
  - [Banners (creatives) via API](https://epom.com/ad-server/docs/api/entities-banners.md)
  - [Sites, zones and placements via API](https://epom.com/ad-server/docs/api/entities-sites-zones-placements.md)
  - [Linking, status, tracking codes and ad tags via API](https://epom.com/ad-server/docs/api/entities-linking-status-tags.md)
- [Targeting, capping and limits](https://epom.com/ad-server/docs/api/targeting-capping-limits.md)
  - [Targeting rules via API](https://epom.com/ad-server/docs/api/targeting-api.md)
  - [Frequency capping via API](https://epom.com/ad-server/docs/api/capping-api.md)
  - [Delivery limits via API](https://epom.com/ad-server/docs/api/limits-api.md)
  - [Price rules, bid floors and pricing via API](https://epom.com/ad-server/docs/api/price-rules-and-floors-api.md)
- [Analytics reports via API](https://epom.com/ad-server/docs/api/analytics-api.md)
- [Network services and miscellaneous APIs](https://epom.com/ad-server/docs/api/network-and-misc.md)
- [API endpoint reference by tag](https://epom.com/ad-server/docs/api/endpoints.md) — generated, exhaustive per-endpoint tables
- [Connect an AI assistant (MCP)](https://epom.com/ad-server/docs/api/mcp.md) — the same API through Claude or another MCP client
- [Glossary](https://epom.com/ad-server/docs/reference/glossary.md)