# Get daily stats

`GET https://api.versionseven.ai/v1/campaigns/{campaign_id}/daily-stats`

Daily activity and outcome counts for a campaign, one row per UTC day. Days with no activity are omitted rather than zero-filled. The rollup is refreshed hourly, so today's row is flagged `partial`.

`from` and `to` bound the window (ISO dates; at most 400 days, `from` not after `to`, else `400 INVALID_WINDOW`). `group_by` splits each day by `sender` or `variation`; the default `none` returns one row per day (`400 INVALID_GROUP_BY` otherwise).

- Required scope: `campaigns:read`

## Request

**cURL**

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

**Node.js**

```javascript
const response = await fetch("https://api.versionseven.ai/v1/campaigns/550e8400-e29b-41d4-a716-446655440000/daily-stats", {
  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/daily-stats",
    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

- `from` (string or null, optional, at most 64 characters, example `"2026-09-01"`): First day (YYYY-MM-DD, or an ISO-8601 datetime whose UTC date is used). Default: 30 days before `to`
- `to` (string or null, optional, at most 64 characters, example `"2026-09-30"`): Last day, inclusive (YYYY-MM-DD, or an ISO-8601 datetime whose UTC date is used). Default: today, UTC
- `group_by` (string or null, optional, at most 32 characters, example `"sender"`, one of `"none"`, `"sender"`, `"variation"`): 'none' (default): one row per day. 'sender': one row per day and sender. 'variation': one row per day and A/B arm

## Response

### `200`

```json
{
  "campaign_id": "550e8400-e29b-41d4-a716-446655440000",
  "window": {
    "from": "string",
    "to": "string",
    "timezone": "UTC"
  },
  "count": 0,
  "days": [
    {
      "day": "string",
      "emails_opened": 1,
      "emails_sent": 1,
      "leads_first_contacted": 1,
      "leads_first_positive": 1,
      "leads_first_replied": 1,
      "li_connection_requests": 1,
      "li_connections_accepted": 1,
      "li_messages": 1,
      "meetings_booked": 1,
      "partial": true,
      "platform": "string",
      "replies_negative": 1,
      "replies_neutral": 1,
      "replies_ooo": 1,
      "replies_positive": 1,
      "sender_key": "string",
      "variation": "string"
    }
  ],
  "group_by": "none",
  "success": true
}
```

- `campaign_id` (string, required)
- `window` (object, required)
  - `from` (string, required): First day covered, YYYY-MM-DD
  - `to` (string, required): Last day covered, YYYY-MM-DD
  - `timezone` (string, optional, default `"UTC"`): Days are UTC calendar days
- `count` (integer, optional, default `0`)
- `days` (array of objects, optional)
  - `day` (string or null, optional): YYYY-MM-DD, UTC
  - `emails_opened` (integer or null, optional)
  - `emails_sent` (integer or null, optional)
  - `leads_first_contacted` (integer or null, optional): Leads whose first touch was that day
  - `leads_first_positive` (integer or null, optional): Leads whose first positive reply came that day
  - `leads_first_replied` (integer or null, optional): Leads whose first reply came that day
  - `li_connection_requests` (integer or null, optional): LinkedIn connection requests sent
  - `li_connections_accepted` (integer or null, optional): LinkedIn connection requests accepted
  - `li_messages` (integer or null, optional): LinkedIn messages sent
  - `meetings_booked` (integer or null, optional)
  - `partial` (boolean or null, optional): True for today: the rollup is refreshed hourly, so today's row is still filling in
  - `platform` (string or null, optional): Only when group\_by=sender: 'email' or 'linkedin'
  - `replies_negative` (integer or null, optional)
  - `replies_neutral` (integer or null, optional)
  - `replies_ooo` (integer or null, optional): Out-of-office replies
  - `replies_positive` (integer or null, optional)
  - `sender_key` (string or null, optional): Only when group\_by=sender
  - `variation` (string or null, optional): Only when group\_by=variation: the A/B arm, or null for leads with none
- `group_by` (string, optional, default `"none"`, one of `"none"`, `"sender"`, `"variation"`): The grouping applied
- `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_WINDOW` | The daily-stats window is unparseable, has from after to, or spans more than 400 days. |
| 400 | `INVALID_GROUP_BY` | group\_by must be none, sender or variation. |
| 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. |
