# Get campaign analytics

`GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/analysis`

Aggregate counts for a campaign: lead states, funnel counts and rates, reply sentiment, per-channel email and LinkedIn stats, and a breakdown per A/B variation. There's no daily timeline and no individual replies.

Rates are `null` when their denominator is zero.

- Required scope: `campaigns:read`

## Request

**cURL**

```bash
curl "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/analysis" \
  -H "Authorization: Bearer $VICTORIA_API_KEY"
```

**Node.js**

```javascript
const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/analysis", {
  headers: {
    Authorization: `Bearer ${process.env.VICTORIA_API_KEY}`,
  },
});

const data = await response.json();
```

**Python**

```python
import os

import requests

response = requests.get(
    "https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/analysis",
    headers={
        "Authorization": f"Bearer {os.environ['VICTORIA_API_KEY']}",
    },
)
data = response.json()
```

## Headers

- `Authorization` (string, required): `Bearer` followed by a space and your API key, for example `Bearer vk_…`.

## Path parameters

- `campaign_id` (string · uuid, required): ID of the campaign.

## Query parameters

- `date_filter` (string or null, optional, at most 64 characters, example `"30d"`): Count from this point onwards: a number of days such as `7d`, `30d` or `90d`, or an ISO 8601 timestamp. Answers `400 INVALID_DATE_FILTER` if it can't be parsed.
- `variation` (string or null, optional, at most 32 characters, example `"a"`, one of `"a"`, `"b"`, `"unassigned"`, `"all"`): Limit the counts to one A/B variation. Answers `400 INVALID_VARIATION` for any other value.

## Response

### `200`

```json
{
  "campaign_id": "550e8400-e29b-41d4-a716-446655440000",
  "analysis": {
    "by_variation": {},
    "channels": {
      "email": {
        "open_rate_pct": 1,
        "opened": 1,
        "positive_leads": 1,
        "replied_leads": 1,
        "sent": 1
      },
      "linkedin": {
        "accept_rate_pct": 1,
        "accepted": 1,
        "connected_leads": 1,
        "messages": 1,
        "positive_leads": 1,
        "replied_leads": 1,
        "requests": 1
      }
    },
    "funnel": {
      "contacted": 1,
      "enrolled": 1,
      "meeting_rate_pct": 1,
      "meetings": 1,
      "positive": 1,
      "positive_rate_pct": 1,
      "replied": 1,
      "reply_rate_pct": 1
    },
    "leads": {
      "active": 1,
      "completed": 1,
      "enrolled": 1,
      "not_contacted": 1,
      "paused": 1
    },
    "replies": {
      "negative": 1,
      "neutral": 1,
      "ooo": 1,
      "positive": 1,
      "total": 1
    },
    "window": {
      "from": "2026-01-14T19:30:00+00:00",
      "to": "2026-01-14T19:30:00+00:00",
      "variation": "a"
    }
  },
  "success": true
}
```

- `campaign_id` (string, required)
- `analysis` (object or null, optional): Aggregate analytics for one campaign, counted by each lead's _first_ event of a kind inside the window.
  - `by_variation` (object or null, optional): Per-arm breakdown keyed by A/B arm: 'a', 'b', 'unassigned' (leads with no arm yet)
  - `channels` (object or null, optional)
    - `email` (object or null, optional)
      - `open_rate_pct` (number or null, optional)
      - `opened` (integer or null, optional)
      - `positive_leads` (integer or null, optional)
      - `replied_leads` (integer or null, optional)
      - `sent` (integer or null, optional)
    - `linkedin` (object or null, optional)
      - `accept_rate_pct` (number or null, optional)
      - `accepted` (integer or null, optional)
      - `connected_leads` (integer or null, optional)
      - `messages` (integer or null, optional)
      - `positive_leads` (integer or null, optional)
      - `replied_leads` (integer or null, optional)
      - `requests` (integer or null, optional): Connection requests sent
  - `funnel` (object or null, optional)
    - `contacted` (integer or null, optional)
    - `enrolled` (integer or null, optional)
    - `meeting_rate_pct` (number or null, optional): meetings / positive; null when there were no positives
    - `meetings` (integer or null, optional)
    - `positive` (integer or null, optional)
    - `positive_rate_pct` (number or null, optional): positive / contacted; null when nobody was contacted
    - `replied` (integer or null, optional)
    - `reply_rate_pct` (number or null, optional): replied / contacted; null when nobody was contacted
  - `leads` (object or null, optional)
    - `active` (integer or null, optional)
    - `completed` (integer or null, optional)
    - `enrolled` (integer or null, optional)
    - `not_contacted` (integer or null, optional)
    - `paused` (integer or null, optional)
  - `replies` (object or null, optional)
    - `negative` (integer or null, optional)
    - `neutral` (integer or null, optional)
    - `ooo` (integer or null, optional): Out-of-office replies
    - `positive` (integer or null, optional)
    - `total` (integer or null, optional)
  - `window` (object or null, optional)
    - `from` (string · date-time or null, optional): Start of the window; null = all time
    - `to` (string · date-time or null, optional): End of the window; null = now
    - `variation` (string or null, optional, one of `"a"`, `"b"`, `"unassigned"`, `"all"`, `null`): The A/B arm the report covers: 'a', 'b', 'unassigned', or 'all'
- `success` (boolean, optional, default `true`)

## Errors

Errors share one JSON body: `success`, `error`, `message`, optional `details`, and `request_id`. For example:

```json
{
  "success": false,
  "error": "UNAUTHORIZED",
  "message": "Invalid or inactive API key",
  "request_id": "734d11a1-54d4-414c-9e8d-4e1ada977db4"
}
```

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `VALIDATION_ERROR` | The request didn't match the endpoint's schema: a missing or malformed field, a bad query parameter, or a value out of range. `details.errors` lists each failing field with its `field`, `message` and `type`. |
| 400 | `INVALID_DATE_FILTER` | `date_filter` must be a number of days such as `30d`, or an ISO 8601 timestamp. |
| 400 | `INVALID_VARIATION` | `variation` must be one of `a`, `b`, `unassigned` or `all`. |
| 401 | `UNAUTHORIZED` | The `Authorization` header is missing or malformed, or the API key is unknown or has been deactivated. |
| 401 | `API_KEY_EXPIRED` | The API key is past its expiry date. Create a new key in the Victoria AI app. |
| 403 | `INSUFFICIENT_SCOPE` | The API key doesn't have the scope this endpoint requires, such as `leads:write`. |
| 403 | `ORGANIZATION_DEACTIVATED` | The organization that owns this API key has been deactivated. |
| 404 | `CAMPAIGN_NOT_FOUND` | No campaign with this ID exists in your organization. |
| 404 | `NOT_FOUND` | No endpoint matches the path. A resource ID in the path that isn't a valid UUID also answers `NOT_FOUND`. |
| 429 | `RATE_LIMITED` | Too many requests for this API key: more than 100 a minute to one endpoint, or 600 a minute in total. Retry after the number of seconds in the `Retry-After` header. See [Rate limits](https://docs.versionseven.ai/guides/rate-limits). |
| 500 | `INTERNAL_ERROR` | Something failed on our side. The response never includes internal details; quote its `request_id` when you contact support. |
| 503 | `UPSTREAM_TIMEOUT` | A service the API depends on timed out. The request is safe to retry. |
| 503 | `AUTH_UNAVAILABLE` | The API key couldn't be checked because the authentication service was unavailable. The request is safe to retry. |

## Response headers

| Header | Description |
| --- | --- |
| `X-Request-ID` | Correlation ID for the request, also returned as `request_id` in error bodies. Send your own `X-Request-ID`, up to 128 letters, digits, `.`, `_`, `:` or `-`, and it's used instead. |
| `RateLimit-Limit` | Requests allowed to this endpoint per minute. |
| `RateLimit-Remaining` | Requests you can still send to this endpoint right now. |
| `RateLimit-Reset` | Seconds until the endpoint's full limit is available again. |
| `Retry-After` | On a `429`: seconds to wait before retrying. |
