mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-09 00:03:10 +00:00
544 lines
18 KiB
Plaintext
544 lines
18 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. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
|
|
|
|
| Parameter | In | Type | Description |
|
|
| --- | --- | --- | --- |
|
|
| `period` | query | string | One of `7d`, `30d`, `90d`. 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"
|
|
}
|
|
],
|
|
"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 }
|
|
]
|
|
}
|
|
```
|
|
|
|
## 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 domain, 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,
|
|
"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",
|
|
"samples": 24,
|
|
"inbox": 22,
|
|
"promotions": 1,
|
|
"spam": 1,
|
|
"other": 0,
|
|
"inbox_rate": 91.67,
|
|
"spam_rate": 4.17
|
|
}
|
|
],
|
|
"warmup_placement": [
|
|
{
|
|
"provider": "outlook",
|
|
"domain": "outlook.com",
|
|
"delivered": 132,
|
|
"spam": 6,
|
|
"inbox_rate": 95.45,
|
|
"spam_rate": 4.55
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`spam_placement_rate` and `inbox_placement_rate` are omitted when there are no seed samples in the window. `by_provider` rolls the same seed samples up per recipient provider; `warmup_placement` is the continuous warmup signal per recipient domain, where `delivered` counts verified warmup arrivals and `spam` the subset the recipient's provider filed into junk. `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`.
|
|
|
|
| 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,
|
|
"average_daily": 17.5,
|
|
"reply_rate": 40.0,
|
|
"target_progress": 0,
|
|
"days_active": 12
|
|
},
|
|
"daily_stats": [
|
|
{ "date": "2026-06-01", "emails_sent": 12, "emails_replied": 5, "target_volume": 12 }
|
|
]
|
|
}
|
|
```
|
|
|
|
`email_account_id` is the zero UUID when no `email_id` filter is supplied.
|
|
|
|
## Get campaign analytics
|
|
|
|
`GET /analytics/campaigns/:id`
|
|
|
|
Returns a single campaign's performance summary plus per-sequence-step stats. 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. |
|
|
|
|
### Response
|
|
|
|
```json
|
|
{
|
|
"campaign_id": "b1f2c3d4-0000-0000-0000-000000000001",
|
|
"name": "Q2 outbound",
|
|
"status": "active",
|
|
"date_range": { "from": "0001-01-01T00:00:00Z", "to": "0001-01-01T00: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,
|
|
"clicks": 61,
|
|
"replies": 28,
|
|
"bounces": 3
|
|
}
|
|
],
|
|
"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 }]
|
|
}
|
|
}
|
|
```
|
|
|
|
`engagement` groups the campaign's human opens and clicks by country (ISO code), mail client or browser, and device type (`desktop`, `mobile`, `tablet`), counting distinct contacts per bucket; a click counts as an open. 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.
|
|
|
|
`machine_opens` is the subset of `unique_opens` from automated fetchers (Apple MPP prefetch, UA-less clients, opens inside ten seconds of the send); human opens are `unique_opens` minus `machine_opens`. `machine_clicks` counts steps whose only clicks 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.
|
|
|
|
## 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. Range start (`YYYY-MM-DD`). |
|
|
| `to` | query | string | Required. Range end (`YYYY-MM-DD`). |
|
|
|
|
### 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`). Defaults to today. |
|
|
|
|
### Response
|
|
|
|
The series is returned under a `data` envelope, with the resolved `date` echoed back. Each item's `hour` is `0`-`23`.
|
|
|
|
```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. Range start (`YYYY-MM-DD`). |
|
|
| `to` | query | string | Required. Range end (`YYYY-MM-DD`). |
|
|
|
|
### 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 the caller owns. Accounts whose status fails to build are skipped. 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), active errors, today's usage, warmup status, and warmup-pool health. The account must belong to the caller. 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": 6,
|
|
"evaluated_at": "2026-06-12T06:00:00Z"
|
|
},
|
|
"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.
|
|
|
|
## Get usage overview
|
|
|
|
`GET /analytics/usage`
|
|
|
|
Returns account, campaign, contact, and API usage counters for the caller. Auth: **Scope** `READ_ANALYTICS` · **Org permission** `view_analytics`.
|
|
|
|
| Parameter | In | Type | Description |
|
|
| --- | --- | --- | --- |
|
|
| `period` | query | string | One of `day`, `week`, `month`. 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": 12400 },
|
|
"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.
|