> ## Documentation Index
> Fetch the complete documentation index at: https://docs.interchange.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Reporting Overview

> How v2 reporting hierarchies, metrics, time-series, and CSV exports fit together — plus how delivery data lands in the reporting pipeline in the first place.

## 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.

```
GET /api/v2/buyer/reporting/metrics
```

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.

<Warning>
  **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.
</Warning>

<Note>
  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.
</Note>

## 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](/v2/object-guides/storefront#murph-selleranalytics)
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](/v2/object-guides/storefront#murph-selleranalytics)
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.

<Note>
  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.
</Note>

## The hierarchy

Every metrics request resolves the same four-level tree before joining delivery
metrics on top:

```
Account
└── Advertiser
    └── Campaign
        └── Media Buy
            └── Package (one per product within the buy)
```

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:

| Metric             | Type           | Notes                                                                                                                                                                                                                                                                                                                                                  |
| ------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `impressions`      | int            | Sum across deduped daily reports                                                                                                                                                                                                                                                                                                                       |
| `spend`            | number         | Sum in the advertiser's primary currency (see [Currency](#currency)). Gross — fee-inclusive, stated at the fee terms locked on each media buy, so it compares directly against your budgets. Legacy media buys created before fee terms were locked report spend net, as the seller reported it. See [Budgets and fees](/v2/concepts/budgets-and-fees) |
| `clicks`           | int            |                                                                                                                                                                                                                                                                                                                                                        |
| `views`            | int            | Viewable impressions (MRC-viewable subset of impressions, per AdCP). Divide by `impressions` for viewability rate.                                                                                                                                                                                                                                     |
| `completedViews`   | int            | Video/audio completions (qualified by `view_duration_seconds` when set on the goal)                                                                                                                                                                                                                                                                    |
| `conversions`      | int            |                                                                                                                                                                                                                                                                                                                                                        |
| `leads`            | int            |                                                                                                                                                                                                                                                                                                                                                        |
| `videoCompletions` | int            |                                                                                                                                                                                                                                                                                                                                                        |
| `ecpm`             | number \| null | `(spend × 1000) / impressions`                                                                                                                                                                                                                                                                                                                         |
| `cpc`              | number \| null | `spend / clicks`                                                                                                                                                                                                                                                                                                                                       |
| `ctr`              | number \| null | `clicks / impressions`                                                                                                                                                                                                                                                                                                                                 |
| `completionRate`   | number \| null | `completedViews / impressions`                                                                                                                                                                                                                                                                                                                         |

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 neither rate is available, summary requests return `FX_RATE_UNAVAILABLE`
instead of returning money under the wrong currency label. Time-series requests
can remain available because each row can retain and declare its source currency.

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.

<Warning>
  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.
</Warning>

## Query parameters

| Param             | Type                      | Default                            | Notes                                                                           |
| ----------------- | ------------------------- | ---------------------------------- | ------------------------------------------------------------------------------- |
| `advertiserId`    | string                    | —                                  | Advertiser ID                                                                   |
| `campaignId`      | string                    | —                                  | Must belong to `advertiserId` if both passed                                    |
| `startDate`       | `YYYY-MM-DD`              | derived from `days`                |                                                                                 |
| `endDate`         | `YYYY-MM-DD`              | today                              |                                                                                 |
| `days`            | int 0..90                 | 7                                  | `0` = "all time" — resolves to the account's first reporting date               |
| `view`            | `summary` \| `timeseries` | `summary`                          |                                                                                 |
| `displayCurrency` | ISO-4217                  | your home currency when resolvable | Consolidated FX-converted rollup (see [Currency](#currency)); summary view only |
| `download`        | bool                      | `false`                            | When `true`, returns a signed CSV URL                                           |
| `demo`            | bool                      | `false`                            | Returns auto-generated demo data — useful for sandbox UIs                       |

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

## Hierarchical summary (`view=summary`)

```bash theme={null}
curl "https://api.interchange.io/api/v2/buyer/reporting/metrics?days=14&advertiserId=42" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "advertisers": [
    {
      "advertiserId": "42",
      "advertiserName": "Acme Corp",
      "currency": "EUR",
      "metrics": { "impressions": 12345, "spend": 678.9, "...": "..." },
      "campaigns": [
        {
          "campaignId": "camp_001",
          "campaignName": "Spring Awareness",
          "metrics": { "...": "..." },
          "mediaBuys": [
            {
              "mediaBuyId": "mb_abc",
              "name": "CTV Always-On",
              "status": "ACTIVE",
              "budget": 50000,
              "metrics": { "...": "..." },
              "packages": [
                {
                  "packageId": "pkg_xyz",
                  "productId": "prod_xyz",
                  "productName": "Premium CTV — Network Supply",
                  "metrics": { "...": "..." }
                }
              ]
            }
          ]
        }
      ]
    }
  ],
  "totals": { "...": "..." },
  "totalsCurrency": "EUR",
  "periodStart": "2026-04-12",
  "periodEnd": "2026-04-25"
}
```

## Time-series (`view=timeseries`)

```bash theme={null}
curl "https://api.interchange.io/api/v2/buyer/reporting/metrics?view=timeseries&days=7" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

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:

```json theme={null}
{
  "timeseries": [
    {
      "date": "2026-04-19",
      "advertiserId": "42",
      "advertiserName": "Acme Corp",
      "currency": "EUR",
      "campaignId": "camp_001",
      "campaignName": "Spring Awareness",
      "mediaBuyId": "mb_abc",
      "mediaBuyName": "CTV Always-On",
      "mediaBuyStatus": "ACTIVE",
      "packageId": "pkg_xyz",
      "productId": "prod_xyz",
      "productName": "Premium CTV — Network Supply",
      "metrics": { "...": "..." }
    }
  ],
  "totals": { "...": "..." },
  "totalsCurrency": "EUR",
  "periodStart": "2026-04-19",
  "periodEnd": "2026-04-25"
}
```

## 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:

```bash theme={null}
curl "https://api.interchange.io/api/v2/buyer/reporting/metrics?view=timeseries&days=90&download=true" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

```json theme={null}
{
  "downloadUrl": "https://storage.googleapis.com/scope3-reporting-exports/...",
  "expiresAt": "2026-05-02T00:00:00Z",
  "fileName": "reporting-2026-01-25-2026-04-25.csv",
  "rowCount": 1842
}
```

<Note>
  * 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`.
</Note>

<Warning>
  **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.
</Warning>

## Audit logs (related)

Audit logs are a sibling read endpoint for the buyer activity feed:

```
GET /api/v2/buyer/audit-logs
```

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.

```bash theme={null}
curl "https://api.interchange.io/api/v2/buyer/audit-logs?campaignId=camp_001&take=100" \
  -H "Authorization: Bearer $SCOPE3_API_KEY"
```

<Tip>
  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.
</Tip>

## How delivery data flows

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](/v2/setup/storefront-onboarding) for the
registration syntax — the reporting transports themselves follow the
[ADCP optimization & reporting spec](https://docs.adcontextprotocol.org/docs/media-buy/media-buys/optimization-reporting).

<CardGroup cols={3}>
  <Card title="WEBHOOK (default)" icon="bolt">
    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.
  </Card>

  <Card title="BUCKET" icon="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.
  </Card>

  <Card title="POLLING" icon="rotate">
    Scope3 calls the agent's `get_media_buy_delivery` on a `DAILY` or
    `MONTHLY` schedule. Best for legacy systems without outbound webhooks.
  </Card>
</CardGroup>

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:

| Header                | Purpose                                                                                            |                 |                                            |
| --------------------- | -------------------------------------------------------------------------------------------------- | --------------- | ------------------------------------------ |
| `X-Scope3-Signature`  | HMAC-SHA256 of `timestamp + "." + raw-body`, hex-encoded, computed with the shared webhook secret. |                 |                                            |
| `X-Scope3-Timestamp`  | RFC3339 timestamp of the request. Receivers MUST reject requests where \`                          | now − timestamp | \` exceeds **5 minutes** to defeat replay. |
| `X-Scope3-Webhook-Id` | Idempotency key. Scope3 deduplicates by this value across retries.                                 |                 |                                            |

**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](/v2/setup/storefront-onboarding)
for the registration syntax and matching rules on the agent side.

## 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.
