BigStats

Setup guide · KumoMTA

Connect KumoMTA to BigStats with an HTTP log hook.

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

Create an ingest key

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

Add the log hook to init.lua

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

Required event types

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 recordBigStats eventStatusWhy it matters
ReceptioninjectedRequiredDenominator for delivery, bounce and complaint rates.
DeliverydeliveredRequiredDelivery rate, ISP placement and delivery-rate-floor alerts.
BouncebouncedRequiredHard/soft/block classification and bounce-rate alerts.
TransientFailuredeferredRequiredDeferral-rate and throttling alerts before a bounce wave.
ExpirationbouncedRecommendedMessages that aged out of the queue after repeated deferrals.
AdminBouncebouncedRecommendedOperator-initiated bounces, kept separate in the raw record.
OOBbouncedRecommendedOut-of-band bounces reported after acceptance.
FeedbackcomplainedRecommendedFeedback-loop complaints and complaint-rate alerts.
RejectionrejectedOptionalRefused at reception — injection problems, not delivery problems.

Step 04

Example webhook payloads

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 dropped

Field mapping

What BigStats reads off each record

KumoMTA fieldBigStats columnNotes
typeevent_typereception→injected, delivery→delivered, bounce/expiration/adminbounce/oob→bounced, transientfailure/delayed→deferred, rejection→rejected, feedback→complained. Anything else lands as other.
timestampoccurred_atUnix seconds, milliseconds or an ISO 8601 string. reception_timestamp / created are accepted as fallbacks. Missing timestamps default to receipt time.
recipientrecipient / recipient_domain / isp_groupString, 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_idFalls 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.addresssending_ipFalls back to egress_source. The /24 is derived from it for clustering.
egress_poolvmtaFalls back to egress_source, then queue.
peer_address.name / .addrremote_mtaFalls back to site, then provider_name — the receiving MX that answered.
response.codesmtp_codeValidated to 200–599. If absent, the first 3-digit code inside response.content is used.
response.enhanced_codedsnclass.subject.detail joined, e.g. 5.1.1. Falls back to an enhanced code parsed out of response.content.
response.contentdiagnosticFull remote reply text. Falls back to reason (used by admin bounces and expirations).
bounce_classificationbounce_type / bounce_categoryUsed 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 / tenantcampaign_idFirst match wins; tenant is the usual KumoMTA multi-brand key.
headers['X-Mailing-Id'] / mailing_id / campaign_idmailing_idFirst match wins.
headers['Subject']subjectHeader lookups are case-insensitive.
(whole record)rawThe complete JSON record is retained on the event row for drill-down and export.

Step 05

Verify without storing anything

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

Alerts from KumoMTA events

Once records are flowing, alerting runs on them automatically — open Alert settings to set thresholds and recipients.

  • Bounce rate over threshold — workspace-wide and per receiving domain, from Bounce, Expiration, AdminBounce and OOB records.
  • Deferral spikes — TransientFailure volume measured against a rolling 7-day baseline, so provider throttling surfaces early.
  • Complaint rate — Feedback records against delivered volume.
  • Delivery-rate floor — Delivery vs Reception dropping below your configured floor.
  • Escalation, Slack, outbound webhooks and the daily digest all work identically for KumoMTA-sourced alerts.