Publisher insights
GET /v1/publisher/insights returns your performance on AI Surfaces, on the
web or on UGC for a date range: delivered impressions, clicks and estimated
earnings, plus requests and fill on AI Surfaces and UGC, viewed impressions on
AI Surfaces and the web, and page views on the web. Rows come by day, by month
or as one total, and can be split by country and US state.
| Endpoint | Auth | Purpose |
|---|---|---|
GET /v1/publisher/insights | your AI API key, or a reporting key | Your reporting for a date range |
Reporting is enabled per account. If your key gets 403 reporting_not_enabled,
ask your Adzen account representative to turn it on.
Request
GET https://api.adzen.ai/v1/publisher/insightsThe path with a trailing slash (/v1/publisher/insights/) also works.
Headers
| Header | Required | Value |
|---|---|---|
X-API-Key | One of X-API-Key or Authorization | Your key (see below) |
Authorization | One of X-API-Key or Authorization | Bearer <key>. If you send both headers, Adzen uses X-API-Key. |
Which key you use depends on your surface:
- AI Surfaces: the key you use for
POST /v1/ai/process. - Web and UGC: a read-only reporting key. Ask your Adzen account
representative for one. It works on this endpoint only. A UGC partner’s
/v1/ugckey doesn’t work here, and the reporting key doesn’t work there.
You read only the data of the account that owns the key. An account is on one
surface, and meta.surface says which (ai, web or ugc). This endpoint rejects
any parameter not listed below with 400 unknown_parameter.
Parameters
| Parameter | Required | Values | Default |
|---|---|---|---|
start_date | yes | YYYY-MM-DD, in timezone | |
end_date | yes | YYYY-MM-DD, in timezone, inclusive | |
timezone | no | An IANA zone whose offset from UTC is a whole number of hours, such as America/New_York. Other zones return unsupported_timezone. | UTC |
granularity | no | day, month, total | day |
breakdowns | no | country, region, or country,region | none |
- The range is at most 93 days, and
end_datecan’t be after the current date intimezone. - Only an omitted parameter takes its default. A parameter sent with an empty
value is an error:
timezone=returnsunsupported_timezone,start_date=orend_date=returnsinvalid_date, andbreakdowns=returnsinvalid_breakdown. - Days and months are local to
timezone. A day that changes clock time has 23 or 25 hours, and still comes back as one row. monthrows are clamped to your range: a range from the 13th to the 29th returns one row with those 17 days.- Without
breakdowns,totalreturns exactly one row, even when every count is zero. Withbreakdowns,totalreturns one row per country or region that has activity, and one all-zero row with the breakdown fields set tonullwhen there is no activity. breakdowns=regionadds bothcountryandregion. Values inbreakdownscan come in any order, with spaces around the comma.
Example
curl -G https://api.adzen.ai/v1/publisher/insights \
-H 'X-API-Key: YOUR_API_KEY' \
-d start_date=2026-10-22 \
-d end_date=2026-10-24 \
-d granularity=totalResponse
{
"data": [
{
"page_views": null,
"ad_requests": 6,
"blocked_requests": 1,
"eligible_requests": 5,
"matched_requests": 4,
"fill_rate": 0.666667,
"eligible_fill_rate": 0.8,
"impressions": 6,
"viewed_impressions": 3,
"clicks": 2,
"invalid_impressions": 1,
"invalid_clicks": 1,
"ctr": 0.333333,
"viewed_ctr": 0.666667,
"estimated_revenue_micros": 1575000,
"rpm_micros": 262500000,
"ecpm_micros": 262500000,
"epc_micros": 787500
}
],
"meta": {
"surface": "ai",
"start_date": "2026-10-22",
"end_date": "2026-10-24",
"timezone": "UTC",
"currency": "USD",
"data_available_end": "2026-10-30T00:35:01.020458Z",
"first_incomplete_date": null,
"warnings": []
}
}With granularity=day each row also has date (YYYY-MM-DD); with month, a
month (YYYY-MM). With breakdowns=region, each row has country and
region:
{
"date": "2026-10-22",
"country": "US",
"region": "US-CA",
"page_views": null,
"ad_requests": 1,
"blocked_requests": 0,
"eligible_requests": 1,
"matched_requests": 1,
"fill_rate": 1.0,
"eligible_fill_rate": 1.0,
"impressions": 2,
"viewed_impressions": 2,
"clicks": 1,
"invalid_impressions": 1,
"invalid_clicks": 0,
"ctr": 0.5,
"viewed_ctr": 0.5,
"estimated_revenue_micros": 875000,
"rpm_micros": 875000000,
"ecpm_micros": 437500000,
"epc_micros": 875000
}On the web the same row has page_views, and the request funnel is null
(web pages don’t send ad requests the way an AI integration does):
{
"date": "2026-10-18",
"country": "US",
"region": "US-CA",
"page_views": 3,
"ad_requests": null,
"blocked_requests": null,
"eligible_requests": null,
"matched_requests": null,
"fill_rate": null,
"eligible_fill_rate": null,
"impressions": 1,
"viewed_impressions": 1,
"clicks": 1,
"invalid_impressions": 1,
"invalid_clicks": 0,
"ctr": 1.0,
"viewed_ctr": 1.0,
"estimated_revenue_micros": 611525,
"rpm_micros": 203841666,
"ecpm_micros": 611525000,
"epc_micros": 611525
}On UGC the row has the request funnel, no page views, and no viewed impressions (see Viewed impressions). With a location breakdown, requests are in the row with no location, because a request has no viewer:
{
"date": "2026-10-28",
"country": "US",
"region": "US-CA",
"page_views": null,
"ad_requests": 0,
"blocked_requests": 0,
"eligible_requests": 0,
"matched_requests": 0,
"fill_rate": null,
"eligible_fill_rate": null,
"impressions": 4,
"viewed_impressions": null,
"clicks": 1,
"invalid_impressions": 1,
"invalid_clicks": 1,
"ctr": 0.25,
"viewed_ctr": null,
"estimated_revenue_micros": 500000,
"rpm_micros": null,
"ecpm_micros": 125000000,
"epc_micros": 500000
}Rows
- With
dayormonth, a row comes back only for a period, country and region that has at least one event. A day with no activity has no row. Treat a missing row as zero. - Rows are sorted by period, oldest first, then by
country, then byregion. Anullcountryorregionsorts first. - A response is at most 20,000 rows. A request that would return more fails
with
400 too_many_rows. Narrow the date range or the breakdowns.
Fields
Every row has every field. A field that doesn’t apply to your surface is
null, never 0:
| Field | AI Surfaces | Web | UGC |
|---|---|---|---|
page_views | null | counted | null |
ad_requests … eligible_fill_rate | counted | null | counted |
viewed_impressions, viewed_ctr | counted | counted | null |
| everything else | counted | counted | counted |
Counts
| Field | What it counts |
|---|---|
page_views | Web only: valid page views on your sites (pages where the Adzen tag ran). |
ad_requests | AI Surfaces: unique POST /v1/ai/process requests. Requests that share an Idempotency-Key count once, dated by the first attempt. A request sent without Idempotency-Key counts on its own, so a retry without the same key counts again. See Idempotency and retries. UGC: every POST /v1/ugc/process call Adzen received, retries included. Test traffic is left out. |
blocked_requests | AI Surfaces: requests not served because location was missing or did not start with US- (case-sensitive, for example US-CA). A request that was retried and then served counts as served, not blocked. UGC: requests Adzen couldn’t evaluate (an invalid request, a disabled account, or an error on Adzen’s side). |
eligible_requests | Requests sent to matching. |
matched_requests | Requests that got an ad in that response. |
impressions | Valid delivered impressions: render beacons received (AI Surfaces), ad units rendered on the page (web), impression pixels fired when the post displayed the ad (UGC). |
viewed_impressions | Valid viewed impressions. See Viewed impressions. |
clicks | Valid clicks. |
invalid_impressions | Impressions judged invalid (bots, blocked IPs, duplicates). Not included in impressions. |
invalid_clicks | Clicks judged invalid. Not included in clicks and never earn. |
Rates. Fractions from 0 to 1, rounded to 6 places. A rate is null, not 0,
when its denominator is 0.
| Field | Formula |
|---|---|
fill_rate | matched_requests / ad_requests |
eligible_fill_rate | matched_requests / eligible_requests |
ctr | clicks / impressions |
viewed_ctr | clicks / viewed_impressions |
Earnings. All money is in micros of USD (1 USD = 1,000,000 micros), rounded down.
| Field | Formula |
|---|---|
estimated_revenue_micros | Your share of what advertisers paid for this traffic |
rpm_micros | Earnings per 1,000 ad_requests (AI Surfaces, UGC) or page_views (web) |
ecpm_micros | Earnings per 1,000 impressions |
epc_micros | Earnings per click |
rpm_micros, ecpm_micros and epc_micros are null when their denominator
is 0 or earnings aren’t available.
Location
country (ISO 3166-1, such as US) and region (ISO 3166-2, such as US-CA)
are set as follows:
- AI Surfaces: from the
locationyou sent onPOST /v1/ai/process. Alocationthat Adzen can’t parse givescountryalone, or neither. Blocked requests have no location, so their rows havecountryandregionset tonull. - Web: from the visitor’s connection, as the Adzen edge sees it.
regionis set for US visitors only. Traffic from before Adzen began recording location, and the rare visitor Adzen can’t place, is in rows withcountryandregionset tonull. Thelocation_partialwarning says when your window reaches back that far. - UGC: from the viewer’s connection, as on the web, for impressions,
clicks and earnings. Requests have no viewer, so with a breakdown every
request is in the rows with
countryandregionset tonull. Viewer location is recorded from November 4, 2026. Earlier traffic is in thenullrows, andlocation_partialsays so.
On every surface the rows of a breakdown add up to the totals.
Dating rules
- Requests are dated when you sent them (AI Surfaces: the first attempt). Page views (web) are dated when the page was viewed.
- UGC data starts on September 11, 2026. Earlier days return no rows.
- Impressions and clicks are dated when they happened, so a click can land on a later day than its request.
- Earnings are dated by the event that was charged: the click for CPC campaigns, the viewed impression for CPM campaigns.
meta
| Field | Meaning |
|---|---|
surface | ai, web or ugc: the surface your account is on |
start_date, end_date, timezone | The window you asked for, as resolved |
currency | Always USD |
data_available_end | UTC time up to which the data covers every event. Events after this time can be missing. null when Adzen can’t determine it. Counts before this time can still change until they pass first_incomplete_date. |
first_incomplete_date | The earliest date in your window that ended less than 72 hours ago, or null. Counts on or after it can still change. |
warnings | {code, message} objects, listed below |
| Warning | Meaning |
|---|---|
revenue_not_enabled | Earnings aren’t enabled for your account. Earnings fields are null. |
revenue_unavailable | Earnings couldn’t be computed for this call. Earnings fields are null; counts are complete. Retry later. |
data_delayed | Recent data is delayed; check data_available_end. |
location_partial | Web and UGC: your breakdown reaches back before visitor location was recorded. That traffic is in the rows with a null country. |
no_sites | Web: no sites are registered to your account yet, so there is nothing to report. |
Finality and estimates
Data refreshes about every 15 minutes. meta.data_available_end gives the
point the data covers.
Counts can change for 72 hours after a day ends, as late events arrive and
invalid traffic is removed. Pull recent days again until they are past
first_incomplete_date.
estimated_revenue_micros is not your payout. Your payout comes from the monthly
statement, which can differ after invalid-traffic review and adjustments.
Earnings are computed per row and rounded down, so breakdown rows can add up
to a little less than the total row (at most 1 micro per row). Counts always
add up exactly.
Viewed impressions
- Web: the Adzen tag measures it. An ad counts as viewed once any part of it has been on screen for one continuous second.
- AI Surfaces:
viewed_impressionscounts the view beacons Adzen receives. See Impression beacons for when a view beacon fires. - UGC: there is one impression pixel, fired when the post displays the ad.
It isn’t a viewability measurement, so
viewed_impressionsandviewed_ctrarenull. Useimpressionsandctr.
Errors
Every error has the same shape. param is null when no single parameter
caused the error.
{
"error": {
"type": "invalid_request_error",
"code": "range_too_large",
"message": "the date range is limited to 93 days",
"param": "end_date"
}
}| Status | type | code | When | Retry? |
|---|---|---|---|---|
400 | invalid_request_error | invalid_date | A date is missing, empty, malformed, out of order, or after the current date | No |
400 | invalid_request_error | range_too_large | The range is over 93 days | No |
400 | invalid_request_error | unsupported_timezone | Unknown or empty zone, or an offset from UTC that is not a whole number of hours | No |
400 | invalid_request_error | invalid_granularity | Not day, month or total | No |
400 | invalid_request_error | invalid_breakdown | Empty, not country / region, or repeated | No |
400 | invalid_request_error | unknown_parameter | A parameter this endpoint doesn’t take | No |
400 | invalid_request_error | duplicate_parameter | The same parameter twice | No |
400 | invalid_request_error | too_many_rows | The response would be over 20,000 rows; narrow the range or the breakdowns | No |
401 | authentication_error | missing_api_key | No key | No |
401 | authentication_error | invalid_api_key | Unknown key, or a key for another product | No |
403 | permission_error | account_disabled | The account is disabled | No |
403 | permission_error | reporting_not_enabled | Reporting isn’t enabled for this account | No |
403 | permission_error | surface_not_supported | Reporting isn’t available for this account’s surface yet | No |
403 | Not JSON | Not JSON | The request came from an IP address outside the US | No |
404 | not_found_error | not_found | An unknown path under /v1/publisher/insights/, for example /v1/publisher/insights/x | No |
405 | invalid_request_error | method_not_allowed | Not a GET | No |
429 | rate_limit_error | rate_limited | Over the rate limit. The response has Retry-After. | Yes, after Retry-After seconds |
500 | api_error | internal_error | An error on Adzen’s side | Yes, with backoff |
503 | api_error | authentication_unavailable | A temporary authentication error | Yes, with backoff |
503 | api_error | sites_unavailable | Web: your sites couldn’t be looked up just now | Yes, with backoff |
503 | api_error | busy | Adzen can’t run the report at the moment. The response has Retry-After: 2. | Yes, after Retry-After seconds |
503 | api_error | timeout | The report took too long to build | Yes, or narrow the date range or breakdowns |
401 responses carry
WWW-Authenticate: ApiKey realm="adzen", header="X-API-Key".
Adzen checks a request in this order and returns the first error it finds:
- The API key (
401,403 account_disabled,503 authentication_unavailable). - Whether reporting is enabled (
403 reporting_not_enabled). - The rate limit (
429). - The parameters (
400).
Rate limit
60 requests per minute for each publisher account, counted across all keys of that account. The window is the clock minute: the count resets at the start of each minute, not 60 seconds after your first request.
Every request that passes the key and reporting checks counts, including
requests that then fail with 400, 429 or 503.
Successful responses and 429s carry X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset (seconds until the current
minute ends). A 429 adds Retry-After with the same value. Wait that many
seconds before retrying. See Rate limits for limits
across products.
Polling
Poll at most once every 15 minutes. Use
breakdowns to get every country and region in one call. When
first_incomplete_date is not null, request the days from that date to the
current date on each poll and replace your stored counts for those days.