# Get the campaigns a watched brand sent

`GET /v1/email/competitive/watchlist/brands/{watchlist_brand_id}/campaigns`

Returns a page of campaigns an email panel observed a watched brand sending over
the period, in the requested order. Sampled statistics describe eligible campaigns
in the first 300 newest panel rows for each tracked domain, independently of the page.

Each campaign is one send the panel saw reach real inboxes, so the subject and
timing are what the brand's subscribers received rather than anything the brand
published. Volume and read rate are panel estimates, fetched while the request
runs.

The panel folds a day's low-volume sending into a single synthetic entry with no
creative and no volume. Those are left out, so the count here is lower than the
number of rows the panel holds and describes campaigns a person would recognise
as campaigns.

API-key calls require Insights preview access for your organization.

## Code samples

### TypeScript

```ts
// Requires Insights preview access for the organization.
const watchlist = await bird.email.competitive.watchlist.get({ range: 30 });
const entry = watchlist.data.find((row) => row.name === "Everlane" && row.watchlist_brand_id);
if (!entry?.watchlist_brand_id) throw new Error("Add Everlane to the watchlist first");
const watchlistBrandId = entry.watchlist_brand_id;
for await (const campaign of bird.email.competitive.watchlist.brands.campaigns.list(watchlistBrandId, { range: 30, limit: 25 })) {
  console.log(campaign.id);
}
```

### Python

```py
# Requires Insights preview access for the organization.
watchlist = client.email.competitive.watchlist.get(range=30)
entry = next((row for row in watchlist.data if row.name == "Everlane" and row.watchlist_brand_id), None)
if entry is None or entry.watchlist_brand_id is None:
    raise ValueError("Add Everlane to the watchlist first")
watchlist_brand_id = entry.watchlist_brand_id
for campaign in client.email.competitive.watchlist.brands.campaigns.list(watchlist_brand_id, range=30, limit=25):
    print(campaign.id)
```

### Go

```go
// Requires Insights preview access for the organization.
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
	log.Fatal(err)
}
ctx := context.Background()
watchlist, err := client.Email.Competitive.Watchlist.Get(ctx, bird.EmailCompetitiveWatchlistGetParams{Range: 30})
if err != nil {
	log.Fatal(err)
}
watchlistBrandID := ""
if watchlist.Data != nil {
	for _, row := range *watchlist.Data {
		if row.Name != nil && *row.Name == "Everlane" && row.WatchlistBrandId != nil {
			watchlistBrandID = string(*row.WatchlistBrandId)
			break
		}
	}
}
if watchlistBrandID == "" {
	log.Fatal("Add Everlane to the watchlist first")
}
for campaign, err := range client.Email.Competitive.Watchlist.Brands.Campaigns.List(ctx, watchlistBrandID, bird.EmailCompetitiveWatchlistBrandsCampaignsListParams{Range: 30, Limit: 25}) {
	if err != nil {
		log.Fatal(err)
	}
	if campaign.Id != nil {
		fmt.Println(*campaign.Id)
	}
}
```

### PHP

```php
// Requires Insights preview access for the organization.
$watchlist = $bird->email->competitive->watchlist->get(['range' => 30]);
$watchlistBrandId = null;
foreach ($watchlist->getData() ?? [] as $row) {
    if ($row->getName() === 'Everlane' && $row->getWatchlistBrandId() !== null) {
        $watchlistBrandId = $row->getWatchlistBrandId();
        break;
    }
}
if ($watchlistBrandId === null) {
    throw new \RuntimeException('Add Everlane to the watchlist first');
}
foreach ($bird->email->competitive->watchlist->brands->campaigns->list($watchlistBrandId, ['range' => 30, 'limit' => 25]) as $campaign) {
    echo $campaign->getId(), "\n";
}
```

### CLI

```sh
bird email competitive watchlist brands campaigns list <watchlist-brand-id>
```

### cURL

```sh
curl -X GET "https://us1.platform.bird.com/v1/email/competitive/watchlist/brands/{watchlist_brand_id}/campaigns" \
  -H "Authorization: Bearer $TOKEN" \
  --url-query "range=30" \
  --url-query "sort=sent_at" \
  --url-query "order=desc" \
  --url-query "limit=25"
```

## Example response `200`

```json
{
  "period": {
    "days": 30,
    "from": "2026-07-13T09:00:00Z",
    "to": "2026-08-12T09:00:00Z"
  },
  "panel_status": "ok",
  "captured": 38,
  "promo_rate": 0.64,
  "truncated": false,
  "data": [
    {
      "id": "3914827265",
      "subject": "The Summer Sale: 40% off everything",
      "sent_at": "2026-08-09T14:02:00Z",
      "image_url": "https://images.example.com/creatives/c154c8c4-6356-40e6-92d2-7c6727ec36ca.jpg",
      "reach": 410000,
      "read_rate": 0.228,
      "has_creative": true,
      "discount_percent": 40,
      "inbox_rate": 0.879,
      "spam_rate": 0.121
    }
  ],
  "next_cursor": "eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE0OjAzOjEwWlwiIiwiaSI6IjAxOTJmM2IxLTRjN2UtN2EyYi05ZDYxLThmM2E1YzJlN2I0MCJ9",
  "prev_cursor": null,
  "refresh_cursor": "eyJ2IjoxLCJzIjoiXCIyMDI2LTA1LTI1VDE2OjQyOjAxWlwiIiwiaSI6IjAxOTJmM2IxLTllMDQtN2NkMy1iODE3LTJhNmY0ZDFjOGUwOSJ9"
}
```

## Path parameters

- `watchlist_brand_id` (string): The watchlist entry whose campaigns to return.

## Query parameters

- `range` (integer)

  How many days back the response covers, counting from now. One of three fixed trend windows rather than an open date range, matching how a competitive-intelligence chart is read. Defaults to 30.

  Possible values: `7`, `30`, `90`
- `sort` (string)

  Field to sort campaigns by.

  Possible values: `sent_at`
- `order` (string)

  Sort direction. Defaults to `desc`, which sorts from newest to oldest or largest to smallest, depending on the selected sort field.

  Possible values: `asc`, `desc`
- `limit` (integer): Maximum number of items to return per page.
- `starting_after` (string): Cursor from the `next_cursor` field of a previous list response. Returns items immediately after the cursor position in the current sort order.
- `ending_before` (string): Cursor from the `prev_cursor` or `refresh_cursor` field of a previous list response. Returns items immediately before the cursor position in the current sort order. `prev_cursor` returns the preceding page. `refresh_cursor` anchors at the first row of that response, which on a newest-first sort is how to fetch the items that have appeared since.

## Response body

- `period` (object, required): The rolling period used for this request.
- `period.days` (integer, required): Length of the period in days.
- `period.from` (string, required): Start of the period, inclusive.
- `period.to` (string, required): End of the period, exclusive. Daily figures therefore run through the previous whole UTC day and never include the one in progress.
- `panel_status` (string, required)

  For this campaign feed, no_data means the requested page is empty; it does not mean the whole period has no campaigns.

  Possible values: `ok`, `not_in_panel`, `no_data`, `unavailable`
- `captured` (integer, required): Number of eligible campaigns in the first 300 newest panel rows for each tracked domain. This sampled value is independent of the returned page.
- `promo_rate` (nullable number, required): Fraction of captured campaigns whose subject leads with a discount. Null when captured is zero. This sampled value is independent of the returned page.
- `truncated` (boolean, required): Whether the sampled statistics or returned page omit part of the requested collection. Use next_cursor to determine whether another page is available.
- `data` (array of object, required): Campaigns in this page, in the requested order.
- `data.id` (string, required)

  The identifier for this campaign. Use it to fetch this one campaign on its own.

  It is a string, and it needs to stay one. The values are long enough that
  JavaScript, and any other language that stores every number as a floating point
  value, will round them, and a rounded identifier matches no campaign at all.
  Compare it and pass it back as text.
- `data.subject` (string, required): The subject line the panel saw on this campaign.
- `data.sent_at` (string, required): When the panel first saw this campaign arrive.
- `data.image_url` (nullable string, required): Where the panel's capture of the rendered email can be fetched, null when it captured none. Panels image only some of what they observe, so an absent creative is an ordinary outcome rather than a failed one. The image is served from the panel's own host rather than from ours, so a page embedding it has to allow that host.
- `data.reach` (nullable integer, required): Estimated recipients this campaign reached, null when the panel observed the campaign but published no estimate for it.
- `data.read_rate` (nullable number, required): Estimated share of recipients who read this campaign, null when the panel published no rate for it. Panel read rates count dwell time, so they do not move with the automatic opens that inflate a sender's own open rate.
- `data.has_creative` (boolean, required): Whether the panel captured the rendered email for this campaign.
- `data.discount_percent` (nullable number, required): The discount the subject line leads with, null when it names none. Read from the subject text, so it finds a stated offer and not one revealed inside the email.
- `data.inbox_rate` (nullable number, required): Share of this campaign that reached an inbox, null when the panel observed it without recording where it landed. It describes this send rather than the brand's domain, so a single bad campaign is visible against a brand whose overall placement still looks healthy.
- `data.spam_rate` (nullable number, required): Share of this campaign that was filed as spam, null on the same terms.
- `next_cursor` (nullable string, required): Cursor for the next page. Pass back as `starting_after` to advance forward. `null` when no next page exists.
- `prev_cursor` (nullable string, required): Cursor for the previous page. Pass back as `ending_before` to step backward. `null` when no previous page exists.
- `refresh_cursor` (nullable string, required): Refresh anchor, the first row of this response. Pass back as `ending_before` to fetch what precedes it in the current sort order. On a newest-first sort those are the items that have appeared since; on any other sort they are the items that sort earlier, so refreshing such a list means re-fetching it instead. Non-`null` whenever `data` is non-empty; `null` only on an empty page. Distinct from `prev_cursor`.

## Related resources

- [Should I use a Bird SDK or call the API directly?](/explained/platform/should-i-use-an-sdk-or-call-the-api-directly) (answer)
- [Build your first integration](/learn/paths/integration) (course)
- [Send your first email](/docs/get-started/send-your-first-email) (docs)

[Get an implementation brief](/learn/workspace?topic=api-basics)
