Overview
v2 reporting metrics are delivery numbers (impressions, spend, clicks, conversions, derived rates) rolled up across a hierarchical tree: account → advertiser → campaign → media buy → package. Every request hits a single endpoint and resolves the same tree before joining delivery metrics on top.date/impressions/spend rows that reconcile to its final totals.
Aggregate-only modular reports still complete the seller’s booking workflow but
are not assigned to a made-up reporting day.
v2 deliberately collapses what was a sprawl of v1 tools (
get_campaign_summary,
export_campaign_data, analyze_tactics, etc.) into one endpoint with a
view switch. The same shape works for dashboards, agent prompts, and BI
exports.Murph seller analytics
Storefront operators can ask Murph for seller analytics. Those analytics combine recent product-discovery intelligence runs with attributed commercial outcomes recorded when a buyer creates or updates media buys. ThesellerAnalytics.outcomes block summarizes approval and delivery activity
attributed to recent runs:
attributedRunCount,eventCountsubmittedCount,forwardedCount,forwardFailedCount,rejectedCount,deliveryReportedCountbookedMediaBuyCount,bookedBudget,deliveredImpressions,deliveredSpend,deliveredCurrency
outcome object with the same counts plus the
latest attributed latestType, latestStatus, and mediaBuyId. Supported
event types are media_buy_submitted_for_approval, media_buy_forwarded,
media_buy_forward_failed, media_buy_rejected, and delivery_reported. See
the Storefront object guide
for the field-level schema, including the buyers[] rollup that ranks buyer
domains by recent runs, asks, win rate, ask-to-book conversion, attributed
bookings, and delivery.
The same payload can include historicalPerformance, strategySignals[], and
seasonality[], and sellerRecommendations[]. Historical performance
summarizes win rate, ask-to-book conversion, price-objection conversion,
packaging conversion, booked budget, delivery rate, repeat buyers, and
product-selection breadth. seasonality[] buckets the same signals by UTC month
so sellers can see periods where demand, objections, or booking size changed.
strategySignals[] turns those metrics into directional negotiation postures
such as holding value, preserving value on price asks, using clearer package
ladders, or starting with fewer products. Posture values are documented in the
Storefront object guide
and include hold_value, value_preserving_compromise, tradeoff_ladder,
direct_fit, price_first, and wholesale_mirror. sellerRecommendations[]
turns the same window into deterministic seller actions.
Outcome totals use last-touch product-overlap attribution: outcome events are
matched to the most recent intelligence run that surfaced the relevant product
IDs. Treat them as directional seller analytics rather than audited financial
reporting. Seller recommendations are directional coaching signals, not
automated pricing or packaging changes.
The hierarchy
Every metrics request resolves the same four-level tree before joining delivery metrics on top:advertiserId narrows to that advertiser, campaignId narrows to
one campaign within an advertiser, and the system enforces that the campaign
belongs to the advertiser when both are passed.
Channel-group lineage
When a campaign uses channel groups, each media buy keeps the group ID and name that compiled into it. REST reporting returns that saved lineage in every view:- A hierarchical summary returns
channelGroup: { channelGroupId, name }on each media buy. - A time-series row returns
channelGroupIdandchannelGroupName. - A CSV export appends
Channel Group IDandChannel Group Namecolumns.
null in JSON and blank CSV fields.
Interchange does not infer a group from a media-buy name, product, or delivery.
REST callers can pass channelGroupId to filter the durable media-buy scope;
combine it with advertiserId or campaignId to narrow to their intersection.
For an agent-side rollup, call V3 get_delivery with
report: "campaign_delivery" and dimensions: ["channel_group"]. Add
filters.channelGroupId when you need one group only:
channelGroup: { id, name }, or null
for an ungrouped buy. The REST /reporting/metrics endpoint returns lineage
and accepts the group as a filter, but does not expose the V3 grouping
dimension.
Available metrics
Every level reports the same metric block:
Derived metrics are
null when their denominator is zero.
Currency
Every advertiser is locked to a single primary currency. Each summary advertiser block carries acurrency field naming it. If delivery arrives in a
different source currency, reporting uses the buy’s booked FX rate or a
creation-date historical rate when one is available.
If a buy’s spend cannot be denominated at all — the buy carries no evidence of
which currency its source is paid in, or its rows span currencies within the
period — summary requests return SPEND_DENOMINATION_UNRESOLVED (HTTP 422)
instead of returning money under the wrong currency label. The error names the
affected media buys in its message and in details.mediaBuyIds. It is classified
terminal: retrying reproduces it, because the data is wrong at rest. Scope the
request to exclude those buys to report on the rest of the account. Time-series
requests can remain available because each row can retain and declare its source
currency. See Cross-currency for the full rule.
Time-series rows carry the currency that actually denominates their money
alongside their advertiser ancestry. Normally this is the advertiser’s primary
currency. If no FX rate is available, the row retains its source currency rather
than relabeling unconverted spend. Both summary and time-series responses expose
totalsCurrency. Dashboard, in-chat, and CSV views use those fields rather than
assuming USD; every CSV row appends a Currency column without shifting existing
CSV columns.
Money totals are exact only when every returned row or advertiser block shares
one currency; the response names it in totalsCurrency. Advertisers with no
returned time-series rows do not affect time-series totalsCurrency. When the
returned data spans currencies, the money metrics in totals (spend, ecpm,
cpc) are null — the platform never adds EUR and USD together as one number.
Count metrics and unitless rates stay summed.
For a single cross-currency figure, pass displayCurrency (summary view
only, any platform-supported ISO-4217 code). The response then includes a
consolidated block: each advertiser’s money is converted at the current
rate-of-the-day and then summed, with a per-advertiser audit trail
(byAdvertiser) and a conversionInfo object recording exactly how the
figure was produced — the rates and pairs applied, the asOf day, and
estimate: true. When your advertisers already share one currency,
displayCurrency defaults to it and the consolidated block only appears when
you ask for a different currency.
Query parameters
Hierarchical summary (view=summary)
Time-series (view=timeseries)
CSV export (download=true)
Append download=true to either view to generate a CSV in GCS and return a
signed URL. Each row includes the effective Currency and appends Channel Group ID and Channel Group Name after the existing columns. Spend, eCPM, and
CPC remain self-describing outside the JSON response, and existing positional
columns retain their previous indexes:
- The signed URL expires in 7 days.
- CSV columns mirror the JSON shape (one row per leaf for summary, one row per leaf × day for time-series).
- Download is the recommended path for ranges longer than 7 days when
view=timeseries.
Audit logs (related)
Audit logs are a sibling read endpoint for the buyer activity feed:CAMPAIGN, CREATIVE,
MEDIA_BUY, and other resource types. Filterable by startDate / endDate,
advertiserId, campaignId, and resourceTypes. Returns up to 500 logs per
page (default 50) plus a total for pagination.
How delivery data flows
This section describes third-party SALES agents — sell-side inventory sources reached over AdCP (see Storefront onboarding). It does not describe the direct ad-platform adapters (Meta, Google, TikTok, and the rest): those adapters don’t push webhook deliveries at all. Apostra polls each one’s own API instead — see the “Webhooks and async lifecycle” section on any ad platform compatibility page for what a specific adapter actually does. Reporting metrics depend on the sales agent reporting actual delivery back to Apostra. Three transports are supported and chosen at agent registration time via the SALES agent’sreportingType field. See the
Storefront onboarding guide for the
registration syntax — the reporting transports themselves follow the
ADCP optimization & reporting spec.
WEBHOOK (default)
Agent posts ADCP
get_media_buy_delivery-shaped payloads to an Apostra
webhook URL embedded in the original create_media_buy call. Best for
most integrations.BUCKET
Agent writes JSON / JSONL / CSV / Parquet files to S3, GCS, or Azure Blob.
Apostra watches the path and ingests on landing. Best for high-volume
batch reporting.
POLLING
Apostra calls the agent’s
get_media_buy_delivery on a DAILY or
MONTHLY schedule. Best for legacy systems without outbound webhooks.(reporting_date, media_buy_id), keeping the most recent received_at. From
there, /reporting/metrics does the math.
Webhook delivery
When a SALES agent is registered withreportingType: WEBHOOK, Apostra issues
a per-media-buy webhook URL inside the original create_media_buy call. The
agent posts ADCP get_media_buy_delivery-shaped payloads back to that URL on
its own cadence. Every request is signed and timestamped:
Operational rules:
- The shared secret is provisioned at agent registration and stored encrypted at rest alongside the agent’s auth credentials. Treat it as production-grade secret material — never log, commit, or paste it.
- Verify the signature in constant time before parsing the body. Reject unsigned, mistimed, or duplicate requests.
- Rotate the secret on a documented schedule and on any suspected exposure. Apostra supports a brief overlap window where both old and new secrets validate so in-flight deliveries are not lost.
410 Gone; use the callback from the current
media-buy request or contact support rather than rewriting the URL. Apostra
returns 200 only after the bridged reporting write is accepted; retry a 503
response, and correct a report that receives 422 before sending it again.
For a previously issued callback that now returns 410, use this cutover
checklist:
- Keep the callback URL byte-for-byte unchanged, including every path ID and the operation ID.
- Publish a
jwks_uriin thebrand.jsonentry for the active registered seller endpoint bound to the retired callback. Contact support if that binding is unclear; do not guess or repoint the callback. - Sign the POST with RFC 9421
SignatureandSignature-Inputheaders. Sign the registered callback URL as@target-uri; do not sign a rewritten proxy host. Shared-secret HMAC cannot authorize a retired callback. - Send the normal
media_buy_deliverypayload with the original upstreammedia_buy_idand package IDs. Retry503; stop and correct401,410, or422before retrying. - After the first
200, replay each missing reporting day in date order. Keep the same media-buy and package IDs; use a new monotonically increasingsequence_numberfor each replayed notification.
Access control
- The account ID is taken from the auth context — buyers can only read metrics for media buys their account owns.
- When
advertiserIdis provided, the platform double-checks advertiser access before issuing any metrics query. - Every metrics query also filters on
customer_idat the data layer, so a missing ACL entry cannot leak data.
Demo mode
Pass?demo=true to get a deterministic, multi-advertiser fixture with realistic
campaign and product names. Useful for storybook / sandbox screens without any
seeded data, and for teaching agents how the schema looks before running a real
query. Demo data is capped to a 90-day window.