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

# Get reporting metrics

> Read delivery metrics across advertisers and campaigns

`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](/v2/buyer/reporting/tasks/get-event-summary).

## Request

<CodeGroup>
  ```bash Summary theme={null}
  curl "https://api.apostra.com/api/v2/buyer/reporting/metrics?days=14&advertiserId=42&demo=false" \
    -H "Authorization: Bearer $SCOPE3_API_KEY"
  ```

  ```bash Time-series CSV theme={null}
  curl "https://api.apostra.com/api/v2/buyer/reporting/metrics?view=timeseries&days=90&download=true&demo=false" \
    -H "Authorization: Bearer $SCOPE3_API_KEY"
  ```
</CodeGroup>

## Parameters

| Param            | Type    | Required | Notes                                                                                                                                 |
| ---------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `advertiserId`   | string  | No       | Filter to one advertiser                                                                                                              |
| `campaignId`     | string  | No       | Filter to one campaign. Must belong to `advertiserId` if both are passed                                                              |
| `channelGroupId` | string  | No       | Filter to media buys in one saved campaign channel group. Combine with `advertiserId` or `campaignId` to narrow to their intersection |
| `mediaBuyId`     | string  | No       | Filter to one media buy owned by the authenticated Buyer                                                                              |
| `startDate`      | string  | No       | `YYYY-MM-DD`. Overrides the `days` window when set                                                                                    |
| `endDate`        | string  | No       | `YYYY-MM-DD`. Defaults to today                                                                                                       |
| `days`           | integer | No       | `0`–`90`, default `7`. Use `0` for the full campaign timeframe                                                                        |
| `view`           | enum    | No       | `summary` (default) or `timeseries`                                                                                                   |
| `download`       | boolean | No       | Default `false`. When `true`, returns a signed CSV URL instead of JSON                                                                |
| `demo`           | boolean | No       | Default `false`. When `true`, returns auto-generated demo data instead of real data                                                   |

## Response

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

```json theme={null}
{
  "advertisers": [
    {
      "advertiserId": "42",
      "advertiserName": "Northwind Outdoors",
      "metrics": { "impressions": 12345, "spend": 678.9, "clicks": 210, "views": 9800, "completedViews": 4200, "conversions": 18, "leads": 3, "videoCompletions": 4200, "conversionValue": 950.0, "ecpm": 55.0, "cpc": 3.23, "ctr": 0.017, "completionRate": 0.43, "cpa": 37.72, "roas": 1.4 },
      "campaigns": [
        {
          "campaignId": "cmp_001",
          "campaignName": "Spring Trail Series",
          "management": "managed",
          "metrics": { "impressions": 12345, "spend": 678.9, "clicks": 210, "views": 9800, "completedViews": 4200, "conversions": 18, "leads": 3, "videoCompletions": 4200, "conversionValue": 950.0, "ecpm": 55.0, "cpc": 3.23, "ctr": 0.017, "completionRate": 0.43, "cpa": 37.72, "roas": 1.4 },
          "mediaBuys": []
        }
      ]
    }
  ],
  "totals": { "impressions": 12345, "spend": 678.9, "clicks": 210, "views": 9800, "completedViews": 4200, "conversions": 18, "leads": 3, "videoCompletions": 4200, "conversionValue": 950.0, "ecpm": 55.0, "cpc": 3.23, "ctr": 0.017, "completionRate": 0.43, "cpa": 37.72, "roas": 1.4 },
  "periodStart": "2026-05-24",
  "periodEnd": "2026-06-06"
}
```

Each campaign block (and each timeseries row) carries a `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`.

When `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.

### Channel-group lineage

When a campaign uses [channel groups](/v2/buyer/campaigns/channel-groups),
reporting carries the group that compiled into each media buy:

* A `summary` response returns
  `advertisers[].campaigns[].mediaBuys[].channelGroup` as
  `{ channelGroupId, name }`.
* A `timeseries` response returns `channelGroupId` and `channelGroupName` on
  every row.
* A CSV export appends `Channel Group ID` and `Channel Group Name` columns.

Legacy or ungrouped media buys return `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.

## Errors

* `400 VALIDATION_ERROR` — malformed `startDate`/`endDate`, `days` outside `0`–`90`, or a `campaignId` that does not belong to the given `advertiserId`.
* `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 in `details.mediaBuyIds`, alongside `details.unresolvedCount`.
  **Classified `terminal` — 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, and `view=timeseries`
  can 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. Classified `transient`; retry shortly.

See [Errors](/v2/reference/errors) for the full error contract, and
[Cross-currency](/v2/concepts/cross-currency) for how delivery spend is
denominated.

## Related

<CardGroup cols={2}>
  <Card title="Reporting tasks" href="/v2/buyer/reporting/tasks" icon="list-check">
    All reporting operations
  </Card>

  <Card title="Reporting overview guide" href="/v2/guides/reporting-overview" icon="book">
    Hierarchy, metrics, CSV export, delivery flow
  </Card>

  <Card title="Get event summary" href="/v2/buyer/reporting/tasks/get-event-summary" icon="clock">
    Hourly event counts by type
  </Card>
</CardGroup>
