Prebid Report API

Pull bid-level Prebid data into Tableau, Looker, BigQuery or any BI tool

Prebid Report API

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.

Authentication

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.

Request

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
ParameterDefaultDescription
startrequiredFirst day, YYYY-MM-DD (UTC)
endtodayLast day, YYYY-MM-DD. Up to 92 days per request
projectsallComma-separated project ids, as GET /api/open/{ORG_ID}/projects lists them. Leave empty for every project in the organization
dimensionsdate,bidderComma-separated list, see below. Leave empty for totals only
metricsthe bid and revenue metricsComma-separated list, see below. ad_requests and pageviews are returned only when you ask for them
exclude_ivt11 drops traffic flagged as invalid (IVT), 0 keeps it
formatjsonjson, csv or ndjson (one row per line, for warehouse loads)
limit10000Rows per page, up to 100,000
offset0Rows to skip, for paging

Dimensions

DimensionDescription
dateDay (YYYY-MM-DD, UTC)
projectProject, returned as project_id and domain
devicemobile, desktop, tablet or other
countryISO 3166-1 alpha-2 country code
ad_unitPrebid ad unit code
bidderPrebid bidder code. Ad server demand that did not come from Prebid shows up under its own code, such as direct, dynamic or amz

Metrics

MetricDescription
bid_requestsBid requests sent to bidders (responses + no-bids + timeouts)
bid_responsesBids returned in time
no_bidsRequests the bidder answered with no bid
timeoutsRequests the bidder did not answer before Prebid's bidderTimeout, plus bids that arrived later than that
response_ratebid_responses / bid_requests, in %
timeout_ratetimeouts / bid_requests, in %
latency_p50, latency_p90Median and 90th percentile bid response time, in ms
bid_cpm_avgAverage CPM of the returned bids
auction_winsBids that won the Prebid auction and were sent to the ad server
win_rateauction_wins / bid_requests, in %
impressionsRendered ad impressions, from all demand
hb_impressionsRendered impressions won by a Prebid bid
revenueRevenue of hb_impressions (sum of winning CPM / 1000)
ecpmrevenue / 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:

MetricDescription
ad_requestsAd slots requested, filled or not. impressions / ad_requests is the fill rate
pageviewsPage 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.

Response

{
  "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.

Freshness

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.

Loading the data into Tableau

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.

Loading the data into BigQuery

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.

Errors

StatusMeaning
403Missing, disabled or wrong API key
404A project in projects does not exist in the organization
422Invalid parameter, or the request needs too much data (narrow the date range, projects or dimensions); the reason is in message
429Rate limit reached, retry after the Retry-After header