Skip to main content

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.
It returns either a hierarchical summary (advertiser → campaign → media buy → package) or a time-series (one row per leaf × day), and can also generate a CSV with a 7-day signed download URL. Metrics come from the reporting pipeline; the hierarchy and access control come from the system of record. Delivery can enter that pipeline from seller webhooks, scheduled source polls, or a storefront’s modular final-report workflow. A modular final report appears in buyer reporting when it includes an explicit currency and per-day 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.
Hourly delivery reporting is not supported on /api/v2/buyer/reporting/metrics — this endpoint is day-grain only. There is no granularity parameter; passing one will be ignored. For hourly event counts (conversions, clicks, impressions tracked via your own pixel/event sources), use GET /api/v2/buyer/advertisers/:id/events/summary instead.
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. The sellerAnalytics.outcomes block summarizes approval and delivery activity attributed to recent runs:
  • attributedRunCount, eventCount
  • submittedCount, forwardedCount, forwardFailedCount, rejectedCount, deliveryReportedCount
  • bookedMediaBuyCount, bookedBudget, deliveredImpressions, deliveredSpend, deliveredCurrency
Each recent run also includes an 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:
Filters cascade: 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.

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 a currency 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.
Consolidated figures are display estimates converted at one current rate-of-the-day — they are never used for billing, invoices, or settlement, which always happen in native currencies. A currency that cannot be priced is excluded from the converted total and listed in conversionInfo.unconverted rather than guessed.

Query parameters

Both summary and timeseries JSON responses honor the requested window up to 90 days. For wider exports use ?download=true to get a CSV.

Hierarchical summary (view=summary)

Time-series (view=timeseries)

Returns flat rows — one per leaf per day — sorted by date ascending. Every row carries the full ancestry so a chart can group by any level without an extra join:

CSV export (download=true)

Append download=true to either view to generate a CSV in GCS and return a signed URL. Each row appends the effective Currency so spend, eCPM, and CPC remain self-describing outside the JSON response while 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.
The signed download URL is itself a bearer credential. Anyone who has the URL can download the export — there is no additional auth check beyond URL knowledge until expiry.
  • Treat downloadUrl like a password or API key: never paste it into chat, tickets, screenshots, public dashboards, or unencrypted email.
  • Don’t log the URL in long-lived application logs.
  • Hand the URL to the consuming system over a secure channel and have that system fetch immediately rather than queuing it for later.
  • The export contains advertiser, campaign, and spend data — treat the downloaded CSV itself as sensitive once retrieved.
Audit logs are a sibling read endpoint for the buyer activity feed:
The buyer feed surfaces meaningful actions on 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.
Use audit logs for “who did what, when” questions — recorded in real time. Use /reporting/metrics for performance numbers — aggregated nightly from agent-reported delivery.

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. Interchange 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 Scope3. Three transports are supported and chosen at agent registration time via the SALES agent’s reportingType 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 a Scope3 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. Scope3 watches the path and ingests on landing. Best for high-volume batch reporting.

POLLING

Scope3 calls the agent’s get_media_buy_delivery on a DAILY or MONTHLY schedule. Best for legacy systems without outbound webhooks.
All three transports land in the same reporting store and are deduped per (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 with reportingType: WEBHOOK, Scope3 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. Scope3 supports a brief overlap window where both old and new secrets validate so in-flight deliveries are not lost.
See the Storefront onboarding guide for the registration syntax and matching rules on the agent side. If a seller source is re-registered or migrated after a media buy starts, keep posting delivery to the callback URL issued with that buy when the request carries the seller’s RFC 9421 signature. Interchange verifies the retired callback identity against either the exact active seller source in its archived route binding or one unambiguous active successor, and verifies that each reported media-buy ID maps to exactly one buyer-owned route. Buyers therefore continue receiving daily reporting without a callback change. Shared-secret HMAC does not identify a retired seller and cannot authorize this bridge. A callback that cannot be tied to one eligible active seller source and unambiguous buyer routes returns 410 Gone; use the callback from the current media-buy request or contact support rather than rewriting the URL. Interchange 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:
  1. Keep the callback URL byte-for-byte unchanged, including every path ID and the operation ID.
  2. Publish a jwks_uri in the brand.json entry 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.
  3. Sign the POST with RFC 9421 Signature and Signature-Input headers. Sign the registered callback URL as @target-uri; do not sign a rewritten proxy host. Shared-secret HMAC cannot authorize a retired callback.
  4. Send the normal media_buy_delivery payload with the original upstream media_buy_id and package IDs. Retry 503; stop and correct 401, 410, or 422 before retrying.
  5. After the first 200, replay each missing reporting day in date order. Keep the same media-buy and package IDs; use a new monotonically increasing sequence_number for each replayed notification.
See Signing the webhooks you send us for the full signing contract.

Access control

  • The account ID is taken from the auth context — buyers can only read metrics for media buys their account owns.
  • When advertiserId is provided, the platform double-checks advertiser access before issuing any metrics query.
  • Every metrics query also filters on customer_id at 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.