---
title: "Stored read: List insights"
method: GET
path: "/observe/insights"
tags: ["Insights"]
---

# Stored read: List insights

`GET /observe/insights`

## Request

- Base URL: `https://api.adcrunch.dev`
- URL: `https://api.adcrunch.dev/observe/insights`
- Auth: HTTP bearer

**Available on:** [![Meta](/providers/meta.svg)](https://docs.adcrunch.dev/connect/providers) [![TikTok](/providers/tiktok.svg)](https://docs.adcrunch.dev/connect/providers) [![Google Ads](/providers/gads.svg)](https://docs.adcrunch.dev/connect/providers) [![OpenAI](/providers/openai.svg)](https://docs.adcrunch.dev/connect/providers)

Gives aggregated metric rows for your organization: what an ad account, a campaign, an ad group or an ad spent over a date range, and what it returned.

Each row carries the metrics you name in `select`. The default metrics are `spend` and `impressions`. Each row also carries a `currency`.

**A metric a provider does not report reads `0`, and never null.** Google Ads and OpenAI report no reach, so `reach`, `cpp` and `frequency` read `0` on every row of theirs. Compare those three inside one provider only.

## Date range

**A date range is required.** Send `since`, and optionally `until`, both written `YYYY-MM-DD`. Or send `relative`: `last_7_days`, `last_15_days` or `last_month`. A request with neither is refused with `400 invalid_request`.

Set `interval` to `day`, `week` or `month` to get one row for each period, each carrying a `date`. A week is dated by its Monday, a month by its first day. Omit `interval` to get one row for the whole range.

## Filters and groups

`advertiserId`, `entityId`, `entityType` and `provider` are **filters**: they decide which rows are read. `breakdown` is separate: it decides how the rows are **grouped**, and which columns name the group.

| `breakdown` | Rows | Each row also carries |
| --- | --- | --- |
| omitted | one, for everything that matched | nothing else |
| a concept | one for each entity of that level, at every provider | `provider`, `advertiserId`, `entityId`, `type` |
| `provider` | one for each ad platform | `provider` |
| `advertiser` | one for each ad account | `provider`, `advertiserId` |
| a provider entity type | one for each entity of that type | `provider`, `advertiserId`, `entityId`, `type` |

### Concepts

A concept names one level with the same word at every provider. There are five: `campaigns`, `ad-groups`, `ads`, `creatives` and `asset-groups`, the names of the entity resources of this API.

| Concept | Meta | TikTok | Google Ads |
| --- | --- | --- | --- |
| `campaigns` | `campaign` | `campaign` | `campaign` |
| `ad-groups` | `adset` | `adgroup` | `ad_group` |
| `ads` | `ad` | `ad` | `ad_group_ad` |
| `creatives` | `creative` | — | — |
| `asset-groups` | — | — | `asset_group` |

`breakdown=ad-groups` answers one row for each ad group of each provider together. **Each row keeps the word of its provider** in `type`: `adset` on a Meta row, `adgroup` on a TikTok row. A provider that does not have the concept adds no row. A concept is valid with or without `provider`. `entityType` takes a concept too.

### Provider entity types

A provider entity type is the word of one provider: `campaign`, `adset` at Meta, `adgroup` at TikTok, `ad_group` at Google Ads, `ad`, and so on. It reads only the rows that carry that word, so `breakdown=ad` reads the ads of Meta and TikTok, and not the `ad_group_ad` rows of Google Ads.

A provider entity type below `campaign` needs a `provider` or an entity filter. `breakdown=adset` with no `provider` is refused with `400 invalid_request`, and its issue names the breakdowns that are available. Send `breakdown=ad-groups` to read that level at every provider.

## Money

Money is in **whole units** of the row’s `currency`, with decimals: a `spend` of `1284.57` is 1,284.57. It is the unit a `budget` takes on an entity, so a budget and a spend compare directly. The Mutations API and a Campaign Plan take the same unit.

Money comes back in the account currency of the rows it came from, and each row says which in its `currency`. A group that mixes two account currencies answers `currency: null` — the money in it was added across currencies and cannot be compared.

Send `currency` — an ISO 4217 code such as `USD` — to make them comparable. AdCrunch converts each day at the European Central Bank rate **of that day**, before adding the rows up, and every row then answers that currency.

## Advertisers

Omit `advertiserId` to read every advertiser of your organization in one call. Send it to read one, and AdCrunch then also makes sure your organization owns it. An `advertiserId` your organization does not own answers an empty list, not a failure.

## Paging

The answer is one page, `{ data, nextCursor }`, of at most `limit` rows (100 by default, 500 at most). When it holds `nextCursor`, more rows exist: send it back as `cursor`, with the same parameters, to get the next page.

The rows are in one order across every provider: the period (with an `interval`), then the advertiser and the entity of the breakdown, then the provider. So a page never splits one group, and the pages of a portfolio read hold no more than `limit` rows however many providers you connect.

The rows always come from what AdCrunch holds. A live read belongs to one advertiser, and no provider aggregates an organization.

## Query parameters

- `advertiserId` string: Filter rows to a single advertiser. Short-circuits the per-org advertiser fan-out and verifies the advertiser belongs to the caller's organization.
- `breakdown` 'campaigns' | 'ad-groups' | 'ads' | 'creatives' | 'asset-groups' | 'provider' | 'advertiser' | 'campaign' | 'adset' | 'adgroup' | 'ad_group' | 'ad_group_ad' | 'insertion_order' | 'line_item' | 'asset_group' | 'keyword' | 'audience' | 'listing_group' | 'ad' | 'creative': Group results by this level. Send a concept to read one level at every provider: `campaigns`, `ad-groups`, `ads`, `creatives` or `asset-groups`. For example, `ad-groups` reads Meta `adset`, TikTok `adgroup` and Google Ads `ad_group` together. Each row keeps the word of its provider in `type`. A concept is valid with or without `provider`, and a provider that does not have the concept adds no row. A provider-native entity type (`campaign`, `adset`, `adgroup`, `ad_group`, `ad_group_ad`, `ad`, …) reads the word of one provider. Below `campaign`, a native type needs `provider` or an entity filter. `provider` and `advertiser` group by the ad platform and by the ad account. Omit `breakdown` to aggregate every matching row into one result.
- `currency` string: Display currency (ISO 4217, e.g. `USD`). When provided, monetary metrics are converted from each row’s account currency using the ECB reference rate of the row’s own date, before any aggregation. Omit to get unconverted account-currency values — pass it whenever the organization has advertisers in more than one currency.
- `cursor` string: The `nextCursor` of the previous page. Omit it to get the first page. Send it with no change, and with the same filters as the request that answered it: a cursor from a different query gets a 400 `invalid_cursor`. Do not build or change a cursor.
- `entityId` string: Filter to a single entity (any type), or — combined with a concept or an entity-type `breakdown` — scope the breakdown to that entity’s subtree.
- `entityType` string: Filter rows to one level. Send a concept (`campaigns`, `ad-groups`, `ads`, `creatives` or `asset-groups`) to read that level at every provider, or a provider-native entity type to read the word of one provider.
- `interval` 'day' | 'week' | 'month': Bucket the time series at this granularity. Omit to collapse to a single row per group.
- `limit` integer: The greatest number of rows on the page, from 1 to 500. The default is 100. A greater value gets a 400, with `error` of `invalid_request`.
- `provider` union: Restrict to a single provider. Ignored when an entity filter (`advertiserId`, etc.) is provided — that filter already pins the provider.
  - 'meta'
  - 'tiktok'
  - 'snapchat'
  - 'gads'
  - 'dv360'
  - 'x'
  - 'openai'
- `relative` union
  - 'last_15_days'
  - 'last_month'
  - 'last_7_days'
- `select` union: Metric columns to include on each row, as a comma-separated string (e.g. `spend,clicks,ctr`) or repeated values. Defaults to `spend,impressions`.
  - string
  - string[]
- `since` string
- `until` string

## Response `200`

One page of the matching rows. This page is the last one, so it carries no `nextCursor`.

- object
  - `data` object[], required: One row for each group. An empty array means no data matched, which is a normal answer and not a failure.
    - `advertiserId` string: The advertiser this row belongs to, prefixed `acc_`. Present when `breakdown` is `advertiser`, a concept or an entity type.
    - `currency` string, nullable, required: The ISO 4217 currency the money in this row is expressed in. It is the account currency when every row in the group shares one, the currency you asked for when you sent `currency`, and **null** when the group mixes two or more account currencies. A null here means the money in this row was added up across currencies and cannot be compared — send `currency` to make it comparable.
    - `date` string: The first day of the period, `YYYY-MM-DD`. Present only when you send `interval`. A week is dated by its Monday, a month by its first day.
    - `entityId` string: The entity this row belongs to, as the bare id its provider gives it. Present when `breakdown` is a concept or an entity type.
    - `provider` string: The ad platform: `meta`, `tiktok` or `gads`. Present when `breakdown` is set to anything. https://docs.adcrunch.dev/connect/providers lists every provider and what each one reports.
    - `type` string: The provider entity type of `entityId`, for example `campaign`, `adset`, `adgroup`, `ad_group` or `ad`. Present when `breakdown` is a concept or an entity type. A concept does not change it: with `breakdown=ad-groups`, a Meta row reads `adset` and a TikTok row reads `adgroup`.
    - `spend` number: Money spent, in the row’s `currency`.
    - `clicks` number: Clicks.
    - `impressions` number: Impressions.
    - `ctr` number: Click-through rate, **as a percentage**: clicks ÷ impressions × 100. A `ctr` of 1.5 means 1.5%.
    - `cpc` number: Cost per click: spend ÷ clicks.
    - `cpm` number: Cost per thousand impressions: spend ÷ impressions × 1000.
    - `cpp` number: Cost per thousand people reached: spend ÷ reach × 1000. Google Ads and OpenAI report no reach, so this reads 0 there.
    - `cpa` number: Cost per conversion: spend ÷ conversions.
    - `roas` number: Return on ad spend: conversion value ÷ spend.
    - `reach` number: People reached. Google Ads and OpenAI do not report it, so it reads 0 there.
    - `frequency` number: Impressions ÷ reach. Google Ads and OpenAI report no reach, so this reads 0 there.
    - `conversions` number: Conversions, as the provider counts them.
    - `conversion_value` number: The value attributed to those conversions, as money.
    - `action_add_to_cart` number: Add-to-cart actions. Meta only; other providers read 0.
    - `action_add_to_cart_value` number: The value attributed to those add-to-cart actions. Meta only.
    - `action_purchase` number: Purchases. Meta only; other providers read 0.
    - `action_purchase_value` number: The value attributed to those purchases. Meta only.
    - `action_initiate_checkout` number: Checkouts started. Meta only; other providers read 0.
    - `action_initiate_checkout_value` number: The value attributed to those checkouts. Meta only.
  - `nextCursor` string: Send this value as `cursor` to get the next page. It is absent on the last page.

## Other responses

- `400`: The request does not match the schema of this operation: a field is missing or has the wrong type, or the body is not valid JSON. `error` is `invalid_request`, and `issues` names each field. Nothing was changed. Or AdCrunch cannot use the `cursor`: it cannot read it, or the cursor belongs to a different query. Then `error` is `invalid_cursor`. Send the `nextCursor` of the previous page with no change, or omit `cursor` to get the first page.
- `401`: No API key, or one that does not resolve. See the security scheme. `error` is `unauthorized`.
- `402`: The organization has no active subscription. An owner subscribes at `paywallUrl`. `error` is `organization_unentitled`.
- `403`: The caller does not hold `observe:read`. `error` is `forbidden`.

---

[API](https://skmtc.dev/adcrunch/apis/adcrunch-api.md) · [All operations](https://skmtc.dev/adcrunch/apis/adcrunch-api/llms.txt) · [OpenAPI document](https://skmtc.dev/adcrunch/apis/adcrunch-api/revisions/a420ba80914f?raw)
