Browse the help center

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.

Reference network manager admin

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:

MethodPathAuth
GET/POST/rest-api/analytics/{format}.doYour 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}.doHash 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.

statisticTypeReportWhat it returnsOver REST
(omitted)GeneralDaily and hourly totalsWorks — this is the default
HADOOPAdvancedTotals broken down by what each ad request carriedWorks
IMPRESSIONS_LOGImpressions LogOne row per impressionWorks
CLICKS_LOGClicks LogOne row per clickWorks
CONVERSIONS_LOGConversions LogOne row per conversionWorks
ACTIONS_LOGActions LogOne row per tracked actionWorks — but see the note below
TARGETING_COOKIETargeting CookieDaily totalsWorks
HEADER_BIDDINGHeader BiddingDaily and hourly totalsWorks — 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, gross and ecpmGross (the money columns need the Gross permissions) — and its own dimensions: group by FILL_SOURCE, COUNTRY, SITE, ZONE and PLACEMENT, and filter with eqStr[FILL_SOURCE], eqStr[COUNTRY] and eqLong on SITE, ZONE or PLACEMENT. It returns the same figures as the report in Account → Analytics. A publisher sees the fills on their own inventory only.

Note: ACTIONS_LOG is 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:

ParameterValues / formatNotes
statisticTypeSee Report typesOmit for the General report
rangeTODAY (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, CUSTOM14 values. CUSTOM reads the period from customFrom / customTo. Each range has its own reporting permission
customFrom, customToyyyy-MM-ddOnly with range=CUSTOM; both default to today, so CUSTOM with only customFrom reports up to today. customTo before customFrom is a customFrom-customTo error
groupRangeNONE (default), HOUR, DAY, WEEK, MONTH, YEAR, HOUR_24Time bucketing; HOUR_24 groups by hour-of-day across days
hourFrom, hourTo0–23Only applied when groupRange=HOUR; hourFrom must not exceed hourTo. A value outside 0–24 is a 400 on that field. Send the two together
usersTimeZoneIdA 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?
groupByRepeatable or comma-separated — see Grouping
eqStr, eqLongFilter maps — see Filters
archivebooleanZips the download: the response is export.<format>.zip holding export.<format>. It does not select older or archived data
offset, limitintegersPagination; 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
hadoopRefDomaintextAdvanced (HADOOP) only: count only requests whose referrer URL contains this text
hadoopAppPackagetextAdvanced only: count only in-app requests whose app package / bundle id contains this text
hadoopCustomParmName, hadoopCustomParmValuetextAdvanced 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:

ParameterValues / formatNotes
displayIdsbooleanAdds entity IDs next to names
showUniquebooleanAdds 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
useReportsLayoutSettingsboolean, default falseWhen 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
reportsLayoutSettingsRepeatable column ids, e.g. impressions, ctr, fill_rate — see Report column layoutApplied 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
splitColumnsByNONE, CONVERSION_ACTION, CUSTOM_PARAMETER, ATTRIBUTION_TYPEThe single dimension spread across columns. Not available on the Advanced report
splitByAttributionbooleanNests a click-through / view-through pair under each conversion-action column. Only meaningful with splitColumnsBy=CONVERSION_ACTION
actionIdsRepeatable or comma-separated conversion action idsRestricts the conversion columns — and the per-action columns of splitColumnsBy=CONVERSION_ACTION — to these actions. Omitted: every action you may see
breakdownByActionsbooleanOne conversions column per conversion action. Ignored when splitColumnsBy is set
breakdownByCustomParameterbooleanBreak rows down by custom parameter values
actionCustomParameterFragment, actionCustomValueFragmentstringsFilter by custom parameter name / value fragment
jsonKeysname (default), idJSON only: how each row names its fields — see JSON output. Other formats ignore it

Corner case: splitColumnsBy takes precedence over breakdownByActions / 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=50 is 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 with limit=50 returns 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: groupBy is 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, add displayIds=true so rows are unambiguous.

Corner case: a groupBy value is refused for one of two reasons, and the answer says which. A grouping your role lacks the permission for answers 403 with permissionDenied: true and errors.forbidden naming 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 answers 400 with errors.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:

MapField typeFields it accepts
eqLong[FIELD]=<id>Numeric entity IDsBANNER, CAMPAIGN, ADVERTISER, PLACEMENT, ZONE, SITE
eqStr[FIELD]=<string>String valuesCHANNEL, 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_ID filters 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 further eqStr fields — its own device, browser, risk, language, connection-type and traffic-source dimensions, for example DEVICE_VENDOR, RISK_LEVEL, CONNECTION_TYPE and LANGUAGE — 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 -G for 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:

MethodPathPurpose
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}.doAll 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. saveReportsLayoutSettings is 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 the X-Ignored-Report-Columns response header (comma-separated, URL-encoded). Dashboard widgets and saved reports, which store the list, refuse it with 400, success:false and the unknown ids under errors — reportsLayoutSettings on a saved report, data.reportsLayoutSettings (and data.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.

ParameterRequiredPurpose
from, toyesRange in yyyy-MM-dd, both days inclusive. to before from is 400.
groupBynoComma-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.
sortnoAny dimension you grouped by, or requests, bids, wins, impressions, spend.
dirnoasc (default) or desc.
startnoRow offset, default 0. Negative is 400.
limitnoDefault 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 429 with errors["Too Many Requests"]: Daily requests limit reached.; a limit above the record-count limit answers 400 with errors.limit naming 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.do under analyticsApi (dailyRequestLimit, requestsUsedToday, recordsPerRequestLimit); the AI assistant’s whoami carries the same block and its analytics_report answer 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 403 naming the permission, e.g. Your role lacks the RPT_GRP_FILTER_CAMPAIGN_BANNER permission. A group-by the report type does not have is a 400 under errors.groupBy, not a 403 (see Grouping). Other validation errors answer 400 with the field in errors, and a failed signature or key 401.
  • A report is counted only once it is known to run: a call refused with 400, 401 or 403 does 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 limit a report returns up to the records-per-request limit (recordsPerRequestLimit in GET /rest-api/network/settings.do); page with offset/limit rather 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 obsolete after five minutes.

Related pages

Please disable Adblock
Are you using adblock? Disable it and reload the page to proceed to the destination page.

A registration has already been submitted from this page. Please try again in 10 minutes.