Files
warmbly/docs/content/docs/api/reference/analytics.mdx
T

677 lines
32 KiB
Plaintext

---
title: Analytics and audit
description: Read dashboard, deliverability, warmup, campaign, account, and usage analytics, plus your organization's audit trail.
---
The analytics endpoints expose the same rollups that power the dashboard: an org-wide overview, deliverability posture, warmup progress, per-campaign performance (with daily and hourly breakdowns and side-by-side comparison), mailbox health, and account usage. The audit endpoint returns your organization's activity trail ("who did what, when, from where"). All of these are read-only.
Every analytics route shares one auth gate: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`. The audit route uses the same org permission with a dedicated scope. See [permissions](/api/permissions/) for the scope reference and [authentication](/api/authentication/) for how to present credentials.
Dates are parsed as `YYYY-MM-DD` unless noted. Errors follow the standard `{error, message, code, request_id}` envelope documented in [error codes](/api/error-codes/).
## Get dashboard analytics
`GET /analytics/dashboard`
Returns the main dashboard overview for the active organization: aggregate stats, recent activity, top campaigns, account health, and a daily trend series. Sent and engagement totals include email steps only; completed wait and action steps do not inflate them. Account-health buckets are mutually exclusive and use the most severe current connection, sync, or warmup-reputation state. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `period` | query | string | One of `7d`, `30d`, `90d`, measured as UTC calendar days including today. Defaults to `7d`; any other value falls back to `7d`. |
### Response
```json
{
"period": "7d",
"overall_stats": {
"total_emails_sent": 1240,
"total_opens": 612,
"machine_opens": 88,
"total_clicks": 143,
"machine_clicks": 6,
"total_replies": 57,
"total_bounces": 9,
"open_rate": 49.35,
"click_rate": 11.53,
"reply_rate": 4.6,
"bounce_rate": 0.73,
"active_campaigns": 4,
"active_accounts": 12
},
"recent_activity": [
{
"type": "replied",
"campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
"campaign_name": "Q2 outbound",
"contact_email": "lead@example.com",
"contact_id": "c0ffee00-0000-0000-0000-000000000002",
"timestamp": "2026-06-11T14:02:11Z"
},
{
"type": "opened",
"campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
"campaign_name": "Q2 outbound",
"contact_email": "lead@example.com",
"contact_id": "c0ffee00-0000-0000-0000-000000000002",
"timestamp": "2026-06-11T13:40:02Z",
"origin": { "client": "Apple Mail", "client_type": "app", "device_type": "mobile", "os": "iOS", "country_code": "US", "city": "Austin" }
}
],
"top_campaigns": [
{
"campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
"name": "Q2 outbound",
"status": "active",
"emails_sent": 820,
"open_rate": 51.2,
"click_rate": 12.1,
"reply_rate": 5.0
}
],
"account_health": {
"total_accounts": 12,
"healthy_accounts": 10,
"warning_accounts": 1,
"error_accounts": 1
},
"daily_trend": [
{ "date": "2026-06-05", "sent": 160, "opens": 79, "clicks": 18, "replies": 7 }
]
}
```
An `opened` or `clicked` entry in `recent_activity` carries `origin` when the person's first open or click on that step was logged: the same client, device and location object a [contact timeline](/api/reference/contacts/#list-a-contacts-timeline) event carries.
## Get deliverability dashboard
`GET /analytics/deliverability`
Returns the organization's deliverability posture for a time window: bounce, complaint, open, click, and reply counts and rates, suppression and dead-letter pressure, reply-intent breakdown, seed inbox-placement (overall and per recipient provider), warmup-derived placement per recipient mail host, an overall health band and a `0-100` composite score (both from the documented thresholds), a daily timeseries, and per-mailbox and per-campaign breakdowns. Requires an organization context. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `from` | query | string | Window start as an RFC 3339 timestamp. Defaults to 7 days ago (UTC). |
| `to` | query | string | Window end as an RFC 3339 timestamp. Defaults to now (UTC). |
### Response
```json
{
"from": "2026-06-05T00:00:00Z",
"to": "2026-06-12T00:00:00Z",
"events_total": 1380,
"bounce_count": 9,
"complaint_count": 1,
"unsubscribe_count": 4,
"reply_count": 57,
"open_count": 612,
"click_count": 143,
"suppressed_recipients": 21,
"dlq_pending": 0,
"intent_positive": 18,
"intent_negative": 6,
"intent_out_of_office": 11,
"intent_question": 9,
"intent_neutral": 13,
"intent_automated": 22,
"emails_sent": 1240,
"bounce_rate": 0.73,
"complaint_rate": 0.08,
"open_rate": 49.35,
"click_rate": 11.53,
"reply_rate": 4.6,
"spam_placement_rate": 6.5,
"inbox_placement_rate": 93.5,
"placement_samples": 40,
"band": "healthy",
"score": 91,
"timeseries": [
{
"date": "2026-06-05",
"sent": 160,
"bounces": 1,
"complaints": 0,
"opens": 79,
"clicks": 18,
"replies": 7,
"unsubscribes": 1
}
],
"by_mailbox": [
{
"email_account_id": "a0a1a2a3-0000-0000-0000-000000000003",
"email": "sales@yourdomain.com",
"sent": 420,
"bounces": 2,
"complaints": 0,
"bounce_rate": 0.48,
"complaint_rate": 0.0,
"band": "healthy"
}
],
"by_campaign": [
{
"campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
"name": "Q2 outbound",
"sent": 820,
"bounces": 5,
"complaints": 1,
"bounce_rate": 0.61,
"complaint_rate": 0.12,
"band": "watch"
}
],
"by_provider": [
{
"provider": "gmail",
"label": "Gmail",
"samples": 24,
"inbox": 21,
"promotions": 1,
"spam": 1,
"other": 0,
"missing": 1,
"inbox_rate": 87.5,
"spam_rate": 4.17
}
],
"warmup_placement": [
{
"provider": "microsoft365",
"label": "Microsoft 365",
"delivered": 132,
"spam": 6,
"inbox_rate": 95.45,
"spam_rate": 4.55
}
]
}
```
The `intent_*` counters and `reply_count` are two different things and do not add up to each other. `reply_count` counts `reply` deliverability events, which are the ones a provider or your own integration reports to [record an event](/api/reference/deliverability-ops/). The `intent_*` counters are the replies Warmbly classified as they arrived in a connected mailbox, one per reply, including the machine ones: a vacation notice lands in `intent_out_of_office` and an autoresponder, ticket acknowledgement or bounce notice in `intent_automated`. A workspace that reports no reply events sees `reply_count` at zero with intents counted normally.
`spam_placement_rate` and `inbox_placement_rate` are omitted when there are no seed samples in the window. Seed samples are the copies of the workspace's [placement tests](/api/reference/placement/) that got a verdict in the window: `inbox`, `promotions`, `other` (another Gmail tab), `spam`, or `missing` (never arrived); copies that failed to send or were cancelled are not samples. `by_provider` rolls the same samples up per seed's provider family, where `provider` is the family id (`gmail`, `google_workspace`, `microsoft365`, `outlook`, `yahoo` and so on) and `label` its display name, and the rates are percentages of `samples`; `warmup_placement` is the continuous warmup signal per recipient mail host, where `provider` is the host id (`google_workspace`, `microsoft365`, `zoho`, `hostinger`, `other` and so on, or the provider group `google`, `microsoft`, `yahoo` or `other` for an arrival whose host was not detected), `label` its display name, `delivered` counts verified warmup arrivals and `spam` the subset the recipient's provider filed into junk. Warmup recipients are mostly other workspaces' mailboxes, so no recipient address or domain is reported. `score` starts at `100` and subtracts saturating penalties for bounce rate (up to `40` points, maxed at `10%`), complaint rate (up to `30` points, maxed at `0.30%`), and spam placement (up to `40` points, maxed at `40%`).
## Get warmup analytics
`GET /analytics/warmup`
Returns warmup send and reply statistics over a date range, with a summary and per-day series. Optionally scoped to a single mailbox. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
Results are scoped to the selected workspace, so every member with access sees the same mailbox history. Workspace-wide results combine all mailboxes into one row per date. `average_daily` uses active days (dates with a warmup statistics row) as its denominator. `target_progress` is total sends divided by total planned target volume across those active days and may exceed `100` when sends beat the plan.
`total_received` and each day's `emails_received` count verified warmup mail that arrived from pool partners, bucketed on the UTC day it was verified. A day with arrivals but no warmup plan is listed with zero sends and does not count towards `days_active`.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `from` | query | string | Required. Range start (`YYYY-MM-DD`). |
| `to` | query | string | Required. Range end (`YYYY-MM-DD`). |
| `email_id` | query | string (uuid) | Optional. Limit to one email account. Invalid UUIDs are ignored. |
### Response
```json
{
"email_account_id": "a0a1a2a3-0000-0000-0000-000000000003",
"email": "",
"date_range": {
"from": "2026-06-01T00:00:00Z",
"to": "2026-06-12T00:00:00Z"
},
"summary": {
"total_sent": 210,
"total_replied": 84,
"total_received": 196,
"average_daily": 17.5,
"reply_rate": 40.0,
"target_progress": 87.5,
"days_active": 12
},
"daily_stats": [
{ "date": "2026-06-01", "emails_sent": 12, "emails_replied": 5, "emails_received": 11, "target_volume": 12 }
]
}
```
`email_account_id` is the zero UUID when no `email_id` filter is supplied.
## Get warmup placement
`GET /analytics/warmup/placement`
Returns where warmup mail landed in partners' mailboxes over a date range: the primary inbox, a Gmail category tab (Promotions, Updates, Social or Forums), or spam. Counts are per UTC day, per recipient provider, and, on the workspace report, per mailbox. Requires an organization context. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
Every figure is measured, not estimated: a delivery is counted when the recipient's own sync finds the warmup email and records the folder it arrived in. `rescued` counts spam placements the recipient's mailbox was told to move back to the inbox; the move itself is not confirmed back, so it is the rescues requested, not a verified count. `unconfirmed` counts completed sends more than 24 hours old that no recipient has reported seeing, bucketed on the day they were sent; a provider filter cannot split it, so it is only reported unfiltered.
`rate` is the headline deliverability figure the dashboard shows next to each mailbox: `inbox_rate` is inbox plus tabs over what was delivered in the trailing 7 UTC days, and stays `null` until `min_sample` (`20`) deliveries are in. `scope` is always `major`: the rate is taken over Google, Microsoft and Yahoo recipients only, the providers that filter on sender reputation. A mailbox whose warmup mail reached only other hosts has no rate (`delivered` `0`, `band` `none`). Other hosts are always in `summary`, `daily` and `providers`, and `other_delivered` and `other_inbox_rate` give their share of the same window (`0` and `null` with none). `band` is `good` at `90` or above, `fair` from `80`, `poor` below `80`, `collecting` below the sample floor and `none` with no deliveries. Each day's `rolling_inbox_rate` is the trailing 7-day figure at the same three providers ending on that day; the day's counts cover every host, so filter by provider to read one. Rates are percentages with two decimals.
Recipient providers are grouped as `google` (Gmail and Google Workspace), `microsoft` (Outlook.com and Microsoft 365), `yahoo` (Yahoo and AOL) and `other`; each group lists its `hosts`, where an empty `host` means the recipient's host was not detected yet.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `from` | query | string | Optional. Range start (`YYYY-MM-DD`). Defaults to 29 days before `to`. |
| `to` | query | string | Optional. Range end (`YYYY-MM-DD`). Defaults to today (UTC). |
| `email_id` | query | string (uuid) | Optional. Limit to one email account in the workspace. Omit for the workspace report, which adds `mailboxes`. |
A range longer than 366 days, `from` after `to`, a malformed date or an invalid `email_id` returns `400`; an `email_id` outside the workspace returns `404`.
### Response
```json
{
"email_account_id": "a0a1a2a3-0000-0000-0000-000000000003",
"date_range": { "from": "2026-06-01T00:00:00Z", "to": "2026-06-30T00:00:00Z" },
"summary": {
"sent": 820, "delivered": 791, "inbox": 702, "tabs": 41, "spam": 48,
"rescued": 44, "unconfirmed": 12, "inbox_rate": 93.93, "spam_rate": 6.07
},
"rate": {
"window_days": 7, "min_sample": 20, "scope": "major", "delivered": 196, "inbox": 180, "tabs": 9, "spam": 7,
"inbox_rate": 96.43, "band": "good"
},
"daily": [
{
"date": "2026-06-30",
"sent": 30, "delivered": 29, "inbox": 27, "tabs": 1, "spam": 1, "rescued": 1, "unconfirmed": 0,
"inbox_rate": 96.55, "spam_rate": 3.45, "rolling_inbox_rate": 96.43,
"groups": [
{ "group": "google", "inbox": 15, "tabs": 1, "spam": 0, "rescued": 0 },
{ "group": "microsoft", "inbox": 12, "tabs": 0, "spam": 1, "rescued": 1 }
]
}
],
"providers": [
{
"group": "google",
"sent": 0, "delivered": 402, "inbox": 351, "tabs": 41, "spam": 10, "rescued": 9, "unconfirmed": 0,
"inbox_rate": 97.51, "spam_rate": 2.49,
"hosts": [
{ "host": "google_workspace", "sent": 0, "delivered": 310, "inbox": 271, "tabs": 33, "spam": 6, "rescued": 6, "unconfirmed": 0, "inbox_rate": 98.06, "spam_rate": 1.94 },
{ "host": "gmail", "sent": 0, "delivered": 92, "inbox": 80, "tabs": 8, "spam": 4, "rescued": 3, "unconfirmed": 0, "inbox_rate": 95.65, "spam_rate": 4.35 }
]
}
]
}
```
The workspace report (no `email_id`) omits `email_account_id` and adds `mailboxes`, one row per mailbox with activity in the range, ordered worst `rate.inbox_rate` first, then mailboxes still collecting. Each row carries the window counts, its own `rate`, and `daily_inbox_rate`, one entry per day of the range (`null` on a day with no deliveries).
Placement history is kept for the life of the mailbox, like warmup volume history, and moves with a workspace export. Deliveries recorded before this report existed, and any arrival the live count missed, are counted by a background pass from the receipts still on file within minutes; those read as inbox or spam only, with category tabs and rescues as `0`.
## Get campaign analytics
`GET /analytics/campaigns/:id`
Returns a single campaign's performance summary plus per-sequence-step stats, for the campaign's whole history or for the emails sent in a date range. `total_contacts` counts enrolled leads, and `emails_pending` is the remaining planned email-step sends across those leads. The campaign must belong to the selected workspace. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | string (uuid) | Campaign id. |
| `from` | query | string | Optional. First day of the period (`YYYY-MM-DD`, UTC). Requires `to`. |
| `to` | query | string | Optional. Last day of the period (`YYYY-MM-DD`, UTC), included. Requires `from`. |
Leave out both `from` and `to` for the campaign's whole history. Supplying only one of them, a date in another format, or a `from` after `to` returns `400` with code `bad_request`.
A period is a send cohort: it selects the emails sent on its days, from the start of `from` to the end of `to` in UTC, and counts every open, click, reply and bounce those emails earned, whenever it arrived. An email sent on September 5 that is replied to on September 10 counts in a September 1 to 7 period; a reply that arrives on September 3 to an email sent in August does not. That keeps each rate's numerator and denominator the same emails. The period scopes `emails_sent`, the open, click, reply and bounce counts and rates, every row in `steps` and every bucket in `engagement` (an open or click counts when the email it answers was sent in the period). `total_contacts` and `emails_pending` describe the campaign as it stands now and are the same whatever the period.
`date_range` is the period the figures cover, as midnight UTC on its first and last day: the `from` and `to` you sent, or for the whole history the day of the campaign's first send (its creation day before anything is sent) through today.
### Response
```json
{
"campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
"name": "Q2 outbound",
"status": "active",
"date_range": { "from": "2026-05-04T00:00:00Z", "to": "2026-06-12T00:00:00Z" },
"summary": {
"total_contacts": 500,
"emails_sent": 820,
"emails_pending": 60,
"unique_opens": 410,
"machine_opens": 52,
"unique_clicks": 99,
"machine_clicks": 4,
"replies": 41,
"bounces": 5,
"unsubscribes": 3,
"open_rate": 50.0,
"click_rate": 12.07,
"reply_rate": 5.0,
"bounce_rate": 0.61
},
"steps": [
{
"step_id": "5e9e0001-0000-0000-0000-000000000004",
"name": "Intro",
"position": 1,
"emails_sent": 500,
"opens": 260,
"machine_opens": 31,
"clicks": 61,
"machine_clicks": 2,
"replies": 28,
"bounces": 3,
"open_rate": 52.0,
"click_rate": 12.2,
"reply_rate": 5.6,
"bounce_rate": 0.6
}
],
"engagement": {
"countries": [{ "key": "US", "opens": 120, "clicks": 31 }, { "key": "DE", "opens": 44, "clicks": 9 }],
"clients": [{ "key": "Gmail", "opens": 98, "clicks": 20 }, { "key": "Outlook", "opens": 51, "clicks": 12 }],
"devices": [{ "key": "desktop", "opens": 140, "clicks": 37 }, { "key": "mobile", "opens": 24, "clicks": 3 }],
"surfaces": [{ "key": "hidden", "opens": 98, "clicks": 0 }, { "key": "desktop_app", "opens": 51, "clicks": 12 }, { "key": "mobile", "opens": 0, "clicks": 3 }]
}
}
```
`engagement` groups the campaign's human opens and clicks by country (ISO code), mail client, device type (`desktop`, `mobile`, `tablet`) and surface, counting distinct contacts per bucket. A `clients` key is the mail client (`Gmail`, `Apple Mail`, `Outlook`), `Webmail in <browser>` for an open read in a browser that named no client, or for a click the browser the link opened in. `surfaces` joins device and app or webmail: `mobile_app`, `desktop_app`, `tablet_app`, `webmail`, the bare device type when neither is known (a click, unless a mail app made the request itself), and `hidden` when a mailbox provider's image proxy fetched the email and the device cannot be known. Each list holds the busiest eight; an empty `key` is unknown. Country needs the GeoLite2 database on the consumer, otherwise every country row is unknown.
`unique_opens`, `opens`, `total_opens` and every open rate count a person's opens only. `machine_opens` is the separate count of steps whose only open came from an automated fetcher (UA-less clients, known scanner networks, and opens inside the instance's automated-open window, which starts when the step is dispatched to a worker); it is not part of the open counts. Apple Mail Privacy Protection's relay (a user agent of just `Mozilla/5.0`) and the stripped WebKit signature Mail on a Mac and the new Outlook for Windows share are counted as a prefetch only inside that window; by themselves they do not prove automation. A person's later open on a machine-opened step moves it from `machine_opens` into the open count. `machine_clicks` counts the contacts whose only clicks on a step were automated (a security gateway walking the links); those are not part of `unique_clicks` or `total_clicks`, which only ever count a person's click.
`steps` lists the campaign's email steps in sequence order, the order the steps list returns them. `position` is the step's 1-based number among those email steps, the `N` the canvas shows as `Email N`. It is not the step's own `position` field: action steps are not counted and a deleted step leaves no gap. Match a row to its step by `step_id`.
Each entry in `steps` carries that step's own rates: `open_rate`, `click_rate`, `reply_rate` and `bounce_rate` are percentages of that step's `emails_sent`, not of the campaign's, so a follow-up that reached a tenth of the contacts still compares against the first touch. They are `0` while the step has sent nothing. `machine_opens` and `machine_clicks` follow the same rules as their summary counterparts, scoped to the step.
## Get campaign daily stats
`GET /analytics/campaigns/:id/daily`
Returns per-day send, open, click, and reply counts for one campaign over a date range. The campaign must belong to the caller. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | string (uuid) | Campaign id. |
| `from` | query | string | Required. First day (`YYYY-MM-DD`, UTC). |
| `to` | query | string | Required. Last day (`YYYY-MM-DD`, UTC), included. |
Each row is one UTC day with at least one send, and its opens, clicks and replies are those earned by that day's sends, whenever they arrived. The rows for a period add up to the [campaign analytics](#get-campaign-analytics) for the same `from` and `to`.
### Response
The series is returned under a `data` envelope.
```json
{
"data": [
{ "date": "2026-06-05", "sent": 120, "opens": 61, "clicks": 14, "replies": 6 },
{ "date": "2026-06-06", "sent": 110, "opens": 58, "clicks": 12, "replies": 5 }
]
}
```
## Get campaign hourly stats
`GET /analytics/campaigns/:id/hourly`
Returns per-hour stats for one campaign on a single day. The campaign must belong to the caller. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | string (uuid) | Campaign id. |
| `date` | query | string | Day to report (`YYYY-MM-DD`, UTC). Defaults to today. |
### Response
The series is returned under a `data` envelope, with the resolved `date` echoed back. Each item's `hour` is the UTC hour, `0`-`23`, so a day's rows add up to that day's row in the [daily stats](#get-campaign-daily-stats).
```json
{
"data": [
{ "hour": 9, "sent": 22, "opens": 11, "clicks": 3, "replies": 1 },
{ "hour": 10, "sent": 30, "opens": 16, "clicks": 4, "replies": 2 }
],
"date": "2026-06-11"
}
```
## Compare campaigns
`GET /analytics/campaigns/compare`
Returns side-by-side performance for up to 10 campaigns over a date range. Every requested campaign must belong to the caller. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `ids` | query | string | Required. Comma-separated campaign UUIDs. Invalid entries are dropped; the list is capped at 10. At least one valid id is required. |
| `from` | query | string | Required. First day (`YYYY-MM-DD`, UTC). |
| `to` | query | string | Required. Last day (`YYYY-MM-DD`, UTC), included. |
Each campaign's figures are the emails it sent from the start of `from` to the end of `to`, with every open, click, reply and bounce they earned, the same send cohort as [campaign analytics](#get-campaign-analytics) for that period.
### Response
```json
{
"campaigns": [
{
"campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
"name": "Q2 outbound",
"status": "active",
"emails_sent": 820,
"open_rate": 50.0,
"click_rate": 12.07,
"reply_rate": 5.0,
"bounce_rate": 0.61
},
{
"campaign_id": "b1f2c3d4-0000-0000-0000-000000000005",
"name": "Reactivation",
"status": "paused",
"emails_sent": 410,
"open_rate": 44.1,
"click_rate": 9.8,
"reply_rate": 3.4,
"bounce_rate": 1.2
}
],
"period": {
"from": "2026-06-01T00:00:00Z",
"to": "2026-06-12T00:00:00Z"
}
}
```
## List account statuses
`GET /analytics/accounts`
Returns the health and usage status of every email account in the selected workspace. The request fails if any account's status cannot be calculated, so clients never receive a silently incomplete list. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
### Response
The list is returned under a `data` envelope (no cursor; all of the caller's accounts are included). Each item has the same shape as [get account status](#get-account-status).
```json
{
"data": [
{
"id": "a0a1a2a3-0000-0000-0000-000000000003",
"email": "sales@yourdomain.com",
"provider": "google",
"status": "active",
"last_synced_at": "2026-06-12T08:00:00Z",
"health": { "status": "healthy", "score": 100, "issues": [] },
"errors": [],
"daily_usage": {
"date": "2026-06-12",
"campaign_sent": 18,
"campaign_limit": 50,
"warmup_sent": 22,
"warmup_limit": 40
},
"in_campaign": true
}
]
}
```
## Get account status
`GET /analytics/accounts/:id`
Returns the detailed status for one email account: a combined health score (folding in warmup-pool reputation and measured warmup placement), active errors, today's UTC usage, warmup status, warmup-pool health, and the trailing warmup inbox rate. The account must belong to the selected workspace. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `id` | path | string (uuid) | Email account id. |
### Response
```json
{
"id": "a0a1a2a3-0000-0000-0000-000000000003",
"email": "sales@yourdomain.com",
"provider": "google",
"status": "active",
"last_synced_at": "2026-06-12T08:00:00Z",
"health": {
"status": "warning",
"score": 90,
"issues": ["Warmup reputation needs watching"]
},
"errors": [
{
"id": "e1e1e1e1-0000-0000-0000-000000000006",
"error_code": "IMAP_AUTH",
"severity": "WARNING",
"title": "Mailbox reconnect recommended",
"message": "Token nearing expiry",
"created_at": "2026-06-11T22:14:00Z"
}
],
"daily_usage": {
"date": "2026-06-12",
"campaign_sent": 18,
"campaign_limit": 50,
"warmup_sent": 22,
"warmup_limit": 40
},
"warmup_status": {
"enabled": true,
"paused": false,
"started_at": "2026-05-20T00:00:00Z",
"current_volume": 22,
"target_volume": 33,
"max_volume": 40,
"reply_rate": 35,
"days_active": 23
},
"warmup_health": {
"state": "watch",
"score": 78,
"spam_score": 0,
"partner_mailboxes_7d": 14,
"partner_domains_7d": 11,
"partner_organizations_7d": 9,
"received_7d": 12,
"senders_7d": 8,
"evaluated_at": "2026-06-12T06:00:00Z"
},
"warmup_placement": {
"window_days": 7,
"min_sample": 20,
"scope": "major",
"delivered": 142,
"inbox": 126,
"tabs": 5,
"spam": 11,
"inbox_rate": 92.25,
"band": "good"
},
"in_campaign": true
}
```
`warmup_status` is present only when warmup has ever been enabled; `warmup_health` is present only when the mailbox is in a warmup pool. `in_campaign` reports whether the mailbox currently backs a live campaign.
`warmup_placement` is the mailbox's measured warmup inbox rate over the trailing 7 UTC days, the same figure [Get warmup placement](#get-warmup-placement) reports as `rate`, and is absent when nothing was delivered in that window. Once `inbox_rate` is present, `health.score` is capped at it (rounded down), so a mailbox landing any mail in spam no longer reads `100`, and a rate below `90` also sets `health.status` to at least `warning` with an issue naming the rate.
`daily_usage.warmup_sent` and `campaign_sent` both count sends the mailbox completed on that date, which is what the caps are enforced against, so neither can read above the target in force.
The three `partner_*_7d` counters are the distinct partners reached by confirmed warmup sends over the last seven days: mailboxes, their domains, and the workspaces behind them. Failed and still-pending attempts do not count. `partner_organizations_7d` of `1` over a full week means the mailbox is only warming inside one workspace, which is expected on a self-hosted instance that is not linked to Warmbly Cloud and worth investigating anywhere else.
`received_7d` and `senders_7d` are the other direction over the same window: verified warmup arrivals and the distinct partners they came from. Only mail that the mailbox's own sync verifies counts. A mailbox with many partners on the sending side and few on the receiving side is being written to less than it writes; the pool favours it on every draw until the two even out.
## Get usage overview
`GET /analytics/usage`
Returns account, campaign, and contact usage counters for the selected workspace. `campaigns.emails_sent` counts sent email steps in the requested period; wait and action steps do not count. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
The `api` object is a compatibility placeholder. For real API-key request totals, error rates, latency, and endpoint breakdowns use `GET /api-keys/usage/summary` and the API-key analytics endpoints.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `period` | query | string | `day` counts from 00:00 UTC today, `week` is a rolling seven-day window, and `month` is a rolling one-month window. Defaults to `day`; any other value falls back to `day`. |
### Response
```json
{
"user_id": "11111111-0000-0000-0000-000000000007",
"period": "day",
"email_accounts": { "total": 12, "active": 11, "in_warmup": 8, "with_errors": 1 },
"campaigns": { "total": 9, "active": 4, "paused": 2, "draft": 3, "emails_sent": 124 },
"contacts": { "total": 8200, "subscribed": 8050, "added_today": 120 },
"api": { "total_calls": 0, "daily_limit": 50000, "top_endpoints": [] }
}
```
## List audit logs
`GET /audit-logs`
Returns the organization-wide activity trail for the caller's current organization ("who did what, when, from where"). The organization is always taken from the session and never from a client parameter, so one organization can never read another's trail. Auth: **Scope** `READ_AUDIT_LOGS` · **Org permission** `view_analytics`.
| Parameter | In | Type | Description |
| --- | --- | --- | --- |
| `limit` | query | int | Page size. Defaults to `50`; must be between `10` and `200` or a `400` is returned. |
| `cursor` | query | string (uuid) | Opaque cursor from `pagination.next_cursor`. Invalid cursors return `400`. |
| `actor_id` | query | string (uuid) | Filter to a single acting member. |
| `entity_id` | query | string (uuid) | Filter to a single entity. |
| `entity_type` | query | string | Filter by entity type (for example `campaign`, `contact`, `email_account`, `api_key`, `webhook`). |
| `action` | query | string | Filter by action (for example `create`, `update`, `delete`, `send`, `revoke`). |
| `date` | query | string | Single-day filter (`YYYY-MM-DD`), expanded to that whole UTC day. |
| `start_date` | query | string | Range start. RFC 3339 or `YYYY-MM-DD`. Overrides `date`. |
| `end_date` | query | string | Range end. RFC 3339 or `YYYY-MM-DD`. Overrides `date`. |
### Response
A `data` plus `pagination` envelope with an opaque cursor. `actor` is null when the acting user has since been deleted; `entity_id`, `changes`, and `metadata` are omitted when empty.
```json
{
"data": [
{
"id": "9c9c9c9c-0000-0000-0000-000000000008",
"org_id": "0a0a0a0a-0000-0000-0000-000000000009",
"user_id": "11111111-0000-0000-0000-000000000007",
"actor": {
"id": "11111111-0000-0000-0000-000000000007",
"first_name": "Ada",
"last_name": "Lovelace",
"email": "ada@yourdomain.com"
},
"action_date": "2026-06-12T08:14:00Z",
"action": "update",
"entity_type": "campaign",
"entity_id": "b1f2c3d4-0000-0000-0000-000000000001",
"ip_address": "203.0.113.10",
"user_agent": "Mozilla/5.0",
"changes": { "status": "paused" },
"timestamp": "2026-06-12T08:14:00Z"
}
],
"pagination": {
"next_cursor": "c1_b3BhcXVlLWN1cnNvcg",
"has_more": true
}
}
```
Secret values (API key material, webhook secrets, passwords) are never recorded in `changes` or `metadata`; the trail records only that a field changed.