# Get one campaign a watched brand sent

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

Returns one campaign an email panel observed a watched brand sending. It carries the
same figures the brand's campaign list gives for that campaign, so a page can open one
campaign without reading the whole list first.

The campaign has to be one the brand in the path sent. An identifier that belongs to
another brand's campaign comes back as not found, whether or not the panel holds it. A
workspace can read the campaigns of the brands it watches, and no others.

The panel folds a day of low-volume sending into one synthetic entry, and the campaign
list leaves those out. They stand for a day of sending rather than for a campaign
anyone sent, so they come back as not found here as well.

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;
const page = await bird.email.competitive.watchlist.brands.campaigns.list(watchlistBrandId, { range: 30, limit: 1 });
const campaign = page.data[0];
if (!campaign) throw new Error("No captured campaigns");
const report = await bird.email.competitive.watchlist.brands.campaigns.get(watchlistBrandId, campaign.id);
console.log(report);
```

### 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
campaign_id = None
for campaign in client.email.competitive.watchlist.brands.campaigns.list(watchlist_brand_id, range=30, limit=1):
    campaign_id = campaign.id
    break
if campaign_id is None:
    raise ValueError("No captured campaigns")
report = client.email.competitive.watchlist.brands.campaigns.get(watchlist_brand_id, campaign_id)
print(report.model_dump_json())
```

### 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")
}
campaignID := ""
for campaign, err := range client.Email.Competitive.Watchlist.Brands.Campaigns.List(ctx, watchlistBrandID, bird.EmailCompetitiveWatchlistBrandsCampaignsListParams{Range: 30, Limit: 1}) {
	if err != nil {
		log.Fatal(err)
	}
	if campaign.Id != nil {
		campaignID = *campaign.Id
		break
	}
}
if campaignID == "" {
	log.Fatal("No captured campaigns")
}
report, err := client.Email.Competitive.Watchlist.Brands.Campaigns.Get(ctx, watchlistBrandID, campaignID)
if err != nil {
	log.Fatal(err)
}
encoded, err := json.MarshalIndent(report, "", "  ")
if err != nil {
	log.Fatal(err)
}
fmt.Println(string(encoded))
```

### 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');
}
$page = $bird->email->competitive->watchlist->brands->campaigns->list($watchlistBrandId, ['range' => 30, 'limit' => 1])->fetch();
$campaignId = ($page->data[0] ?? null)?->getId();
if ($campaignId === null) {
    throw new \RuntimeException('No captured campaigns');
}
$report = $bird->email->competitive->watchlist->brands->campaigns->get($watchlistBrandId, $campaignId);
var_dump($report);
```

### CLI

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

### cURL

```sh
curl -X GET "https://us1.platform.bird.com/v1/email/competitive/watchlist/brands/{watchlist_brand_id}/campaigns/{campaign_id}" \
  -H "Authorization: Bearer $TOKEN"
```

## Example response `200`

```json
{
  "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
}
```

## Path parameters

- `watchlist_brand_id` (string): The watched brand that sent the campaign.
- `campaign_id` (string): The campaign to return, taken from the brand's campaign list.

## Response body

- `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.
- `subject` (string, required): The subject line the panel saw on this campaign.
- `sent_at` (string, required): When the panel first saw this campaign arrive.
- `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.
- `reach` (nullable integer, required): Estimated recipients this campaign reached, null when the panel observed the campaign but published no estimate for it.
- `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.
- `has_creative` (boolean, required): Whether the panel captured the rendered email for this campaign.
- `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.
- `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.
- `spam_rate` (nullable number, required): Share of this campaign that was filed as spam, null on the same terms.

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