Analytics reports via API
Pull reports over the API — the report types the REST path serves, formats, date ranges, group-by, eqStr/eqLong filter maps, pagination, exports, column ids, saved reports and DSP spend, with the refusals and the daily quota.
Reports you run in Account → Analytics can also be pulled over the API, in five output
formats. This page documents the report endpoints, their filter model, and which report types
the REST path serves. Examples authenticate with an X-Api-Key header or with the hash in the
path; see the API overview for both. All report parameters, with descriptions and
an example request:
GET /rest-api/analytics/{format}.do.
Endpoints
Four operations of the Analytics group — two paths, each on GET and POST — are the report engine itself, in two flavors that differ only in how you authenticate:
| Method | Path | Auth |
|---|---|---|
| GET/POST | /rest-api/analytics/{format}.do | Your API key in the X-Api-Key header (or Authorization: Bearer <key>). A call with no key, or a key that matches no one, is a 401 |
| GET/POST | /rest-api/analytics/{format}/{login}/{hash}/{timestamp}.do | Hash auth in the path |
A client that cannot set a header may pass the key as an apiKey query parameter instead; this
endpoint is the only one that accepts that fallback, and a header wins when both are sent. A
deactivated account is refused on both flavors — 401, Account is not active under
rest_error.
{format} is JSON, CSV, XLSX, PDF or HTML. Both flavors accept exactly the same
report parameters, on both GET and POST; nothing is available on one and not the other.
For the hash flavor, hash = MD5( MD5(password) + timestamp ) and timestamp is Unix time
in milliseconds, valid within ±5 minutes — same rules as everywhere else, just placed in
the path. See the worked example.
# API-key flavor, JSON, last 7 days grouped by day
curl -H "X-Api-Key: $APIKEY" "https://<your-network-domain>/rest-api/analytics/JSON.do?range=LAST_7_DAYS&groupRange=DAY"
# Hash flavor, CSV download
curl -o report.csv \
"https://<your-network-domain>/rest-api/analytics/CSV/john.doe/$HASH/$TS.do?range=LAST_MONTH&groupBy=SITE"
The rest of the Analytics group is the column-id list and the layout-settings reader, the DSP
spend report and saved reports — each covered in its own section below — plus
two diagnostics, GET /rest-api/analytics/report-check.do and
GET /rest-api/analytics/raw-log-analyze.do, documented in the
generated reference.
Report types
statisticType selects the report. The REST path serves every value below, and omitting
the parameter is itself a report type — the default is the General report, not the
Advanced one.
statisticType | Report | What it returns | Over REST |
|---|---|---|---|
| (omitted) | General | Daily and hourly totals | Works — this is the default |
HADOOP | Advanced | Totals broken down by what each ad request carried | Works |
IMPRESSIONS_LOG | Impressions Log | One row per impression | Works |
CLICKS_LOG | Clicks Log | One row per click | Works |
CONVERSIONS_LOG | Conversions Log | One row per conversion | Works |
ACTIONS_LOG | Actions Log | One row per tracked action | Works — but see the note below |
TARGETING_COOKIE | Targeting Cookie | Daily totals | Works |
HEADER_BIDDING | Header Bidding | Daily and hourly totals | Works — see the note below |
Three legacy names are accepted and silently mapped to HADOOP: HADOOP_ADREQUESTS,
BROWSER_OS and HADOOP_SECURITY. They exist so bookmarked URLs and saved widgets keep
working; use HADOOP in new code.
Note: the Header Bidding report has its own columns —
fills,unfilled,hbFillRate,grossandecpmGross(the money columns need the Gross permissions) — and its own dimensions: group byFILL_SOURCE,COUNTRY,SITE,ZONEandPLACEMENT, and filter witheqStr[FILL_SOURCE],eqStr[COUNTRY]andeqLongonSITE,ZONEorPLACEMENT. It returns the same figures as the report in Account → Analytics. A publisher sees the fills on their own inventory only.
Note:
ACTIONS_LOGis the one report type the API serves that the UI’s report-type picker never offers.
Each report type is also gated by its own reporting permission — the Analytics Report Type …
group in the role editor (ACTIONS_LOG uses the Conversions Log one) — and the report is
refused with 403 naming it, for example Your role lacks the RPT_TYPE_HADOOP permission.
A report with no statisticType needs the General report permission. A type that works in
principle can therefore still be refused for the calling user.
Report parameters
The report parameters are identical on all four report operations. Time and scope first:
| Parameter | Values / format | Notes |
|---|---|---|
statisticType | See Report types | Omit for the General report |
range | TODAY (default), YESTERDAY, CURRENT_WEEK, LAST_WEEK, LAST_2_WEEKS, CURRENT_MONTH, LAST_7_DAYS, LAST_30_DAYS, LAST_MONTH, LAST_3_MONTH, LAST_6_MONTH, CURRENT_YEAR, LAST_YEAR, CUSTOM | 14 values. CUSTOM reads the period from customFrom / customTo. Each range has its own reporting permission |
customFrom, customTo | yyyy-MM-dd | Only with range=CUSTOM; both default to today, so CUSTOM with only customFrom reports up to today. customTo before customFrom is a customFrom-customTo error |
groupRange | NONE (default), HOUR, DAY, WEEK, MONTH, YEAR, HOUR_24 | Time bucketing; HOUR_24 groups by hour-of-day across days |
hourFrom, hourTo | 0–23 | Only applied when groupRange=HOUR; hourFrom must not exceed hourTo. A value outside 0–24 is a 400 on that field. Send the two together |
usersTimeZoneId | A fixed-offset zone name: UTC_0, UTC_PLUS_2, UTC_MINUS_5 … (UTC_MINUS_12 to UTC_PLUS_14) | Advanced (HADOOP) reports only; default UTC. Not an IANA ID — an unknown value silently means UTC, and there is no daylight-saving switch. See Which time zone do reports use? |
groupBy | Repeatable or comma-separated — see Grouping | |
eqStr, eqLong | Filter maps — see Filters | |
archive | boolean | Zips the download: the response is export.<format>.zip holding export.<format>. It does not select older or archived data |
offset, limit | integers | Pagination; offset defaults to 0. Omit limit and you get up to the network’s Records per request limit (10,000 by default); a larger limit is a 400. See the corner case below |
hadoopRefDomain | text | Advanced (HADOOP) only: count only requests whose referrer URL contains this text |
hadoopAppPackage | text | Advanced only: count only in-app requests whose app package / bundle id contains this text |
hadoopCustomParmName, hadoopCustomParmValue | text | Advanced only: count only requests carrying this custom parameter (the name the ad tag passes), and with a value, only where it has that value |
Then the ones that shape the columns rather than the rows:
| Parameter | Values / format | Notes |
|---|---|---|
displayIds | boolean | Adds entity IDs next to names |
showUnique | boolean | Adds unique visitor/click columns (Advanced reports, Unique view). Omitted, a unique column listed in reportsLayoutSettings asks for them as well — columns.do lists them for the Unique view; showUnique=false still leaves them out |
useReportsLayoutSettings | boolean, default false | When true, output only the metric columns listed in reportsLayoutSettings (grouping and date columns always stay). It does not load the layout saved in the UI. When false, every metric column your role may see is returned |
reportsLayoutSettings | Repeatable column ids, e.g. impressions, ctr, fill_rate — see Report column layout | Applied only with useReportsLayoutSettings=true; an id that is no column of the report type is dropped, whether or not it would be applied, and named in the X-Ignored-Report-Columns response header — the report still runs |
splitColumnsBy | NONE, CONVERSION_ACTION, CUSTOM_PARAMETER, ATTRIBUTION_TYPE | The single dimension spread across columns. Not available on the Advanced report |
splitByAttribution | boolean | Nests a click-through / view-through pair under each conversion-action column. Only meaningful with splitColumnsBy=CONVERSION_ACTION |
actionIds | Repeatable or comma-separated conversion action ids | Restricts the conversion columns — and the per-action columns of splitColumnsBy=CONVERSION_ACTION — to these actions. Omitted: every action you may see |
breakdownByActions | boolean | One conversions column per conversion action. Ignored when splitColumnsBy is set |
breakdownByCustomParameter | boolean | Break rows down by custom parameter values |
actionCustomParameterFragment, actionCustomValueFragment | strings | Filter by custom parameter name / value fragment |
jsonKeys | name (default), id | JSON only: how each row names its fields — see JSON output. Other formats ignore it |
Corner case:
splitColumnsBytakes precedence overbreakdownByActions/breakdownByCustomParameter, which it derives, so a report cannot ask for two column splits at once. Money columns are rounded to the network’s analytics decimal precision; a request cannot change it.
Corner case: on the report endpoints
limit=50is read as “no limit given”. 50 is the default when you omit the parameter, and the endpoint treats it as the omitted case: a request withlimit=50returns up to the network’s records-per-request limit, not 50 rows. Ask for 49 or 51 when you need a page of about that size.
Corner case:
groupByis checked on submit, not silently trimmed. A dimension your role lacks, or one the report type does not have, refuses the whole request instead of returning a report with those columns quietly missing — see Grouping for the two answers.
Grouping
groupBy is repeatable. Its schema accepts 68 dimension values, but being in that list does
not mean the report accepts it: the same value list serves group-by, both filter maps and the
search endpoint’s entities parameter, so it is a superset of what any single report offers.
Which dimension belongs to which report type is the subject of
Analytics dimensions; the generated reference lists
the dimensions per report type next to the parameter:
GET /rest-api/analytics/{format}.do.
Group by the entity’s name dimension (CAMPAIGN, not CAMPAIGN_ID — no report groups by the
_ID values) and add displayIds=true for the ids. Repeat the parameter for multi-level grouping:
curl -H "X-Api-Key: $APIKEY" "https://<your-network-domain>/rest-api/analytics/JSON.do?\
range=LAST_30_DAYS&groupBy=CAMPAIGN&groupBy=COUNTRY&groupRange=DAY&limit=1000"
Corner case: grouping by the name dimension (
CAMPAIGN) merges entities that share a name. If you post-process results programmatically, adddisplayIds=trueso rows are unambiguous.
Corner case: a
groupByvalue is refused for one of two reasons, and the answer says which. A grouping your role lacks the permission for answers403withpermissionDenied: trueanderrors.forbiddennaming the permission —Your role lacks the RPT_GRP_FILTER_ZONE_PLACEMENT permission needed to group by ZONE.A grouping the report type does not have answers400witherrors.groupBy—The General report does not group by DEVICE_FORMAT; the Advanced report (statisticType=HADOOP) does.— and no permission will make it work: send the report type that offers it. Neither is counted against the daily limit.
Filters: eqStr and eqLong
Filters are maps passed in bracket notation. The two maps are not interchangeable — each takes its own set of fields:
| Map | Field type | Fields it accepts |
|---|---|---|
eqLong[FIELD]=<id> | Numeric entity IDs | BANNER, CAMPAIGN, ADVERTISER, PLACEMENT, ZONE, SITE |
eqStr[FIELD]=<string> | String values | CHANNEL, COUNTRY, PLACEMENT_TYPE, BANNER_LABEL, PLACEMENT_LABEL, ACTION, BROWSER, OPERATING_SYSTEM, UNIVERSAL_AD_ID |
Both maps accept the full 68-value list as their keys, for the same reason as
groupBy; the fields above are the ones the General report filters on. Two notes:
UNIVERSAL_AD_IDfilters by the ISCI / Ad-ID code set on video creatives, and needs the Banner ISCI / Ad-ID Code permission. The raw-log reports resolve it to the creatives carrying that code.- The Advanced report (
statisticType=HADOOP) reads furthereqStrfields — its own device, browser, risk, language, connection-type and traffic-source dimensions, for exampleDEVICE_VENDOR,RISK_LEVEL,CONNECTION_TYPEandLANGUAGE— each behind its own reporting permission. Analytics dimensions says which dimension belongs to which report.
Repeat a key for OR-matching within the same field:
# US or Germany traffic on two specific sites
curl -H "X-Api-Key: $APIKEY" "https://<your-network-domain>/rest-api/analytics/JSON.do?\
range=YESTERDAY&groupBy=SITE&groupBy=COUNTRY\
&eqStr\[COUNTRY\]=US&eqStr\[COUNTRY\]=DE\
&eqLong\[SITE\]=100&eqLong\[SITE\]=101"
Different fields are combined with AND: the example above reads “(country US or DE) and (site 100 or 101)”.
Note: remember to escape (or quote) the square brackets in shells; curl also accepts
--data-urlencode 'eqStr[COUNTRY]=US'with-Gfor GET requests.
Raw event logs
Set statisticType to IMPRESSIONS_LOG, CLICKS_LOG, CONVERSIONS_LOG or ACTIONS_LOG to
receive raw event rows instead of aggregates — one row per impression, click or conversion,
with the same range, filter and pagination parameters. These are significantly heavier than
aggregated reports; keep date ranges short and always paginate.
curl -H "X-Api-Key: $APIKEY" "https://<your-network-domain>/rest-api/analytics/CSV.do?\
statisticType=CLICKS_LOG&range=YESTERDAY&eqLong\[CAMPAIGN\]=2001&limit=5000"
Raw-log results are cached per query on the server. A report whose numbers look frozen is usually a cache that has not been invalidated because no new log flush has landed — see Analytics discrepancies.
Report column layout
The metric columns a report can show are named by short ids (impressions, ctr, fill_rate,
sumGross…). Two reads tell you which ids you may use:
| Method | Path | Purpose |
|---|---|---|
| GET | /rest-api/analytics/columns.do?statisticType=… | The columns you may use for a report type — id, display name, and the picker group (BASIC, PUBLISHER, ADVERTISER) with its groupName — exactly what the Analytics column picker offers your role. Omit statisticType for the General report. Standard REST authentication (hash or X-Api-Key). 403 naming the report-type permission when your role cannot run that type |
| GET/POST | /rest-api/analytics/reports-layout-settings/{login}/{hash}/{timestamp}.do | All available analytics columns grouped by report type, plus your current selection (legacy path-hash authentication). Optional statsType narrows it to one report type |
To get a subset of columns, send useReportsLayoutSettings=true together with the column ids in
reportsLayoutSettings on the report call. The call does not read the layout you saved in the
UI — the ids you send are the whole selection. The same column ids are what saved reports take
in reportsLayoutSettings and dashboard widgets in data.reportsLayoutSettings /
data.choosenMetric.
Corner case: the layout is read-only over the API.
saveReportsLayoutSettingsis accepted but has no effect — writing the layout is only supported from the UI.
Corner case: an id that names no column of the report type for your role — a display name such as
Revenue, a typo, or a retired id — is handled differently per endpoint. The report endpoint (/rest-api/analytics/{format}…) drops it, runs the report and names the dropped ids in theX-Ignored-Report-Columnsresponse header (comma-separated, URL-encoded). Dashboard widgets and saved reports, which store the list, refuse it with400,success:falseand the unknown ids undererrors—reportsLayoutSettingson a saved report,data.reportsLayoutSettings(anddata.choosenMetric) on a widget.
JSON output
A JSON report is an array with one object per row. By default each field is named by the
column’s display name in your interface language — "Impressions" for an English-speaking
user, "Impressionen" for a German one — so the same script reads different keys depending on
whose account runs it.
Several columns share a display name in every language: the Base, Publisher and Advertiser
Impressions, Clicks, CTR, Requests, Fill rate, Conversions, CCR and their
post-click / post-view variants, and Gross and Net, which two column ids each carry. A row may only
carry each name once, so when your role sees more than one of them the last keeps the display
name and each earlier one is named by its column id instead:
[{"impressions":"1820","publisher_impressions":"1700","Impressions":"1650",
"clicks":"40","publisher_clicks":"38","Clicks":"37"}]
Here Impressions is the Advertiser figure — the value a JSON parser read under that name before
release 153, when the three were all sent as Impressions and only the last survived parsing.
For scripts, send jsonKeys=id: every field is then named by its column id (impressions,
publisher_impressions, advertiser_impressions, sumGross, site_name…), the same in every
language and never repeated. The ids are the ones Report column layout
lists. A saved report keeps jsonKeys like any other parameter, and its scheduled JSON e-mail
names its fields the same way.
curl -H "X-Api-Key: $APIKEY" "https://<your-network-domain>/rest-api/analytics/JSON.do?\
range=YESTERDAY&groupBy=SITE&displayIds=true&jsonKeys=id"
Note: CSV, XLSX, PDF and HTML show display names as column headers, and repeat a shared name, as the report page does. A CSV parser that keys rows by header name keeps only one of them — read CSV by column position, or use JSON with
jsonKeys=id.
DSP endpoint traffic and spend
What a demand partner’s endpoint was billed, in the shape invoicing needs — one row per endpoint instead of one per creative:
GET /rest-api/rtb/dsp-analytics/search.do
Standard REST authentication — the hash in the query string, or an X-Api-Key header. The
caller needs the DSP Partners Analytics permission; without it the call is 403 naming
RTB_DSP_PARTNERS_ANALYTICS, not an empty result.
| Parameter | Required | Purpose |
|---|---|---|
from, to | yes | Range in yyyy-MM-dd, both days inclusive. to before from is 400. |
groupBy | no | Comma-separated dimensions: date, dspPartnerName, placementId, countryCode. An unknown name is 400 rather than a silently ignored breakdown. Omit it for a single total row over the range. |
sort | no | Any dimension you grouped by, or requests, bids, wins, impressions, spend. |
dir | no | asc (default) or desc. |
start | no | Row offset, default 0. Negative is 400. |
limit | no | Default 1000, must be between 1 and 10000; outside that is 400. |
Rows carry the dimensions you asked for and always carry the metrics — requests, bids,
wins, impressions, clicks, spend. Dimensions you did not group by are aggregated over
and come back null.
countryCode is null for a second reason too — when the ad request’s country could not be
resolved. Under groupBy=countryCode a null therefore means unknown country, not
aggregated.
curl -G 'https://<your-network-domain>/rest-api/rtb/dsp-analytics/search.do' \
--data-urlencode 'from=2026-08-01' \
--data-urlencode 'to=2026-08-31' \
--data-urlencode 'groupBy=dspPartnerName' \
--data-urlencode 'sort=spend' --data-urlencode 'dir=desc' \
--data-urlencode "username=$USER_NAME" \
--data-urlencode "timestamp=$TS" --data-urlencode "hash=$HASH"
spend is the gross booked on the delivery that endpoint won, so it matches the Gross a
standard report shows for the partner’s OpenRTB creatives. It follows the partner’s pricing
model — impressions for CPM and CPI, clicks for CPC — which is why both counts come back with
it, and it counts delivery rather than auction wins. See
What Spend counts.
Saved reports
Report templates, their schedules and their recipients are managed under
/rest-api/saved-reports — list and create, read, update
and delete one, add and remove recipients, and set and clear the schedule. The operations,
their parameters and the refusals are described in
Saved and scheduled reports.
Self-serve dashboard analytics
Self-serve advertiser dashboards have a dedicated aggregate endpoint (hash auth in path,
Dashboard tag rather than Analytics):
GET/POST /rest-api/self-serv/dashboard/{login}/{hash}/{timestamp}.do
It takes the report parameters except archive, displayIds and showUnique, and returns current-period KPIs (impressions, clicks, revenue, conversions,
fill rate, CTR, eCPM) against the equivalent previous period. Dashboard widget management
endpoints are covered in
Network services and miscellaneous APIs.
Limits and performance
- Two ceilings apply, both network settings that Epom support changes on request: Daily
requests per user limit (default 100) and
Records per request limit (default 10,000). The daily limit answers
429witherrors["Too Many Requests"]: Daily requests limit reached.; alimitabove the record-count limit answers400witherrors.limitnaming the ceiling. When you hit them, back off and batch — don’t poll per entity. - The daily count is kept per user and starts again each day by the server’s date, not the
network’s reporting time zone. Your own count and limit are in
GET /rest-api/network/settings.dounderanalyticsApi(dailyRequestLimit,requestsUsedToday,recordsPerRequestLimit); the AI assistant’swhoamicarries the same block and itsanalytics_reportanswer says how many requests are left. - A report the role may not run — a report type, date range, group-by or filter whose
reporting permission it lacks — answers
403naming the permission, e.g.Your role lacks the RPT_GRP_FILTER_CAMPAIGN_BANNER permission.A group-by the report type does not have is a400undererrors.groupBy, not a403(see Grouping). Other validation errors answer400with the field inerrors, and a failed signature or key401. - A report is counted only once it is known to run: a call refused with
400,401or403does not spend the daily quota, because the checks happen first. A call that is accepted and then fails on the server (500) has already been counted. - Without
limita report returns up to the records-per-request limit (recordsPerRequestLimitinGET /rest-api/network/settings.do); page withoffset/limitrather than requesting everything. - Reports reflect processed log data, not the live counter — very recent traffic (typically the last minutes) may not appear yet. For discrepancy questions see Analytics discrepancies.
Corner case: timestamps in the hash-auth path are validated within ±5 minutes. Long-running export scripts must compute the hash per request, not once at startup, or requests start failing with
Request is obsoleteafter five minutes.