GET /api/v2/buyer/reporting/metrics
Returns delivery metrics rolled up across the advertiser → campaign → media buy → package tree. Choose a hierarchical summary or flat timeseries view, scope with advertiserId, campaignId, channelGroupId, or mediaBuyId, and set the window with days or an explicit startDate/endDate. This endpoint is day-grain only — for hourly counts use Get event summary.
Request
Parameters
Response
The example below uses illustrative, non-zero
conversionValue, cpa, and
roas values to show the response shape. See the note below the response for
how these fields behave when a seller has not reported conversion data.management state:
tracked (a campaign you did not set up through the platform, mirrored
read-only from a connected provider account) or managed (authored or adopted
through the platform). This surface currently reports managed campaigns only —
tracked mirror delivery is excluded — so totals never silently mix the two
states.
conversionValue is advertiser-attributed revenue from conversions; unlike
spend, it is never grossed up with platform fees. cpa (spend / conversions)
is null when there are no conversions. roas (conversionValue / spend) is
null when spend is zero, or when there is no conversion signal at all — zero
conversions and zero attributed value; it reports a true 0 only when
conversions are real but their attributed value is zero.
These fields depend on the seller reporting conversion data. Where a seller
reports none, conversionValue is 0 and cpa and roas are null.
viewability contains the seller-reported viewable_impressions and
measurable_impressions counts. Its viewable_rate is
viewable_impressions / measurable_impressions. See
Viewability for which reports
count. The object is null, never a zero-filled object, when no report in scope
measured viewability. views is a different metric: it counts content views,
not viewable impressions.
A CSV export carries Viewable Impressions, Measurable Impressions, and
Viewable Rate after Channel Group Name.
Joining packages to your own data
A package has two ids.packageId is Apostra’s id for it. sellerPackageId is
the seller’s own id for the same package. A {PACKAGE_ID} macro on your landing
pages or trackers can arrive with either one, depending on how the seller
served the ad. When one creative serves several packages, the seller fills it
with its own id. To join site sessions or conversions to delivery by package,
match the id your URL carries against packageId or sellerPackageId.
A CSV export carries it as its last column,
Seller Package ID, which is empty
when the id is not known.
Goal progress
Every media buy block, and every campaign block, that has something to judge against carriesgoalProgress: delivery compared with the buyer’s goal and the
seller’s commitment. A media buy is judged against its own
goal commitment; a campaign against its
primary goal, with the weakest commitment across its buys. The campaign’s
answeredTarget is likewise the weakest answer: the highest cost per unit, or
the lowest rate or return. It is null when the buys answered in different
kinds. Blocks with neither a goal nor a commitment omit the field. The timeseries view carries the same
judgements, over the whole period, in a goalProgress object with mediaBuys[]
and campaigns[]. What each part means and how to read it is explained in
Goal-seeking campaigns.
The rows below give the media buy block’s path; the campaign block carries
the same object at advertisers[].campaigns[].goalProgress.
reportingPeriodEnd, nextExpectedAt, notificationType, and sequenceNumber
come from the validated delivery-report envelope processed through either a
reporting webhook or a storefront route poll. The API does not infer an omitted
envelope value; when no processed envelope has supplied it, that field remains
unavailable. A later report that omits an optional value does not clear a
previously stored seller-stated value.
A media buy committed at 3.00 per click that has delivered 500 clicks for
2,000 reads actual: { kind: "cost_per", value: 4, unit: "clicks", units: 500 }
and verdict: "behind". The same buy on its first day with two clicks reads the
actual and verdictWithheld: "too_few_observations", with no verdict.
A
viewable_rate goal is judged only from the viewable and measurable
impression counts in the AdCP viewability object. Reporting does not read
that object yet, so a viewable_rate goal currently reads
verdictWithheld: "metric_not_reported" with viewability in
missingMetrics. The views metric is content views — the quantity CPV
pricing bills on — and is never used as a stand-in for viewable impressions.Spend pace
goalProgress.pace answers a separate question from the goal: is the money
moving at the rate the flight implies? “Spend 200 a day at 80% viewable” is two
answers, not one — the pace verdict and the goal verdict are reported side by
side. Pace is not spend ÷ budget: a buy three days into a thirty-day flight
that has spent a tenth of its budget is exactly on pace, while a buy on its last
day that has spent the same tenth is nine tenths undelivered.
Pace is always measured over the window you asked for. Narrow
days and both
actualSpend and expectedSpend shrink together, so the verdict stays
comparable; it never compares a week of spend with a month of expectation.download=true, the response is instead { downloadUrl, expiresAt, fileName, rowCount } with a signed URL that expires in 7 days. Treat that URL as a bearer credential.
Cost per outcome
Sellers’conversions and cpa are empty when no seller reports conversions.
To get the cost of an outcome you count yourself, such as a sign-up, upload
your counts as measurement records
that name the media buy and package, then read the timeseries with
outcomeType:
outcomeType needs campaignId or mediaBuyId, and startDate and
endDate no more than 90 days apart. It cannot be combined with
view=summary, download=true or demo=true. Each row then also carries:
A record’s
value counts the outcomes in its own window; it is never read as
a running total. Only records whose window is a single day count, on that
day. A record covering several days is not split across them and is not
counted, so a weekly count added beside daily ones never counts the same
days twice. Upload one record per package per day to read every day.
Rows come from delivery: a package day with spend and no record reads
outcomeCount: null, and a record for a day with no delivery has no row to
appear on. Records that name only a campaign belong to no media buy and do not
appear here.
Channel-group lineage
When a campaign uses channel groups, reporting carries the group that compiled into each media buy:- A
summaryresponse returnsadvertisers[].campaigns[].mediaBuys[].channelGroupas{ channelGroupId, name }. - A
timeseriesresponse returnschannelGroupIdandchannelGroupNameon every row. - A CSV export appends
Channel Group IDandChannel Group Namecolumns.
channelGroup: null in a summary,
channelGroupId: null and channelGroupName: null in time-series JSON, and
blank CSV fields. These values are saved media-buy lineage; Interchange does
not infer them from names, products, or delivery.
This REST endpoint accepts channelGroupId as a scope filter, but it does not
group rows by that field. For an agent-side rollup, call V3 get_delivery with
report: "campaign_delivery", dimensions: ["channel_group"], and, when you
need one group, filters.channelGroupId. Rows then return the selected
dimension as channelGroup: { id, name }, or null for an ungrouped buy.
When you pass filters.mediaBuyId, get_delivery returns delivery for that
media buy only; it does not include other media buys from the same storefront.
Errors
400 VALIDATION_ERROR— malformedstartDate/endDate,daysoutside0–90, acampaignIdthat does not belong to the givenadvertiserId, oroutcomeTypewithout the scope and dates Cost per outcome needs.422 SPEND_DENOMINATION_UNRESOLVED— one or more media buys in scope have spend that cannot be denominated, so no figure would be correct. The error names them in its message and indetails.mediaBuyIds, alongsidedetails.unresolvedCount. Classifiedterminal— do not retry. The cause is delivery data that is wrong at rest (a source reporting a currency it is not paid in, a buy missing its cross-currency booking evidence, or currencies mixed within the period), so every retry reproduces it. Report the named ids to support; to keep reporting in the meantime, scope the request to exclude them (a single campaign, or a period they did not deliver in). Unaffected buys report normally, andview=timeseriescan still return rows for buys whose days each carry their own currency.503 FX_RATE_UNAVAILABLE— the exchange-rate feed could not price a currency pair. Classifiedtransient; retry shortly.
Related
Reporting tasks
All reporting operations
Reporting overview guide
Hierarchy, metrics, CSV export, delivery flow
Get event summary
Hourly event counts by type