---
title: "Live read: List an advertiser’s ad groups"
method: GET
path: "/observe/{advertiserId}/ad-groups"
tags: ["Ad groups"]
---

# Live read: List an advertiser’s ad groups

`GET /observe/{advertiserId}/ad-groups`

## Request

- Base URL: `https://api.adcrunch.dev`
- URL: `https://api.adcrunch.dev/observe/{advertiserId}/ad-groups`
- 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)

Asks the provider for the ad groups of one ad account, now. This is a **live read**: the path names the advertiser, so the answer is what the provider holds, not what AdCrunch holds.

An advertiser that your organization does not own answers `404`, with `error` of `not_found`. An advertiser that does not exist answers the same. The answer does not tell the two apart.

Send `campaignId` to read one campaign’s ad groups.

A live row carries no `createdAt`, `updatedAt` or `deletedAt`, because AdCrunch stores nothing for it. Each request counts against the rate limits of the provider.

For the stored rows of the same advertiser, read `/observe/ad-groups?advertiserId=…`. That path takes every `status`, and the Status section below lists the statuses this one takes.

`limit` and `cursor` page the answer. Send `nextCursor` back as `cursor` to get the next page. A live cursor is good only for a live read. At TikTok, every page keeps the page size of the first.

## Status

`status` selects the status an entity is set to, not whether it delivers, and every row of the answer carries it. A provider takes a status only where one request to it selects exactly the entities in that status. Any other status answers `501`.

The stored read, `/observe/ad-groups?advertiserId=…&status=…`, takes every status. A status that a provider never uses answers an empty list.

| Provider | Takes `status` |
| --- | --- |
| Meta | none yet |
| TikTok | `DELETED`, `ARCHIVED` |
| Google Ads | `ACTIVE`, `PAUSED`, `DELETED`, `ARCHIVED` |

## Path parameters

- `advertiserId` string, required: The ad account to read, prefixed `acc_`. It must belong to your organization. One that does not answers `404`, with `error` of `not_found`.

## Query parameters

- `ids` string: Ids to filter on, separated by commas. An id is the bare id its provider gives.
- `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.
- `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`.
- `status` 'ACTIVE' | 'PAUSED' | 'DELETED' | 'ARCHIVED': Read one status, normalized across the providers: the status the entity is set to, not whether it delivers. Every row of the answer carries this status. A provider that cannot select this status exactly answers `501`; the description of this operation lists what each provider selects.
- `campaignId` string: Read only what this campaign holds. Send the bare id its provider gives, which is the `campaignId` every row answers.

## Response `200`

The matching ad groups of this advertiser.

- object
  - `data` object[], required: The matching rows. From the store, the row AdCrunch stored last comes first; from the provider, the provider’s own order. An empty array means nothing matched, which is a normal answer and not a failure.
    - `advertiserId` string, required: The advertiser that owns it, prefixed `acc_`.
    - `createdAt` number, nullable, required: When AdCrunch first stored this row. **Null when the row was read live**: AdCrunch holds no copy of it. `createdAt`, `updatedAt` and `deletedAt` are AdCrunch’s own times, so all three are null together on a live row.
    - `createdTime` number, nullable, required: When the provider created the entity. This is the provider’s own time, so a live row carries it. Null where the provider reports none.
    - `currency` string, nullable, required: The account currency, ISO 4217. Every row of one advertiser carries the same one. Null when AdCrunch does not know it yet.
    - `deletedAt` number, nullable, required: When AdCrunch marked the row deleted. A listing never carries a deleted row, so this is null.
    - `id` string, required: The id the provider gives, with no prefix. It is unique for one provider and one type, and it may legitimately recur across two types or two providers.
    - `name` string, required: The name at the provider.
    - `path` string, required: The ancestors and this entity, ids joined by `/`, oldest first. This is what makes a subtree one string comparison.
    - `provider` string, required: The ad platform: `meta`, `tiktok` or `gads`. Those three are the providers whose entities AdCrunch reads, and https://docs.adcrunch.dev/connect/providers says how deep each one goes.
    - `status` 'ACTIVE' | 'PAUSED' | 'DELETED' | 'ARCHIVED', required: The status, normalized across the providers. TikTok `ENABLE` and `DISABLE` read here as `ACTIVE` and `PAUSED`.
    - `updatedAt` number, nullable, required: When AdCrunch last rewrote this row. Null if it never changed.
    - `updatedTime` number, nullable, required: When the provider last edited the entity. Null where the provider reports none.
    - `bidAmount` number, nullable, required: The amount that the bid strategy takes: a bid cap or a cost cap, in **whole units** of `currency`, the unit of `budget`. A bid is per optimization event, and per 1,000 impressions when the optimization goal is impressions or reach. Null for a strategy that takes no amount or a ROAS floor, and when AdCrunch does not know the account currency. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads.
    - `bidStrategy` string, nullable, required: How the provider bids for this entity, in the provider’s word: Meta `LOWEST_COST_WITHOUT_CAP` (Highest volume), `LOWEST_COST_WITH_BID_CAP` (Bid cap), `COST_CAP` (Cost per result goal) or `LOWEST_COST_WITH_MIN_ROAS` (ROAS goal). The strategy sits where the budget sits: on the campaign when the campaign carries the budget, otherwise on each ad set. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose bid AdCrunch does not read yet.
    - `budget` number, nullable, required: The budget this entity carries itself, in **whole units** of `currency`: `50` is 50.00, never 50 cents. It is the unit of `spend` on an Insight, so a budget and a spend compare directly. AdCrunch converts each provider’s own unit — Meta’s minor unit, TikTok’s whole units and Google Ads’ micros — so every provider answers the same unit here. Null when the entity carries no budget of its own: `budgetLevel` then says where its budget lives. A campaign answers a budget only when its `budgetLevel` is `campaign`, so a budget always paces delivery at the level that answers it. Null also when AdCrunch does not know the account currency yet, because nobody can read a budget with no currency. At Google Ads one budget may be shared by several campaigns, and each of them answers the whole shared amount. **A stored budget can be older than the rest of the row.** Meta does not mark a campaign changed when only its budget changes, and AdCrunch does not yet follow a Google Ads budget change, so the ingestion can miss a budget edited in the provider’s own interface. A live read answers the budget the provider holds now. AdCrunch takes a budget in the same whole units when it writes one, so a budget you read here goes back with no conversion.
    - `budgetLevel` 'campaign' | 'ad_group', nullable, required: Where the budget of this entity’s campaign lives. `campaign` when the campaign carries it — Meta Advantage campaign budget, TikTok Campaign Budget Optimization, and every Google Ads campaign. `ad_group` when each ad group carries its own. Null on a TikTok campaign whose payload does not say.
    - `budgetType` 'daily' | 'lifetime', nullable, required: `daily` for a budget spent each day, `lifetime` for one spent over the whole life of the entity. Null exactly when `budget` has no provider value to describe.
    - `campaignId` string, nullable, required: The campaign it belongs to, as the bare id its provider gives. Null only when the stored path of the row names no campaign.
    - `endTime` string, nullable, required: When delivery of the ad set stops, as an ISO 8601 timestamp that the provider writes: Meta `2026-12-31T23:59:00+0100`. Null when the ad set has no end. It goes back to a schedule change as it is. Null on a stored row: AdCrunch keeps the schedule in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose schedule AdCrunch does not read yet.
    - `roasFloor` number, nullable, required: The lowest return on ad spend that the provider bids for, as a ratio: `1.5` is a value of 1.5 times the spend. Meta `LOWEST_COST_WITH_MIN_ROAS` only, and null for every other strategy. Null on a stored row: AdCrunch keeps the bid in the stored payload only, and a live read answers it.
    - `startTime` string, nullable, required: When delivery of the ad set starts, as an ISO 8601 timestamp that the provider writes: Meta `2026-10-01T00:00:00+0200`. It goes back to a schedule change as it is. Null on a stored row: AdCrunch keeps the schedule in the stored payload only, and a live read answers it. Null at TikTok and Google Ads, whose schedule AdCrunch does not read yet.
    - `type` string, required: The type the provider uses, kept as the provider writes it: `adset` at Meta, `adgroup` at TikTok and `ad_group` at Google Ads. The resource names the concept, and the row keeps the provider’s own word, because a Meta `adset` and a Google Ads `ad_group` are not the same object.
  - `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`.
- `404`: Your organization does not own the advertiser in the path, or it does not exist. The answer does not tell the two apart. `error` is `not_found`. AdCrunch asks the provider nothing.
- `409`: This advertiser has no usable connection to its provider. A person must reconnect it. The stored read of the same resource, with `advertiserId`, still answers what AdCrunch holds.
- `501`: AdCrunch cannot read this resource live from this advertiser’s provider. It is permanent. Read the stored copy instead, at the same resource without the advertiser in the path and with `advertiserId`.
- `502`: The provider refused the request, or could not answer it. Often transient, so a retry is worth making.

---

[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)
