unMTA

Events

Export email events in real-time via webhooks or archive logs to S3-compatible storage.

Overview

The Events page provides two ways to export event data from your unMTA cluster:

MethodUse Case
WebhooksReal-time HTTP notifications for integrations, analytics, and automated workflows
Log ShippingPeriodic upload to S3 for compliance, archival, and batch processing

Both methods deliver the same event data in the same JSON format—choose the one that fits your use case, or use both together.

Event logs on each MTA are automatically deleted after 24 hours. Enable webhooks and/or log shipping to preserve your event data beyond this retention window.

Event Types

unMTA generates events throughout the email delivery lifecycle:

EventDescription
ReceptionMessage accepted by unMTA for delivery
DeliveryMessage successfully delivered to the recipient's mail server
BouncePermanent delivery failure (5xx SMTP response)
TransientFailureTemporary delivery failure (4xx SMTP response, will retry)
ExpirationMessage exceeded maximum lifetime in queue
AdminBounceMessage bounced via admin API
OOBOut-of-band bounce received after initial acceptance
FeedbackARF feedback report (spam complaint from recipient)
RejectionSMTP rejection during session (e.g., authentication failure)

Event Payload

Each event is a JSON object. The example below shows a Delivery event, which includes the most fields:

{
  "type": "Delivery",
  "id": "d7f8a9b0c1d2e3f4",
  "sender": "d7f8a9b0c1d2e3f4@c-abc123.unmta.net",
  "recipient": "user@example.com",
  "queue": "example.com",
  "site": "example.com",
  "peer_address": {
    "name": "mx.example.com",
    "addr": "192.0.2.1"
  },
  "response": {
    "code": 250,
    "content": "OK"
  },
  "timestamp": 1706000000,
  "created": 1706000000,
  "num_attempts": 1,
  "bounce_classification": "Uncategorized",
  "egress_pool": "default",
  "egress_source": "default-source",
  "delivery_protocol": "ESMTP",
  "meta": {
    "from": "sender@example.com",
    "to": "user@example.com",
    "subject": "Your order has shipped",
    "message_id": "<abc123@example.com>",
    "x_mailer": "MyApp/1.0",
    "hostname": "m-12345678.unmta.net",
    "authn_id": "api_user"
  },
  "headers": {},
  "session_id": "984851cf-952c-4be6-82fd-375345ab3419",
  "nodeid": "a3541223-aa59-4ad2-80ea-8785d23e29e6",
  "tls_cipher": "TLS_AES_256_GCM_SHA384",
  "tls_protocol_version": "TLSv1.3"
}

Key Fields

FieldDescription
typeEvent type (see Event Types table above)
idUnique message identifier
senderEnvelope sender (bounce address)
recipientEnvelope recipient
queueDestination queue
siteMail exchanger site
peer_addressRemote server hostname and IP
responseSMTP response code and message
timestampUnix timestamp when the event occurred
createdUnix timestamp when the message was received
num_attemptsNumber of delivery attempts
bounce_classificationBounce category (e.g., Uncategorized, AuthenticationFailed)
metaMessage headers captured at reception
session_idSMTP session identifier
nodeidMTA node that processed the event

Event-Specific Fields

Different event types include additional fields:

Reception events:

  • reception_protocol — How the message was received (ESMTP, HTTP)
  • size — Message size in bytes

Delivery, Bounce, and TransientFailure events:

  • delivery_protocol — Protocol used for delivery (ESMTP)
  • egress_pool — Outbound IP pool used
  • egress_source — Specific outbound source

Feedback events:

  • feedback_report — ARF report details

Rejection events:

  • Most identification fields (id, sender, recipient, queue) may be empty
  • bounce_classification indicates the rejection reason

Captured Headers

The following headers are captured in the meta field:

  • from, to, cc, reply_to
  • subject, message_id
  • in_reply_to, references
  • list_id, list_unsubscribe, list_unsubscribe_post
  • All custom X-* headers

System Meta Fields

In addition to captured headers, unMTA adds the following system-generated fields to meta:

FieldDescription
hostnameHostname of the MTA node that processed the message (e.g., m-12345678.unmta.net)
authn_idAuthenticated identity used to submit the message

Webhooks

Webhooks deliver events to your HTTP endpoint in real-time. Use webhooks when you need immediate notification of email events—for example, to update a CRM when an email bounces or to trigger workflows when messages are delivered.

How Webhooks Work

  1. Events are collected as they occur
  2. Events are batched (up to 500 per request) for efficiency
  3. Your endpoint receives a JSON array of event objects
  4. unMTA retries on failure with exponential backoff

Multiple Webhook Destinations

Each cluster supports multiple webhook destinations. This lets you route events to different endpoints based on the message — for example, sending transactional events to one system and marketing events to another.

Each destination has:

  • A user-defined name for your reference
  • A unique 6-character ID (e.g. abc123) used to route messages at send time
  • Its own URL and signing secret
  • An optional default flag — if set, this destination receives events for any message that doesn't specify a destination

Routing Events to a Destination

Add the X-UNMTA-Event-Dest header to your message at injection time with the destination's 6-character ID:

X-UNMTA-Event-Dest: abc123

All lifecycle events for that message (Delivery, Bounce, TransientFailure, Expiration) are sent to the specified destination. OOB bounces and ARF feedback reports are also routed back to the correct destination via the encoded return-path.

If no header is present: events go to the destination marked as default.

If no default is configured and no header is set: no webhook is sent for that message.

Configuring Webhook Destinations

  1. Navigate to the Events page from the sidebar
  2. In the Webhooks section, click Add webhook
  3. Enter a name and your HTTPS endpoint URL
  4. Optionally enable Set as default to receive events from messages with no explicit destination
  5. Click Save
  6. Open the destination to copy the ID (used in X-UNMTA-Event-Dest) and secret (used for signature verification)

You can add as many destinations as needed. To temporarily stop sending to a destination without deleting it, use the Disable webhook option in the actions menu.

Each destination's signing secret is auto-generated and cannot be changed. Copy it when you create the destination and store it securely.

URL Requirements

  • Must be a valid URL with http:// or https:// protocol
  • Cannot be localhost, 127.0.0.1, or ::1
  • Cannot use private IP ranges (10.x.x.x, 172.16-31.x.x, 192.168.x.x)
  • Cannot use local domains (.local, .lan, .internal)

Webhook Request Format

POST /your-endpoint HTTP/1.1
Host: your-server.com
Content-Type: application/json
User-Agent: unMTA-Webhooks/1.0
X-Webhook-Id: a1b2c3d4e5f6...
X-Webhook-Timestamp: 1706000000
X-Webhook-Signature: v1=abc123def456...

[
  {"type": "Delivery", "id": "msg1", ...},
  {"type": "Bounce", "id": "msg2", ...}
]

Webhook Authentication

Every webhook request includes a cryptographic signature so you can verify it came from unMTA. Each destination is signed with its own secret.

HeaderDescription
X-Webhook-IdUnique identifier for this batch (SHA256 hash of payload)
X-Webhook-TimestampUnix timestamp when the request was sent
X-Webhook-SignatureHMAC-SHA256 signature in format v1=<hex>

The signature is computed as:

signing_string = webhook_id + "." + timestamp + "." + payload
signature = HMAC-SHA256(secret, signing_string)

Always verify webhook signatures before processing events. This prevents attackers from sending fake events to your endpoint.

Idempotent Consumers

Webhook consumers must be designed to handle duplicate events. Use the X-Webhook-Id header to deduplicate.

If one webhook destination is unreachable or returns errors, the entire event batch is retried — including events destined for healthy endpoints that already received them successfully. This means a working endpoint may receive the same event multiple times until all destinations in the batch succeed or retries are exhausted.

The multi-webhook feature is designed for a small number of distinct integrations (e.g., a monitoring system, a transactional app, a marketing platform). It is not intended for per-end-user fan-out.

If you need to distribute events to many downstream consumers (e.g., a reseller or ESP routing events to dozens of end users), we recommend a single default webhook destination pointing to a central event server you control. That server can then fan out events internally with full control over retry logic, buffering, and per-consumer delivery.

This avoids:

  • Batch fragmentation — a 500-event batch splitting into dozens of individual HTTP requests
  • Retry amplification — one slow or unreachable consumer causing duplicates across all other consumers

Verifying Webhook Signatures

$payload = file_get_contents('php://input');
$webhookId = $_SERVER['HTTP_X_WEBHOOK_ID'];
$timestamp = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'];
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'];

$signingString = $webhookId . '.' . $timestamp . '.' . $payload;
$expectedSignature = 'v1=' . hash_hmac('sha256', $signingString, $secret);

if (!hash_equals($expectedSignature, $signature)) {
http_response_code(401);
exit('Invalid signature');
}

$events = json_decode($payload, true);
foreach ($events as $event) {
// Process each event
}
const crypto = require('crypto');

app.post('/webhook', (req, res) => {
  const payload = JSON.stringify(req.body);
  const webhookId = req.headers['x-webhook-id'];
  const timestamp = req.headers['x-webhook-timestamp'];
  const signature = req.headers['x-webhook-signature'];

  const signingString = `${webhookId}.${timestamp}.${payload}`;
  const expectedSignature = 'v1=' + crypto
    .createHmac('sha256', secret)
    .update(signingString)
    .digest('hex');

  if (!crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(signature)
  )) {
    return res.status(401).send('Invalid signature');
  }

  for (const event of req.body) {
    // Process each event
  }

  res.status(200).send('OK');
});
import hmac
import hashlib
import json
from flask import Flask, request

app = Flask(**name**)

@app.route('/webhook', methods=['POST'])
def webhook():
payload = request.get_data(as_text=True)
webhook_id = request.headers.get('X-Webhook-Id')
timestamp = request.headers.get('X-Webhook-Timestamp')
signature = request.headers.get('X-Webhook-Signature')

    signing_string = f"{webhook_id}.{timestamp}.{payload}"
    expected_signature = 'v1=' + hmac.new(
        secret.encode(),
        signing_string.encode(),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected_signature, signature):
        return 'Invalid signature', 401

    events = json.loads(payload)
    for event in events:
        # Process each event
        pass

    return 'OK', 200
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io"
    "net/http"
)

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    payload, _ := io.ReadAll(r.Body)
    webhookId := r.Header.Get("X-Webhook-Id")
    timestamp := r.Header.Get("X-Webhook-Timestamp")
    signature := r.Header.Get("X-Webhook-Signature")

    signingString := webhookId + "." + timestamp + "." + string(payload)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(signingString))
    expectedSignature := "v1=" + hex.EncodeToString(mac.Sum(nil))

    if !hmac.Equal([]byte(expectedSignature), []byte(signature)) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    var events []map[string]interface{}
    json.Unmarshal(payload, &events)
    for _, event := range events {
        // Process each event
    }

    w.WriteHeader(http.StatusOK)
}

Handling Failures

When your endpoint returns a 4xx or 5xx status code, unMTA will retry the webhook with exponential backoff. Because retries affect the entire batch across all destinations, your endpoint must be idempotent:

  • Use the X-Webhook-Id header to deduplicate requests
  • Store processed webhook IDs and skip duplicates
  • Return 200 OK as soon as you've received the events

Log Shipping

Log shipping periodically uploads event logs to S3-compatible storage. Use log shipping when you need to archive events for compliance, run batch analytics, or integrate with data warehouses.

Supported Storage Services

AWS S3 — Native Amazon S3 with region selection

S3-Compatible Services:

  • MinIO
  • DigitalOcean Spaces
  • Backblaze B2
  • Cloudflare R2
  • Wasabi
  • Any S3-compatible API

Upload Frequency

FrequencyUse Case
5 minutesNear real-time analysis, high-volume monitoring
10 minutes
15 minutes
30 minutesBalance of timeliness and efficiency
1 hourStandard operational logging
3 hours
6 hours
12 hours
24 hoursDaily archival, compliance retention

Logs are uploaded as compressed JSON files (zstd compression).

Configuring Log Shipping

  1. Navigate to the Events page from the sidebar
  2. In the S3 Log Shipping section, click Edit Log Shipping
  3. Enable the Enable S3 log shipping toggle
  4. Select your service type:
    • AWS S3 — Enter your region
    • S3 Compatible — Enter your endpoint URL
  5. Enter your bucket name
  6. Optionally enter a path prefix (e.g., logs/unmta/)
  7. Enter your access key and secret key
  8. Select your upload frequency
  9. Click Test Connection to verify access
  10. Click Save

S3 Bucket Requirements

Your S3 credentials need the following permissions:

PermissionRequiredPurpose
s3:PutObjectYesUpload log files
s3:DeleteObjectNoClean up test files during connection test

Example IAM Policy (AWS):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["s3:PutObject"],
      "Resource": "arn:aws:s3:::your-bucket-name/*"
    }
  ]
}

Log File Format

Log files are uploaded with the following characteristics:

  • Format: JSON, zstd compressed
  • Structure: One event per line (JSONL format)
  • Organization: Files organized by timestamp in your bucket
  • Content: Same event payload structure as webhooks

Best Practices

For Webhooks

  • Always verify signatures — Each destination has its own secret; reject requests with invalid signatures to prevent spoofing
  • Respond quickly — Return 200 OK immediately after receiving; process events asynchronously
  • Handle retries — Use X-Webhook-Id for deduplication; a retry affects all destinations in a batch, so any endpoint may receive duplicates
  • Use HTTPS — Encrypt webhook traffic to protect event data in transit
  • Monitor failures — A single failing destination can cause retries across the entire batch; keep all endpoints healthy
  • Use a central fan-out for many consumers — See Recommended Architecture for Many Destinations

For Log Shipping

  • Choose appropriate frequency — Higher frequencies provide fresher data but create more small files
  • Use path prefixes — Organize logs by environment or purpose (e.g., production/unmta/)
  • Set retention policies — Configure S3 lifecycle rules to manage storage costs
  • Secure credentials — Use IAM roles with minimal required permissions

On this page