> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developers.hive.co/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.hive.co/_mcp/server.

# Campaign Reporting

Campaign endpoints are **read-only**: they return reporting data and accept no writes. All of them require the `campaigns:read` scope.

Stats are served from a cache that refreshes as engagement arrives. Use `stats_refreshed_at` to tell when the numbers were last refreshed, rather than assuming they're live. A refresh doesn't necessarily change the numbers.

## Endpoints

| Channel | List campaigns          | Single campaign stats                       | Aggregate stats               |
| ------- | ----------------------- | ------------------------------------------- | ----------------------------- |
| Email   | `GET /campaigns/email/` | `GET /campaigns/email/{campaign_id}/stats/` | `GET /campaigns/email/stats/` |
| SMS     | `GET /campaigns/sms/`   | `GET /campaigns/sms/{campaign_id}/stats/`   | `GET /campaigns/sms/stats/`   |
| Ads     | `GET /campaigns/ads/`   | `GET /campaigns/ads/{campaign_id}/stats/`   | `GET /campaigns/ads/stats/`   |

### Common patterns

* **Listing**: every list endpoint accepts `limit` (`0`–`500`, default `50`), `offset` (default `0`), and `stats` (a boolean such as `true`/`false` or `1`/`0`; `true` includes the full `stats` object on every campaign; default `false`). An out-of-range `limit` or `offset`, or an invalid `stats` value, returns `422 Unprocessable Entity`. Responses include `offset`, `limit`, `total_items`, and `items`.
* **Campaign IDs**: each item's `id` is the Hive campaign ID, used as `campaign_id` in the stats endpoints.
* **Aggregates**: counts and revenue are summed, and rates are recalculated from the summed counts rather than averaged. Filters are optional and can be combined; `campaign_ids` is a comma-separated list of up to `100` IDs; duplicates are removed before the limit is checked. Datetime filters without a UTC offset are treated as UTC. Omitting all filters aggregates every campaign for the brand. Aggregate `stats_refreshed_at` is the **oldest** refresh timestamp among the contributing campaigns, so the whole total is at least that fresh. How campaigns without a refresh time are handled differs by channel; see each aggregate section.
* **Window boundaries**: the `*_after` filter is inclusive and the `*_before` filter is exclusive, so adjacent windows never double-count a campaign sent exactly on the boundary.

> **Fetching stats efficiently**
>
> Call a list endpoint without `stats`, and compare each campaign's `stats_refreshed_at` against
> what you stored. Only request stats for campaigns whose timestamp changed.

Common errors:

* `404 Not Found` (single campaign stats): no campaign with that ID exists for the authorized brand.
* `422 Unprocessable Entity` (aggregates): the `*_before` filter isn't later than the `*_after` filter, `campaign_ids` is empty or has more than 100 distinct IDs, one or more `campaign_ids` don't exist for the brand, or a datetime filter is malformed.

## Email campaigns

### List email campaigns

`GET /campaigns/email/` lists sent email campaigns, most recently sent first. An A/B test appears once, as its parent campaign; its variants aren't listed separately, and the parent's `stats` and `stats_refreshed_at` combine all its variants.

```json
{
    "offset": 0,
    "limit": 50,
    "total_items": 2,
    "items": [
        {
            "id": "9a1b2c3d4e",
            "name": "Summer Fest - Announce",
            "subject": "Tickets are live",
            "time_sent": "2026-07-01T15:00:00Z",
            "num_recipients": 48210,
            "stats_refreshed_at": "2026-07-02T03:00:00Z"
        }
    ]
}
```

| Field                        | Description                                                                                                                  |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `total_items`                | Total number of sent campaigns for the brand                                                                                 |
| `items[].id`                 | Hive campaign ID                                                                                                             |
| `items[].name`               | Campaign name                                                                                                                |
| `items[].subject`            | Email subject line, if set                                                                                                   |
| `items[].time_sent`          | ISO 8601 timestamp the campaign was sent                                                                                     |
| `items[].num_recipients`     | Number of recipients the campaign was sent to                                                                                |
| `items[].stats_refreshed_at` | ISO 8601 timestamp the cached stats were last refreshed, or `null` if there are no stats yet or no refresh time was recorded |
| `items[].stats`              | Only present when `stats=true`; same shape as the single campaign stats                                                      |

### Email campaign stats

`GET /campaigns/email/{campaign_id}/stats/` returns engagement and revenue stats for one campaign.

```json
{
    "campaign_id": "9a1b2c3d4e",
    "stats_refreshed_at": "2026-07-02T03:00:00Z",
    "stats": {
        "counts": {
            "sent": 48210,
            "delivered": 47980,
            "opened": 21455,
            "clicked": 3120,
            "bounced": 230,
            "soft_bounced": 640,
            "spam_complained": 4,
            "unsubscribed": 61
        },
        "rates": {
            "delivery_rate": 99.523,
            "open_rate": 44.717,
            "click_rate": 6.503,
            "bounce_rate": 0.479,
            "spam_complaint_rate": 0.008,
            "unsubscribe_rate": 0.127,
            "estimated_non_prefetched_open_rate": 31.884
        },
        "revenue": {
            "ecommerce": { "num_orders": 12, "total_revenue": 540.0 },
            "ticketing": { "num_orders": 340, "total_revenue": 15300.0 },
            "total": { "num_orders": 352, "total_revenue": 15840.0 }
        }
    }
}
```

| Field                                            | Description                                                                                                                             |
| ------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| `stats.counts`                                   | Recipient counts for the campaign                                                                                                       |
| `stats.counts.bounced`                           | Hard bounces only: mail that will never arrive                                                                                          |
| `stats.counts.soft_bounced`                      | Retryable delivery failures, such as a full mailbox or a temporary rejection                                                            |
| `stats.rates`                                    | Percentages rounded to 3 decimal places. Every rate is calculated against `delivered`, except `delivery_rate`, which is against `sent`. |
| `stats.rates.estimated_non_prefetched_open_rate` | Open rate estimate that discounts privacy-proxy prefetch opens, such as Apple Mail Privacy Protection                                   |
| `stats.revenue`                                  | Attributed revenue split by channel, rounded to 2 decimal places                                                                        |

> **A soft bounce is not a failed send**
>
> A soft bounce is a delivery attempt the receiving server deferred, and it's usually retried. A
> recipient who soft bounces and is then delivered on a later attempt is counted in **both**
> `soft_bounced` and `delivered`, so the two don't sum to `sent`. Soft bounces are excluded from
> `bounced` and `bounce_rate`; only `bounced` represents mail that will never arrive. Recipients
> who soft bounce repeatedly are eventually reclassified as hard bounced.

> **Null sections**
>
> `stats` is `null` for a campaign with no cached stats, and `counts`, `rates`, and `revenue` are
> each `null` when that section hasn't been computed. Engagement and revenue refresh
> independently, so a campaign can return `counts` and `rates` while `revenue` is still `null`.

### Aggregate email stats

`GET /campaigns/email/stats/` totals stats across multiple campaigns. Filters: `sent_after`, `sent_before` (ISO 8601 datetimes, filtering on send time), and `campaign_ids`.

```json
{
    "campaigns_included": 12,
    "stats_refreshed_at": "2026-08-02T03:00:00Z",
    "stats": {
        "counts": { "...": "same shape as single campaign stats" },
        "rates": { "...": "same shape as single campaign stats" },
        "revenue": { "...": "same shape as single campaign stats" }
    }
}
```

* `campaigns_included`: number of campaigns that matched the filters. An A/B test counts once, though its stats come from its variants.
* `stats_refreshed_at`: the oldest refresh timestamp across the engagement and revenue stats of the contributing campaigns (and A/B variants). Stats with no recorded refresh time are skipped; `null` if none has one.
* `stats`: same shape as single campaign stats, or `null` when no matched campaign had stats.

## SMS campaigns

### List SMS campaigns

`GET /campaigns/sms/` lists sent SMS campaigns, most recently sent first.

```json
{
    "offset": 0,
    "limit": 50,
    "total_items": 2,
    "items": [
        {
            "id": "def456abc1",
            "name": "Presale Reminder",
            "time_sent": "2026-08-01T12:00:00Z",
            "num_recipients": 200,
            "stats_refreshed_at": "2026-08-02T03:00:00Z"
        }
    ]
}
```

Fields match email campaigns, except there's no `subject`, `name` is `null` if the campaign was never named, and `num_recipients` can be `null`.

### SMS campaign stats

`GET /campaigns/sms/{campaign_id}/stats/` returns engagement and revenue stats for one SMS campaign.

```json
{
    "campaign_id": "def456abc1",
    "stats_refreshed_at": "2026-08-02T03:00:00Z",
    "stats": {
        "counts": {
            "sent": 200,
            "delivered": 194,
            "clicked": 38,
            "failed": 6,
            "opted_out": 2,
            "opted_in": 1,
            "undeliverable": 3
        },
        "rates": {
            "delivery_rate": 97.0,
            "click_rate": 19.588,
            "opt_out_rate": 1.031,
            "failed_rate": 3.093
        },
        "revenue": {
            "ecommerce": { "num_orders": 3, "total_revenue": 120.0 },
            "ticketing": { "num_orders": 25, "total_revenue": 1125.0 },
            "total": { "num_orders": 28, "total_revenue": 1245.0 }
        }
    }
}
```

| Field                                             | Description                                                                                                                             |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `stats.counts.failed`                             | Messages the carrier rejected or couldn't deliver                                                                                       |
| `stats.counts.undeliverable`                      | Numbers that can't receive SMS at all, such as landlines                                                                                |
| `stats.counts.opted_in`, `stats.counts.opted_out` | Subscription changes attributed to the campaign                                                                                         |
| `stats.rates`                                     | Percentages rounded to 3 decimal places. Every rate is calculated against `delivered`, except `delivery_rate`, which is against `sent`. |
| `stats.revenue`                                   | Attributed revenue split by channel, rounded to 2 decimal places                                                                        |

> **SMS has no opens**
>
> There's no SMS equivalent of an email open, so clicks are the engagement signal and `click_rate`
> is the headline number. For the same reason there's no `opened`, `bounced`, or
> `spam_complained`.

> **Null stats**
>
> `stats` is `null` for a campaign whose stats have never been computed. Unlike email, the
> sections are computed together, so once `stats` is present, `counts`, `rates`, and `revenue` are
> all present. Zeroed revenue means no attributed orders, not missing data.

### Aggregate SMS stats

`GET /campaigns/sms/stats/` totals stats across multiple SMS campaigns. Filters: `sent_after`, `sent_before` (filtering on send time), and `campaign_ids`.

* `campaigns_included`: number of campaigns that matched the filters, whether or not they had stats to contribute.
* `stats_refreshed_at`: the oldest refresh timestamp among campaigns with stats; campaigns whose stats were never computed are skipped. `null` when no matched campaign had stats.
* `stats`: same shape as single campaign stats, or `null` when no matched campaign had stats.

## Ad campaigns

> **Every ad figure is Meta-reported**
>
> Hive serves these numbers from its cache of Meta's campaign insights and doesn't recompute them.
> Unlike email and SMS, there's no Hive-attributed order revenue for ads: purchase figures are
> Meta's own attribution, which is why they carry the `meta_` prefix.

### List ad campaigns

`GET /campaigns/ads/` lists published Meta ad campaigns, most recently **created** first.

```json
{
    "offset": 0,
    "limit": 50,
    "total_items": 2,
    "items": [
        {
            "id": "84213",
            "name": "Summer Fest - Retargeting",
            "status": "active",
            "objective": "OUTCOME_SALES",
            "start_time": "2026-06-15T04:00:00Z",
            "stop_time": "2026-07-01T04:00:00Z",
            "budget_cents": 50000,
            "budget_type": "DAILY",
            "stats_refreshed_at": "2026-07-02T03:00:00Z"
        }
    ]
}
```

| Field                                     | Description                                                                                                                                           |
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `total_items`                             | Total number of published ad campaigns for the brand                                                                                                  |
| `items[].id`                              | Hive campaign ID                                                                                                                                      |
| `items[].name`                            | Campaign name                                                                                                                                         |
| `items[].status`                          | `active`, `paused`, `publishing`, `archived`, `deleted`, `disconnected`, `campaign_over`, `attribution_over`, or `unknown`                            |
| `items[].objective`                       | Meta campaign objective, for example `OUTCOME_SALES` or `OUTCOME_TRAFFIC`                                                                             |
| `items[].start_time`, `items[].stop_time` | ISO 8601 flight dates; either can be `null`                                                                                                           |
| `items[].budget_cents`                    | Budget in cents, or `null`                                                                                                                            |
| `items[].budget_type`                     | `DAILY` or `LIFETIME`, describing what `budget_cents` covers, or `null`                                                                               |
| `items[].stats_refreshed_at`              | ISO 8601 timestamp Hive last replaced the cached stats with fresh figures from Meta, or `null` if it hasn't refreshed since Hive began recording this |
| `items[].stats`                           | Only present when `stats=true`; same shape as single campaign stats                                                                                   |

### Ad campaign stats

`GET /campaigns/ads/{campaign_id}/stats/` returns lifetime performance stats for one ad campaign.

```json
{
    "campaign_id": "84213",
    "stats_refreshed_at": "2026-07-02T03:00:00Z",
    "stats": {
        "spend": 1240.55,
        "impressions": 184320,
        "reach": 96110,
        "frequency": 1.918,
        "clicks": 3821,
        "unique_clicks": 3104,
        "cost_per_click": 0.32,
        "cost_per_unique_click": 0.4,
        "click_through_rate": 2.073,
        "results": 412,
        "cost_per_result": 3.01,
        "meta_purchase_value": 5820.0,
        "meta_purchase_roas": 4.691
    }
}
```

| Field                                                 | Description                                                                                                                                                                                                                                                        |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `stats_refreshed_at`                                  | ISO 8601 timestamp the cached stats last came from Meta                                                                                                                                                                                                            |
| `stats.spend`                                         | Amount spent, rounded to 2 decimal places                                                                                                                                                                                                                          |
| `stats.impressions`                                   | Times the ads were shown                                                                                                                                                                                                                                           |
| `stats.reach`                                         | Unique people who saw the ads                                                                                                                                                                                                                                      |
| `stats.frequency`                                     | Average impressions per person reached, rounded to 3 decimal places                                                                                                                                                                                                |
| `stats.clicks`, `stats.unique_clicks`                 | Total clicks, and the number of distinct people who clicked                                                                                                                                                                                                        |
| `stats.cost_per_click`, `stats.cost_per_unique_click` | Spend divided by the matching click count, rounded to 2 decimal places                                                                                                                                                                                             |
| `stats.click_through_rate`                            | Clicks as a percentage of impressions, rounded to 3 decimal places                                                                                                                                                                                                 |
| `stats.results`                                       | Count of the outcome the campaign optimizes for, which depends on its objective                                                                                                                                                                                    |
| `stats.cost_per_result`                               | Spend divided by `results` (as reported by Meta), rounded to 2 decimal places                                                                                                                                                                                      |
| `stats.meta_purchase_value`                           | Purchase value Meta attributes to the campaign, rounded to 2 decimal places. Meta reports purchases under several overlapping action types, so this is the highest value among them (preferring Meta's aggregated `omni_purchase`/`purchase` types), not their sum |
| `stats.meta_purchase_roas`                            | Meta's return on ad spend: attributed purchase value divided by spend, rounded to 3 decimal places, chosen the same way as `meta_purchase_value`                                                                                                                   |

> **Null stats**
>
> `stats` is `null` for a campaign whose insights have never been cached. `spend`, `impressions`,
> `reach`, `frequency`, `clicks`, and `unique_clicks` default to `0`. Other metrics are `null`
> when Meta reported nothing for them, which is common for purchase figures on campaigns without
> conversion tracking.

### Aggregate ad stats

`GET /campaigns/ads/stats/` totals stats across multiple ad campaigns. Spend and counts are summed, and derived metrics are recalculated from those sums rather than averaged.

Filters: `created_after`, `created_before` (ISO 8601 datetimes), and `campaign_ids`. Unlike the email and SMS aggregates, which filter on send time, ad aggregates filter on when the campaign was **created**.

```json
{
    "campaigns_included": 6,
    "stats_refreshed_at": "2026-07-02T03:00:00Z",
    "stats": { "...": "same shape as single campaign stats" }
}
```

* `campaigns_included`: number of campaigns that matched the filters, whether or not they had stats to contribute.
* `stats_refreshed_at`: the oldest refresh timestamp among the contributing campaigns, or `null` if any of them has no recorded refresh time.
* `stats`: same shape as single campaign stats, or `null` when no matched campaign had stats.

> **Reach is summed, not deduplicated**
>
> Someone reached by three campaigns is counted three times in the aggregate `reach`, because Meta
> reports reach per campaign and the overlap between campaigns can't be derived from those
> figures.