Pull bid-level Prebid data into Tableau, Looker, BigQuery or any BI tool
The Prebid Report API returns your header bidding data as a flat table: one row per combination of the dimensions you ask for, with one column per metric. It is built for BI tools and data warehouses, so you can keep all of your reporting in one place.
The data comes from the PubPerf tag in the browser, not from your ad server, so it shows what actually happened in each Prebid auction: requests, bids, no-bids, timeouts, latency and winning CPMs.
Create an API key under Connect > API (see API Keys & Integrations) and send it in the Authorization header:
curl -H "Authorization: Bearer YOUR_API_KEY" \
"https://app.pubperf.com/api/open/ORG_ID/prebid/report?start=2026-10-01&end=2026-10-07"
ORG_ID is the organization id shown in your dashboard URL. Requests are limited to 30 per minute per API key.
An OpenAPI 3 description of this endpoint is available for code generators and API clients.
GET https://app.pubperf.com/api/open/{ORG_ID}/prebid/report
| Parameter | Default | Description |
|---|---|---|
start | required | First day, YYYY-MM-DD (UTC) |
end | today | Last day, YYYY-MM-DD. Up to 92 days per request |
projects | all | Comma-separated project ids, as GET /api/open/{ORG_ID}/projects lists them. Leave empty for every project in the organization |
dimensions | date,bidder | Comma-separated list, see below. Leave empty for totals only |
metrics | the bid and revenue metrics | Comma-separated list, see below. ad_requests and pageviews are returned only when you ask for them |
exclude_ivt | 1 | 1 drops traffic flagged as invalid (IVT), 0 keeps it |
format | json | json, csv or ndjson (one row per line, for warehouse loads) |
limit | 10000 | Rows per page, up to 100,000 |
offset | 0 | Rows to skip, for paging |
| Dimension | Description |
|---|---|
date | Day (YYYY-MM-DD, UTC) |
project | Project, returned as project_id and domain |
device | mobile, desktop, tablet or other |
country | ISO 3166-1 alpha-2 country code |
ad_unit | Prebid ad unit code |
bidder | Prebid bidder code. Ad server demand that did not come from Prebid shows up under its own code, such as direct, dynamic or amz |
| Metric | Description |
|---|---|
bid_requests | Bid requests sent to bidders (responses + no-bids + timeouts) |
bid_responses | Bids returned in time |
no_bids | Requests the bidder answered with no bid |
timeouts | Requests the bidder did not answer before Prebid's bidderTimeout, plus bids that arrived later than that |
response_rate | bid_responses / bid_requests, in % |
timeout_rate | timeouts / bid_requests, in % |
latency_p50, latency_p90 | Median and 90th percentile bid response time, in ms |
bid_cpm_avg | Average CPM of the returned bids |
auction_wins | Bids that won the Prebid auction and were sent to the ad server |
win_rate | auction_wins / bid_requests, in % |
impressions | Rendered ad impressions, from all demand |
hb_impressions | Rendered impressions won by a Prebid bid |
revenue | Revenue of hb_impressions (sum of winning CPM / 1000) |
ecpm | revenue / hb_impressions × 1000 |
The metrics above are returned by default. These are returned when you list them in metrics. They are plain counts, so you can add them up in your own database and work out ratios such as fill rate or page RPM yourself:
| Metric | Description |
|---|---|
ad_requests | Ad slots requested, filled or not. impressions / ad_requests is the fill rate |
pageviews | Page views. Page views have no ad unit or bidder, so pageviews can only be combined with the date, project, device and country dimensions. revenue / pageviews × 1000 is the page RPM |
Revenue and CPMs are in the currency Prebid reported for the winning bids, which is your ad server currency when you use the Prebid.js currency module. Every row has a currency column. If a row combines projects that use different currencies, currency is MIXED: add the project dimension, or filter projects, to keep currencies apart.
Ratios such as win_rate and ecpm are recalculated for every row from the underlying counts, so they stay correct whichever dimensions you pick. Do not average them again in your BI tool: sum the counts and recompute the ratio instead.
{
"meta": {
"start": "2026-10-01",
"end": "2026-10-07",
"timezone": "UTC",
"dimensions": ["date", "bidder"],
"metrics": ["bid_requests", "win_rate", "revenue", "ecpm"],
"exclude_ivt": true,
"limit": 10000,
"offset": 0,
"rows": 2,
"has_more": false
},
"columns": ["date", "bidder", "currency", "bid_requests", "win_rate", "revenue", "ecpm"],
"data": [
{ "date": "2026-10-01", "bidder": "appnexus", "currency": "EUR", "bid_requests": 1203311, "win_rate": 6.12, "revenue": 412.0811, "ecpm": 2.4113 },
{ "date": "2026-10-01", "bidder": "rubicon", "currency": "EUR", "bid_requests": 1187420, "win_rate": 5.4, "revenue": 377.9024, "ecpm": 2.2871 }
]
}
With format=csv the response is a CSV file with a header row. With format=ndjson it is newline-delimited JSON: one row object per line, without meta. In both cases the X-Has-More header is 1 when more rows are available.
When has_more is true, request the next page with offset increased by limit. Rows are sorted by the dimensions, so pages do not overlap.
The report covers complete days in UTC, so the latest day available is yesterday. Figures for the last three days can still be updated as late data arrives, so a daily job should fetch the last three days and replace them.
Tableau cannot send an Authorization header from a plain web data source, so the simplest setup is a small scheduled job that writes the report to a file or table Tableau already reads. For example, a daily job that writes each of the last three days to its own CSV file:
import csv, datetime, requests
API = "https://app.pubperf.com/api/open/ORG_ID/prebid/report"
KEY = "YOUR_API_KEY"
for days_ago in (1, 2, 3):
day = datetime.date.today() - datetime.timedelta(days=days_ago)
params = {
"start": day.isoformat(),
"end": day.isoformat(),
"dimensions": "date,project,device,country,ad_unit,bidder",
"limit": 100000,
}
rows, offset = [], 0
while True:
r = requests.get(API, params={**params, "offset": offset},
headers={"Authorization": f"Bearer {KEY}"}, timeout=120)
r.raise_for_status()
body = r.json()
rows += body["data"]
if not body["meta"]["has_more"]:
break
offset += params["limit"]
with open(f"prebid_report_{day}.csv", "w", newline="") as f:
writer = csv.DictWriter(f, fieldnames=body["columns"])
writer.writeheader()
writer.writerows(rows)
Point Tableau at the folder as a wildcard union, or load each file into your database. Each run replaces the files of the last three days with their latest figures.
The same approach works for loading the data into a warehouse such as BigQuery, Snowflake or Redshift, which Tableau can then connect to directly.
Use format=ndjson, which BigQuery loads as NEWLINE_DELIMITED_JSON. Load each of the last three days into its own date partition; --replace overwrites the partition, so re-running a day, for example after a failed job, is safe:
for n in 1 2 3; do
day=$(date -d "$n days ago" +%F)
curl -s -H "Authorization: Bearer $KEY" \
"https://app.pubperf.com/api/open/$ORG/prebid/report?start=$day&end=$day&dimensions=date,project,device,country,ad_unit,bidder&format=ndjson&limit=100000" \
> report.ndjson
bq load --source_format=NEWLINE_DELIMITED_JSON --autodetect --replace \
--time_partitioning_field=date \
"pubperf.prebid_report\$${day//-/}" report.ndjson
done
Run it daily with Cloud Scheduler and a Cloud Run job. If a day has more than limit rows (X-Has-More: 1), fetch the next page with offset and append it before loading.
| Status | Meaning |
|---|---|
403 | Missing, disabled or wrong API key |
404 | A project in projects does not exist in the organization |
422 | Invalid parameter, or the request needs too much data (narrow the date range, projects or dimensions); the reason is in message |
429 | Rate limit reached, retry after the Retry-After header |