Sign inGet Started

Get the campaigns a watched brand sent

GET
/v1/email/competitive/watchlist/brands/{watchlist_brand_id}/campaigns
// 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);
}
Response200
{
  "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"
}

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

Parameters

watchlist_brand_idstring

The watchlist entry whose campaigns to return.

Query Parameters

rangeinteger

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

sortstring

Field to sort campaigns by.

Possible values: sent_at

orderstring

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

limitinteger

Maximum number of items to return per page.

starting_afterstring

Cursor from the next_cursor field of a previous list response. Returns items immediately after the cursor position in the current sort order.

ending_beforestring

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 Payload

period
object
required

The rolling period used for this request.

Show child attributes
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 100 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 contains a recognized percentage-discount offer. Dollar discounts and free-shipping offers do not count. 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.

Show child attributes
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 first recognized percentage-discount offer in the subject, null when none is recognized. Does not detect dollar discounts, free shipping or offers revealed only 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.

Continue with the documentation, guides and examples for this topic.