Setup guide · KumoMTA
Everything needed to stream KumoMTA log records into BigStats: the log hook configuration, the record types you should send, example payloads for each one, and the alerts they drive.
Looking for the overview instead? KumoMTA reporting.
Step 01
In BigStats, open Settings → Ingestion and create a key for this KumoMTA node or cluster. Keys are workspace-scoped; use separate keys per node if you want to trace where records came from. Send the key as ?key= or the X-BigStats-Key header.
Endpoint
POST https://bigstats.email/api/public/ingest/kumomta?key=<INGEST_KEY>
Step 02
Place this in your policy before the init block completes, then reload KumoMTA. Include any headers you want to report on — Subject, campaign and mailing identifiers are mapped automatically.
init.lua
-- init.lua — post every log record to BigStats
kumo.on('init', function()
kumo.configure_log_hook {
name = 'bigstats',
headers = { 'Subject', 'Message-ID', 'X-Campaign-Id', 'X-Mailing-Id' },
-- omit meta/deferred_spool tuning here; defaults are fine to start
}
end)
kumo.on('make.bigstats', function(domain, tenant, campaign)
local sender = {}
function sender:send(message)
local response = kumo.http.build_client({})
:post('https://bigstats.email/api/public/ingest/kumomta')
:header('X-BigStats-Key', '<INGEST_KEY>')
:header('Content-Type', 'application/json')
:body(message:get_data())
:send()
if not response:status_is_success() then
kumo.reject(500, 'bigstats: ' .. response:text())
end
return response:text()
end
return sender
end)Step 03
The four required records are the minimum for correct rates and alerting. KumoMTA does not emit open, click or unsubscribe events, so those streams stay empty unless another system posts them.
| KumoMTA record | BigStats event | Status | Why it matters |
|---|---|---|---|
| Reception | injected | Required | Denominator for delivery, bounce and complaint rates. |
| Delivery | delivered | Required | Delivery rate, ISP placement and delivery-rate-floor alerts. |
| Bounce | bounced | Required | Hard/soft/block classification and bounce-rate alerts. |
| TransientFailure | deferred | Required | Deferral-rate and throttling alerts before a bounce wave. |
| Expiration | bounced | Recommended | Messages that aged out of the queue after repeated deferrals. |
| AdminBounce | bounced | Recommended | Operator-initiated bounces, kept separate in the raw record. |
| OOB | bounced | Recommended | Out-of-band bounces reported after acceptance. |
| Feedback | complained | Recommended | Feedback-loop complaints and complaint-rate alerts. |
| Rejection | rejected | Optional | Refused at reception — injection problems, not delivery problems. |
Step 04
Message accepted into a KumoMTA queue. Establishes the denominator for delivery and bounce rates.
Reception → injected
{
"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 recipients
{
"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" }
}Request formats
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 }
] }Responses
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 droppedField mapping
| KumoMTA field | BigStats column | 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. |
Step 05
Run a dry run first: it authenticates your key and returns the normalized event exactly as BigStats would store it. Then remove dry_run=1 and watch the live feed on the Settings page confirm records arriving.
curl
curl -s -X POST \
'https://bigstats.email/api/public/ingest/kumomta?key=<INGEST_KEY>&dry_run=1' \
-H 'Content-Type: application/json' \
-d '{"type":"Bounce","recipient":"test.user@gmail.com","timestamp":1785000000,
"source_address":{"address":"203.0.113.42"},
"response":{"code":550,"enhanced_code":{"class":5,"subject":1,"detail":1},
"content":"5.1.1 user unknown"}}'
# dry_run authenticates and normalizes the payload but stores nothing:
# { "ok": true, "dry_run": true, "received": 1, "events": [ … ] }Step 06
Once records are flowing, alerting runs on them automatically — open Alert settings to set thresholds and recipients.