# Performance tracking

> Every campaign recipient is sent as an individual tracked email, so opens, clicks, bounces, and complaints surface per-contact in the dashboard and via webhooks.

A campaign is not a single send — it fans out into **one tracked email per recipient**. Each of those emails is logged exactly like a transactional [`POST /emails`](https://www.mailblastr.com/docs/api/emails-send) send, with its own `id`, status, and event timeline.

That means campaign performance reuses the same delivery, open, and click tracking as the rest of the platform — there is no separate analytics surface to learn.

## What is tracked

- **Delivery** — `email.sent`, `email.delivered`, and `email.delivery_delayed` per recipient.
- **Engagement** — `email.opened` and `email.clicked`, when open/click tracking is enabled on the sending domain.
- **Negative signals** — `email.bounced` and `email.complained`, which also add the address to your account suppression list.
- **Unsubscribes** — recipients who opt out via the per-campaign unsubscribe link, flipping that contact to `unsubscribed` in the audience.

> **Note:** Open and click tracking is a per-domain setting. If a campaign shows deliveries but no opens, confirm tracking is enabled on the from-domain under Domains.

## Headline metrics

Open a sent campaign in the dashboard to see its performance right away. The headline insights are:

- **Emails delivered** — how many copies the receiving servers accepted.
- **Open rate** — the share of delivered emails that were opened.
- **Click rate** — the share of delivered emails with at least one tracked-link click.
- **Unsubscribed** — how many recipients opted out from this campaign.

> **Warning:** Open rates can be inaccurate. Some inbox providers pre-fetch or proxy the tracking pixel (e.g. Apple Mail Privacy Protection), inflating opens, while others block it entirely, suppressing them. Treat open rate as a directional signal, not an exact count.

## Reading results

Each recipient email carries an event log you can read the same way as any other email:

- In the **dashboard**, open the campaign to see per-recipient status and aggregate delivery, open, and click counts.
- Via the **API**, retrieve an individual recipient email with [`GET /emails/:id`](https://www.mailblastr.com/docs/api/emails-get) to inspect its `last_event` and status.
- Via **webhooks**, subscribe to the `email.*` events to stream delivery and engagement to your own systems in real time. See [Event types](https://www.mailblastr.com/docs/webhooks/events).

## Aggregate stats endpoint

For a single rolled-up view, call `GET /campaigns/:id/stats`. It returns the campaign’s aggregate open, click, and unsubscribe counts (with the underlying delivered/sent totals) plus the top clicked links — the same numbers shown on the dashboard, fetched in one request.

**Node.js**

```js
import { MailBlastr } from 'mailblastr';

const mb = new MailBlastr('mb_xxxxxxxxx');

const { data, error } = await mb.campaigns.stats('CAMPAIGN_ID');
console.log({ data, error });
```

**Ruby**

```ruby
require "mailblastr"

Mailblastr.api_key = "mb_xxxxxxxxx"

Mailblastr::Campaigns.stats("CAMPAIGN_ID")
```

**PHP**

```php
$mailblastr = Mailblastr::client('mb_xxxxxxxxx');

$mailblastr->campaigns->stats('CAMPAIGN_ID');
```

**Python**

```python
import mailblastr

mailblastr.api_key = "mb_xxxxxxxxx"

mailblastr.Campaigns.stats("CAMPAIGN_ID")
```

**Go**

```go
package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    req, _ := http.NewRequest("GET", "https://api.mailblastr.com/campaigns/CAMPAIGN_ID/stats", nil)
    req.Header.Set("Authorization", "Bearer mb_xxxxxxxxx")
    res, _ := http.DefaultClient.Do(req)
    defer res.Body.Close()
    out, _ := io.ReadAll(res.Body)
    fmt.Println(string(out))
}
```

**Rust**

```rust
use reqwest::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let res = Client::new()
        .get("https://api.mailblastr.com/campaigns/CAMPAIGN_ID/stats")
        .header("Authorization", "Bearer mb_xxxxxxxxx")
        .send()
        .await?;
    println!("{}", res.text().await?);
    Ok(())
}
```

**Java**

```java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.mailblastr.com/campaigns/CAMPAIGN_ID/stats"))
    .header("Authorization", "Bearer mb_xxxxxxxxx")
    .method("GET", HttpRequest.BodyPublishers.noBody())
    .build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
```

**.NET**

```csharp
using System.Net.Http;
using System.Text;

var client = new HttpClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://api.mailblastr.com/campaigns/CAMPAIGN_ID/stats");
request.Headers.Add("Authorization", "Bearer mb_xxxxxxxxx");
var response = await client.SendAsync(request);
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

**cURL**

```bash
curl -X GET 'https://api.mailblastr.com/campaigns/CAMPAIGN_ID/stats' \
  -H 'Authorization: Bearer mb_xxxxxxxxx'
```

**CLI**

```bash
mailblastr campaigns stats CAMPAIGN_ID
```

Because bounces and complaints feed account-wide suppression, contacts that hard-bounce or complain are automatically excluded from subsequent campaigns — keeping your sender reputation healthy without manual list hygiene.
