Developer documentation
Pull deliveries, bounces, deferrals, opens and clicks into your own warehouse, dashboards or scripts over a read-only JSON/CSV API — or connect Claude, ChatGPT and other LLM clients directly to BigStats over the Model Context Protocol.
Authentication
Create a token in the dashboard under API & MCP clients. The full value is shown once; only a SHA-256 hash is stored. Send it on every request as Authorization: Bearer <token>. Tokens are read-only and cannot modify data or reach any other workspace. Each token carries explicit scopes — reports for aggregate endpoints and events for raw rows with recipient addresses. The same screen shows last-used timestamps, lists every connected MCP/OAuth assistant, and revokes either instantly.
Base URL
https://bigstats.email/api/public/v1
Endpoints
GET /api/public/v1/overview
Delivery KPIs and a time series for the workspace: injected, delivered, deferred, bounced, opened, clicked, complaints, plus the previous period for comparison.
Query: range=1h|3h|6h|12h|24h|7d|30d|90d (default 7d), cluster=<cluster id>
GET /api/public/v1/domains
Per-receiver performance grouped by recipient domain or ISP group.
Query: range=…, group_by=domain|isp (default domain), cluster=<cluster id>
GET /api/public/v1/bounces
Bounce and deferral diagnostics: category mix, sending-IP pressure and top SMTP response codes.
Query: range=…, cluster=<cluster id>
GET /api/public/v1/clusters
List the MTA clusters defined on the workspace with the /24 sending blocks each one covers. Use an id as the cluster filter on the other endpoints.
Query: none
GET /api/public/v1/pools
Shared IP pools: sending IPs used by more than one sender, senders rotating across several IPs, volume mix and rotation flags.
Query: range=…, cluster=<cluster id>
GET /api/public/v1/pool-detail
Per-member delivery health for one shared IP (split by sender) or one sender (split by IP): delivery, bounce, hard-bounce, deferral and complaint rates, bounce categories and top SMTP codes.
Query: mode=ip|sender (default ip), key=<ip or sender> (required), range=…, cluster=…, limit (1–500, default 100), compare=true|false
GET /api/public/v1/pool-domains
Drill through from one IP or sender to the receiving side — performance grouped by recipient domain or ISP group, so you can see where issues concentrate.
Query: mode=ip|sender, key=<ip or sender> (required), group_by=domain|isp (default domain), range=…, from/to (ISO 8601, override range), cluster=…, limit (1–500, default 100)
GET /api/public/v1/pool-series
Time-bucketed trend for one IP or sender — events, delivered, delivery rate, bounce rate, complaint rate and hard bounces per hour or per day, with an optional previous-period overlay.
Query: mode=ip|sender, key=<ip or sender> (required), range=…, bucket=hour|day, cluster=…, compare=true|false
GET /api/public/v1/events
Row-level events with all configurable fields (opens, clicks, bounce detail, SMTP code, DSN, diagnostic, VMTA, sending IP). JSON or CSV.
Query: event_type, domain, campaign_id, cluster, from (ISO 8601), to (ISO 8601), limit (1–5000, default 500), format=json|csv
Examples
cURL — overview
curl -s "https://bigstats.email/api/public/v1/overview?range=7d" \ -H "Authorization: Bearer $BIGSTATS_TOKEN"
cURL — bounce CSV export
curl -s "https://bigstats.email/api/public/v1/events?event_type=bounced&range=7d&format=csv&limit=5000" \ -H "Authorization: Bearer $BIGSTATS_TOKEN" -o bounces.csv
cURL — shared pool diagnosis
# Which senders share 203.0.113.42, and how is each one doing? curl -s "https://bigstats.email/api/public/v1/pool-detail?mode=ip&key=203.0.113.42&range=30d&compare=true" \ -H "Authorization: Bearer $BIGSTATS_TOKEN" # Where is that IP struggling, by ISP group? curl -s "https://bigstats.email/api/public/v1/pool-domains?mode=ip&key=203.0.113.42&group_by=isp&range=30d" \ -H "Authorization: Bearer $BIGSTATS_TOKEN"
JavaScript / TypeScript
const res = await fetch(
"https://bigstats.email/api/public/v1/domains?range=30d&group_by=isp",
{ headers: { Authorization: `Bearer ${process.env.BIGSTATS_TOKEN}` } },
);
if (!res.ok) throw new Error(`BigStats API ${res.status}`);
const { rows } = await res.json();
for (const r of rows) {
console.log(r.bucket, r.delivered, r.bounced);
}Python
import os, requests
r = requests.get(
"https://bigstats.email/api/public/v1/events",
params={"event_type": "bounced", "limit": 1000},
headers={"Authorization": f"Bearer {os.environ['BIGSTATS_TOKEN']}"},
timeout=30,
)
r.raise_for_status()
for e in r.json()["events"]:
print(e["occurred_at"], e["smtp_code"], e["bounce_category"], e["diagnostic"])Example response — /overview
{
"range": "7d",
"from": "2026-07-28T19:00:00.000Z",
"to": "2026-08-04T19:00:00.000Z",
"totals": {
"injected": 41293811, "delivered": 40510442, "deferred": 612004,
"bounced": 402118, "opened": 9120344, "clicked": 1044219, "complained": 3122
},
"previous": { "delivered": 38911204, "bounced": 448190 },
"series": [{ "bucket": "2026-07-28", "delivered": 5712001, "bounced": 61224 }]
}Event fields
| occurred_at | ISO 8601 timestamp of the event |
| event_type | injected · delivered · deferred · bounced · opened · clicked · unsubscribed · complained · rejected |
| recipient / recipient_domain | Address and receiving domain |
| isp_group | gmail · microsoft · yahoo · apple · other |
| campaign_id / mailing_id / message_id | Identifiers passed through from your MTA |
| sending_ip / vmta / remote_mta | Which IP and binding sent it, and which server answered |
| ip_block | The /24 the sending IP belongs to — the unit clusters are built from |
| smtp_code / dsn / diagnostic | Raw SMTP reply, enhanced status code and diagnostic text |
| bounce_type / bounce_category | hard · soft · block, and the classified reason |
| url / user_agent | Click destination and the client that generated the open or click |
Raw event queries are bounded to a rolling window for performance; aggregate endpoints read from pre-computed rollups and stay fast at billion-per-day volume. Rate limit: 60 requests per minute per token.
KumoMTA webhooks
Point a KumoMTA HTTP log hook at the ingest endpoint below and post records in the new_json log format. BigStats normalizes every record type into a single event model, so a KumoMTA deployment reports side by side with any other supported MTA.
Request
POST /api/public/ingest/kumomta?key=ga_live_…
Content-Type: application/json
# the key may also be sent as the header: x-bigstats-key: ga_live_…
# a single record …
{ "type": "Delivery", "recipient": "jordan@gmail.com", "timestamp": 1785000012 }
# … or a batch: a JSON array, newline-delimited JSON,
# or an object wrapper using "events", "records" or "logs"
{ "records": [
{ "type": "Reception", "recipient": "jordan@gmail.com", "timestamp": 1785000000 },
{ "type": "Delivery", "recipient": "jordan@gmail.com", "timestamp": 1785000012 }
] }Response
HTTP/1.1 200 OK
{ "ok": true, "received": 2 }
# failures
401 { "error": "missing ingest key" } # no ?key= and no x-bigstats-key header
401 { "error": "invalid ingest key" } # unknown or deactivated key
400 { "error": "no events found in payload" }
413 { "error": "too many events in one batch" } # hard cap of 25,000 records per POST
500 { "error": "could not store events" } # safe to retry; duplicates are droppedReception → injected — Message accepted into a KumoMTA queue. Establishes the denominator for delivery and bounce rates.
KumoMTA record — Reception
{
"type": "Reception",
"id": "01HZX9K7Q2W8N4C6",
"sender": "news@brand.example.com",
"recipient": "jordan@gmail.com",
"queue": "gmail.com",
"site": "",
"size": 42311,
"timestamp": 1785000000,
"tenant": "brand-weekly",
"campaign": "weekly-digest-2026-08",
"egress_pool": "pool-transactional",
"egress_source": "203.0.113.42",
"source_address": { "address": "203.0.113.42", "port": 0 },
"headers": {
"Subject": "Your weekly delivery digest",
"Message-ID": "<a1b2c3@brand.example.com>",
"X-Campaign-Id": "weekly-digest-2026-08",
"X-Mailing-Id": "m-99231"
},
"meta": { "list_id": "weekly" }
}Batched transaction — one record, one event per recipient
{
"type": "Delivery",
"id": "01HZXA13S5U7W9Y1",
"sender": "news@brand.example.com",
"recipient": ["a@gmail.com", "b@gmail.com", "c@gmail.com"],
"timestamp": "2026-08-04T12:15:00Z",
"egress_pool": "pool-transactional",
"source_address": { "address": "203.0.113.42" },
"peer_address": { "name": "gmail-smtp-in.l.google.com" },
"response": { "code": 250, "content": "2.0.0 OK - gsmtp" }
}How each field on a KumoMTA record maps onto the BigStats event columns you query over the API, MCP or CSV export. Unknown fields are never dropped — the full record is stored on raw.
| KumoMTA field | BigStats field | Notes |
|---|---|---|
| type | event_type | reception→injected, delivery→delivered, bounce/expiration/adminbounce/oob→bounced, transientfailure/delayed→deferred, rejection→rejected, feedback→complained. Anything else lands as other. |
| timestamp | occurred_at | Unix seconds, milliseconds or an ISO 8601 string. reception_timestamp / created are accepted as fallbacks. Missing timestamps default to receipt time. |
| recipient | recipient / recipient_domain / isp_group | String, object with email/address, or an array for batched transactions — an array produces one BigStats event per recipient. Lowercased; the domain is split off and grouped into gmail · microsoft · yahoo · apple · other. |
| headers['Message-ID'] | message_id | Falls back to id, then queue_id. |
| sender | (dedupe key) | Envelope sender. Combined with type, message_id, recipient and timestamp into the dedupe hash, so replayed records never double-count. |
| source_address.address | sending_ip | Falls back to egress_source. The /24 is derived from it for clustering. |
| egress_pool | vmta | Falls back to egress_source, then queue. |
| peer_address.name / .addr | remote_mta | Falls back to site, then provider_name — the receiving MX that answered. |
| response.code | smtp_code | Validated to 200–599. If absent, the first 3-digit code inside response.content is used. |
| response.enhanced_code | dsn | class.subject.detail joined, e.g. 5.1.1. Falls back to an enhanced code parsed out of response.content. |
| response.content | diagnostic | Full remote reply text. Falls back to reason (used by admin bounces and expirations). |
| bounce_classification | bounce_type / bounce_category | Used as the declared hint; BigStats still classifies from the SMTP code and diagnostic text into hard · soft · block plus a reason category. |
| headers['X-Campaign-Id'] / campaign / tenant | campaign_id | First match wins; tenant is the usual KumoMTA multi-brand key. |
| headers['X-Mailing-Id'] / mailing_id / campaign_id | mailing_id | First match wins. |
| headers['Subject'] | subject | Header lookups are case-insensitive. |
| (whole record) | raw | The complete JSON record is retained on the event row for drill-down and export. |
KumoMTA does not emit open or click records; those streams stay empty unless your tracking layer posts them separately. Records are de-duplicated on event type, message id, recipient, timestamp and sender, so log hook retries and replays are safe. See the KumoMTA setup guide.
Amazon SES via SNS
Send SES configuration-set events (Send, Delivery, DeliveryDelay, Bounce, Complaint, Reject) to an SNS topic and subscribe this HTTPS URL. BigStats verifies Amazon's signature, confirms the subscription automatically and turns each affected recipient into one event. DeliveryDelay counts as a deferral; opens and clicks are ignored. Tag messages with tenant to split reporting; the configuration set or dedicated IP pool becomes the Virtual MTA. SES doesn't expose the shared IP used, so shared sending reports as "SES shared".
SNS HTTPS subscription endpoint
https://bigstats.email/api/public/ingest/ses?key=<INGEST_KEY>
SignalMTA webhooks
Add a SignalMTA webhook endpoint pointing at the URL below with an X-BigStats-Key header (recommended). Passing ?key= in the URL is accepted for compatibility only. Each recipient in data.rcptTo becomes one event.
Request
curl -X POST https://bigstats.email/api/public/ingest/signalmta \ -H "Content-Type: application/json" \ -H "X-BigStats-Key: <INGEST_KEY>" \ -d @envelope.json
SignalMTA delivered → delivered
{
"id": "evt_delivered_0001",
"type": "delivered",
"createdAt": "2026-09-28T15:04:05Z",
"endpoint": "ep_bigstats",
"observability": {
"tenant": "brand-a",
"stream": "newsletter-sept"
},
"data": {
"messageId": "<msg-0001@brand.example.com>",
"rcptTo": [
"test.user@gmail.com"
],
"route": {
"tenant": "brand-a",
"stream": "newsletter-sept",
"pool": "pool-marketing",
"sendingIp": "203.0.113.42"
},
"remoteMta": "gmail-smtp-in.l.google.com",
"smtpCode": 250,
"response": "250 2.0.0 OK"
}
}SignalMTA deferred → deferred
{
"id": "evt_deferred_0001",
"type": "deferred",
"createdAt": "2026-09-28T15:04:05Z",
"endpoint": "ep_bigstats",
"observability": {
"tenant": "brand-a",
"stream": "newsletter-sept"
},
"data": {
"messageId": "<msg-0001@brand.example.com>",
"rcptTo": [
"test.user@gmail.com"
],
"route": {
"tenant": "brand-a",
"stream": "newsletter-sept",
"pool": "pool-marketing",
"sendingIp": "203.0.113.42"
},
"smtpCode": 421,
"enhancedStatus": "4.7.0",
"response": "421 4.7.0 Try again later"
}
}SignalMTA bounced → bounced
{
"id": "evt_bounced_0001",
"type": "bounced",
"createdAt": "2026-09-28T15:04:05Z",
"endpoint": "ep_bigstats",
"observability": {
"tenant": "brand-a",
"stream": "newsletter-sept"
},
"data": {
"messageId": "<msg-0001@brand.example.com>",
"rcptTo": [
"test.user@gmail.com"
],
"route": {
"tenant": "brand-a",
"stream": "newsletter-sept",
"pool": "pool-marketing",
"sendingIp": "203.0.113.42"
},
"smtpCode": 550,
"enhancedStatus": "5.1.1",
"classification": "bad-mailbox",
"response": "550 5.1.1 The email account that you tried to reach does not exist."
}
}SignalMTA complaint → complained
{
"id": "evt_complaint_0001",
"type": "complaint",
"createdAt": "2026-09-28T15:04:05Z",
"endpoint": "ep_bigstats",
"observability": {
"tenant": "brand-a",
"stream": "newsletter-sept"
},
"data": {
"messageId": "<msg-0001@brand.example.com>",
"rcptTo": [
"test.user@gmail.com"
],
"route": {
"tenant": "brand-a",
"stream": "newsletter-sept",
"pool": "pool-marketing",
"sendingIp": "203.0.113.42"
},
"category": "abuse"
}
}SignalMTA suppressed → rejected
{
"id": "evt_suppressed_0001",
"type": "suppressed",
"createdAt": "2026-09-28T15:04:05Z",
"endpoint": "ep_bigstats",
"observability": {
"tenant": "brand-a",
"stream": "newsletter-sept"
},
"data": {
"messageId": "<msg-0001@brand.example.com>",
"rcptTo": [
"test.user@gmail.com"
],
"route": {
"tenant": "brand-a",
"stream": "newsletter-sept",
"pool": "pool-marketing",
"sendingIp": "203.0.113.42"
},
"explanation": "Recipient on suppression list"
}
}| type | event_type | delivered→delivered, deferred→deferred, bounced→bounced, complaint→complained, suppressed/rejected→rejected, unsubscribe(d)→unsubscribed, injected/queued/accepted→injected. Anything else lands as other. Opens and clicks are never produced. |
| data.timestamp / createdAt | occurred_at | ISO 8601 or Unix seconds/ms. Falls back to receipt time. |
| data.rcptTo / data.recipients / data.recipient | recipient / recipient_domain / isp_group | Array or string — one BigStats event per recipient. |
| data.messageId | message_id | |
| data.route.tenant / observability.tenant | campaign_id | First match wins. |
| data.route.stream / observability.stream | mailing_id | First match wins. |
| data.route.pool / vmta / egressPool / source | vmta | First match wins. |
| data.outboundIp / data.sendingIp / data.route.sendingIp | sending_ip | The /24 is derived for clustering. |
| data.remoteMta / data.route.remoteMta | remote_mta | |
| data.smtpCode | smtp_code | Validated to 200–599; falls back to a code parsed from the response. |
| data.enhancedStatus | dsn | e.g. 5.1.1. |
| data.response / diagnosticCode / explanation | diagnostic | First match wins. |
| data.classification / data.category | bounce_type / bounce_category | Declared hint; BigStats still classifies from code and text. |
| id + type + recipient | dedupe_hash | Replay-safe. Without an id, a hash of type, messageId, recipient, timestamp, code and response is used. |
| (whole envelope) | raw | Retained for drill-down and export. |
SignalMTA opens and clicks are not ingested. Replays are de-duplicated on webhook id, type and recipient.
MCP server
BigStats exposes a Model Context Protocol server so an LLM can query your delivery reporting directly and answer questions like “why did Microsoft bounce rate double yesterday?”. The connection uses OAuth: you sign in to BigStats and approve the client, and every tool call then runs as your account — scoped to your workspaces, read-only, with recipient addresses never exposed.
MCP endpoint (Streamable HTTP)
https://bigstats.email/mcp
Client config (Claude Desktop / Cursor style)
{
"mcpServers": {
"bigstats": {
"url": "https://bigstats.email/mcp"
}
}
}Settings → Connectors → Add custom connector, paste https://bigstats.email/mcp, then complete the BigStats sign-in and approve access.
Add BigStats as a remote MCP server using the same URL. Clients that support dynamic registration configure themselves; no client ID or secret to create.
| list_workspaces | List the workspaces the signed-in account can report on. |
| delivery_overview | KPIs and time series for a workspace and range. |
| domain_breakdown | Per-domain or per-ISP performance. |
| bounce_analysis | Bounce categories, IP pressure and SMTP codes. |
| shared_pools | Shared and rotating sending IPs, with sender mix and rotation flags. |
| pool_detail | Per-member delivery health for one IP or sender, with optional previous-period comparison. |
| pool_domains | Where one IP or sender is struggling, by recipient domain or ISP group. |
| search_events | Row-level drill-down, without recipient addresses. |
Errors
| 401 | Missing or invalid bearer token — create a new token in API & MCP clients. |
| 403 | The token lacks the scope this endpoint needs (reports or events). Issue a token with that scope. |
| 400 | Invalid query parameter. The response includes a detail array naming the field. |
| 500 | Query failed. Retry with a smaller range or narrower filters. |