daimon.email
Webhooks

Webhook Setup Guide

Real-time email notifications via webhooks

Webhook Setup Guide

Webhooks provide real-time notifications when events occur in your daimon.email account, eliminating the need for constant polling and reducing latency for your agents.

Why Use Webhooks?

Real-Time Delivery

Receive notifications within milliseconds of email arrival. No polling delays.

Reduced Latency

Agents respond instantly to emails instead of waiting for the next poll cycle.

Lower Costs

Fewer API calls = lower bandwidth and compute costs compared to polling.

Event-Driven Architecture

Build reactive systems that respond to email events as they happen.

Webhook Events

daimon.email webhooks deliver these event types:

EventDescriptionAvailability
message.receivedNew email received in an inboxAll tiers
message.bouncedOutbound email bounced (permanent failure)Paid tiers
message.complaintRecipient marked email as spamPaid tiers
account.upgradedAccount upgraded to paid tierAll tiers
webhook.unhealthyWebhook endpoint failing health checksAll tiers

Note

See Webhook Event Reference for complete payload examples.

Creating a Webhook

Step 1: Set Up Endpoint

Your webhook endpoint must:

  • Accept POST requests
  • Use HTTPS (required for security)
  • Return 200 OK within 5 seconds
  • Verify HMAC signature (see Verifying HMAC)
import express from 'express';
import crypto from 'crypto';

const app = express();

app.post('/webhook/daimon', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-daimon-signature'] as string;
  const secret = process.env.WEBHOOK_SECRET!;

  // Verify HMAC signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(req.body)
    .digest('hex');

  if (signature !== expectedSignature) {
    console.error('Invalid signature');
    return res.status(401).send('Unauthorized');
  }

  // Parse payload
  const payload = JSON.parse(req.body.toString());

  // Handle event
  if (payload.event === 'message.received') {
    console.log(`New message: ${payload.message.subject}`);
    // Process the message
  }

  // Acknowledge receipt
  res.status(200).send('OK');
});

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});
from flask import Flask, request
import hmac
import hashlib
import os
import json

app = Flask(__name__)

@app.route('/webhook/daimon', methods=['POST'])
def webhook():
    signature = request.headers.get('X-Daimon-Signature')
    secret = os.environ['WEBHOOK_SECRET'].encode()

    # Verify HMAC signature
    expected_signature = hmac.new(
        secret,
        request.data,
        hashlib.sha256
    ).hexdigest()

    if signature != expected_signature:
        print('Invalid signature')
        return 'Unauthorized', 401

    # Parse payload
    payload = json.loads(request.data)

    # Handle event
    if payload['event'] == 'message.received':
        print(f"New message: {payload['message']['subject']}")
        # Process the message

    # Acknowledge receipt
    return 'OK', 200

if __name__ == '__main__':
    app.run(port=3000)
package main

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

type WebhookPayload struct {
    Event   string                 `json:"event"`
    Message map[string]interface{} `json:"message"`
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    signature := r.Header.Get("X-Daimon-Signature")
    secret := []byte(os.Getenv("WEBHOOK_SECRET"))

    // Read body
    body, _ := io.ReadAll(r.Body)

    // Verify HMAC signature
    mac := hmac.New(sha256.New, secret)
    mac.Write(body)
    expectedSignature := hex.EncodeToString(mac.Sum(nil))

    if signature != expectedSignature {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    // Parse payload
    var payload WebhookPayload
    json.Unmarshal(body, &payload)

    // Handle event
    if payload.Event == "message.received" {
        subject := payload.Message["subject"].(string)
        println("New message:", subject)
        // Process the message
    }

    // Acknowledge receipt
    w.WriteHeader(http.StatusOK)
    w.Write([]byte("OK"))
}

func main() {
    http.HandleFunc("/webhook/daimon", webhookHandler)
    http.ListenAndServe(":3000", nil)
}

Warning

Never skip signature verification. Without it, anyone can send fake webhook events to your endpoint.

Step 2: Register Webhook

Create the webhook via API using your account API key (not inbox API key):

curl -X POST https://api.daimon.email/v1/webhooks \
  -H "Authorization: Bearer dm_live_account123..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-platform.com/webhook/daimon",
    "events": ["message.received", "message.bounced"],
    "inbox_id": "inbox_abc123"
  }'
import { DaimonClient } from 'daimon-email';

const client = new DaimonClient({
  apiKey: accountApiKey // Use account key, not inbox key
});

const webhook = await client.webhooks.create({
  url: 'https://your-platform.com/webhook/daimon',
  events: ['message.received', 'message.bounced'],
  inboxId: 'inbox_abc123' // Optional: filter to specific inbox
});

console.log('Webhook created:', webhook.id);
console.log('Secret:', webhook.secret);

// Store the secret securely for signature verification
await secretsManager.store('webhook-secret', webhook.secret);
from daimon_email import DaimonClient

client = DaimonClient(api_key=account_api_key)

webhook = client.webhooks.create(
    url='https://your-platform.com/webhook/daimon',
    events=['message.received', 'message.bounced'],
    inbox_id='inbox_abc123'  # Optional: filter to specific inbox
)

print(f"Webhook created: {webhook.id}")
print(f"Secret: {webhook.secret}")

# Store the secret securely for signature verification
secrets_manager.store('webhook-secret', webhook.secret)

Response

{
  "result": {
    "id": "wh_abc123",
    "url": "https://your-platform.com/webhook/daimon",
    "events": ["message.received", "message.bounced"],
    "inbox_id": "inbox_abc123",
    "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
    "status": "active",
    "created_at": "2026-03-16T10:30:00Z"
  },
  "next_steps": [
    "Store the secret securely — it's only shown once",
    "Use the secret to verify HMAC signatures on incoming webhooks",
    "Test the webhook with POST /v1/webhooks/{id}/test"
  ]
}

Warning

The webhook secret is only returned once during creation. Store it securely in your secrets manager. If you lose it, you'll need to rotate the webhook secret via POST /v1/webhooks/{id}/rotate-secret.

Step 3: Test Your Webhook

Send a test event to verify your endpoint is working:

curl -X POST https://api.daimon.email/v1/webhooks/wh_abc123/test \
  -H "Authorization: Bearer dm_live_account123..."
const testResult = await client.webhooks.test('wh_abc123');

console.log('Test status:', testResult.status);
console.log('Response code:', testResult.responseCode);
console.log('Latency:', testResult.latencyMs, 'ms');
test_result = client.webhooks.test('wh_abc123')

print(f"Test status: {test_result.status}")
print(f"Response code: {test_result.response_code}")
print(f"Latency: {test_result.latency_ms} ms")

Response

{
  "result": {
    "status": "success",
    "response_code": 200,
    "response_body": "OK",
    "latency_ms": 123,
    "tested_at": "2026-03-16T10:35:00Z"
  }
}

If the test fails, check:

  • Is your endpoint reachable via HTTPS?
  • Does it return 200 OK within 5 seconds?
  • Is HMAC signature verification implemented correctly?

Webhook Filtering

Filter by Inbox

Limit webhook events to a specific inbox:

const webhook = await client.webhooks.create({
  url: 'https://your-platform.com/webhook/daimon',
  events: ['message.received'],
  inboxId: 'inbox_abc123' // Only receive events for this inbox
});

Without inboxId, the webhook receives events for all inboxes in your account.

Filter by Event Type

Subscribe to only the events you care about:

const webhook = await client.webhooks.create({
  url: 'https://your-platform.com/webhook/daimon',
  events: [
    'message.received',  // New emails
    'message.bounced'    // Bounces only
  ]
  // Ignores: message.complaint, account.upgraded, webhook.unhealthy
});

Info

You can create multiple webhooks with different event filters and inbox filters.

Webhook Health Monitoring

daimon.email monitors webhook health automatically and marks webhooks as unhealthy after consecutive failures.

Health Check Criteria

A webhook is marked unhealthy if:

  • 5 consecutive requests return non-200 status codes
  • 5 consecutive requests timeout (>5 seconds)
  • 5 consecutive requests fail with network errors

When a Webhook Goes Unhealthy

  1. Webhook is paused: No more events are delivered
  2. Event is sent: You receive a webhook.unhealthy event
  3. Retries stop: Failed events are dropped (not queued)
{
  "event": "webhook.unhealthy",
  "webhook_id": "wh_abc123",
  "url": "https://your-platform.com/webhook/daimon",
  "failure_count": 5,
  "last_error": "Connection timeout after 5000ms",
  "detected_at": "2026-03-16T11:00:00Z",
  "next_steps": [
    "Check your endpoint is reachable and returning 200 OK",
    "Fix the issue and re-enable via POST /v1/webhooks/{id}/enable"
  ]
}

Re-enabling an Unhealthy Webhook

Once you've fixed the issue:

curl -X POST https://api.daimon.email/v1/webhooks/wh_abc123/enable \
  -H "Authorization: Bearer dm_live_account123..."

Response

{
  "result": {
    "id": "wh_abc123",
    "status": "active",
    "health": "healthy",
    "re_enabled_at": "2026-03-16T11:05:00Z"
  }
}

Retry Logic

daimon.email retries failed webhook deliveries with exponential backoff:

AttemptDelayTotal Time
1Immediate0s
25s5s
315s20s
445s1m 5s
5135s3m 20s

After 5 failed attempts, the webhook is marked unhealthy and retries stop.

Note

To avoid losing events during downtime, implement a fallback polling mechanism or use a message queue (e.g., SQS, RabbitMQ) between daimon.email webhooks and your agent workers.

Best Practices

Verify Signatures

Always verify HMAC signatures. Never trust webhook payloads without verification.

Return 200 Quickly

Acknowledge receipt immediately. Process events asynchronously in a background job.

Use Idempotency Keys

Store event_id in your database to prevent duplicate processing if webhooks are retried.

Monitor Health

Set up alerts for webhook.unhealthy events. Monitor latency and error rates.

Example: Idempotent Event Processing

import { DaimonWebhookPayload } from 'daimon-email';

async function handleWebhook(payload: DaimonWebhookPayload) {
  const eventId = payload.event_id;

  // Check if we've already processed this event
  const existing = await db.events.findOne({ eventId });

  if (existing) {
    console.log(`Event ${eventId} already processed, skipping`);
    return; // Idempotent: safe to retry
  }

  // Process the event
  if (payload.event === 'message.received') {
    await processMessage(payload.message);
  }

  // Mark as processed
  await db.events.create({ eventId, processedAt: new Date() });
}

Managing Webhooks

List All Webhooks

curl -X GET https://api.daimon.email/v1/webhooks \
  -H "Authorization: Bearer dm_live_account123..."

Response

{
  "result": {
    "webhooks": [
      {
        "id": "wh_abc123",
        "url": "https://your-platform.com/webhook/daimon",
        "events": ["message.received"],
        "status": "active",
        "health": "healthy"
      },
      {
        "id": "wh_def456",
        "url": "https://backup.com/webhook",
        "events": ["message.bounced", "message.complaint"],
        "status": "paused",
        "health": "unhealthy"
      }
    ],
    "total": 2
  }
}

Update Webhook

Change webhook URL or event filters:

curl -X PATCH https://api.daimon.email/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer dm_live_account123..." \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["message.received", "message.bounced", "message.complaint"]
  }'

Delete Webhook

curl -X DELETE https://api.daimon.email/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer dm_live_account123..."

Warning

Deleting a webhook is permanent. You cannot recover the webhook secret after deletion.

Debugging Webhooks

View Webhook Logs

Check recent delivery attempts and failures:

curl -X GET https://api.daimon.email/v1/webhooks/wh_abc123/logs \
  -H "Authorization: Bearer dm_live_account123..."

Response

{
  "result": {
    "logs": [
      {
        "event_id": "evt_abc123",
        "event_type": "message.received",
        "delivered_at": "2026-03-16T10:40:00Z",
        "response_code": 200,
        "latency_ms": 89
      },
      {
        "event_id": "evt_def456",
        "event_type": "message.received",
        "attempted_at": "2026-03-16T10:45:00Z",
        "response_code": 500,
        "error": "Internal Server Error",
        "latency_ms": 1234,
        "retry_count": 3
      }
    ],
    "total": 2
  }
}

Common Issues

Webhook returns 401 Unauthorized

Problem: HMAC signature verification is failing.

Solution:

  • Verify you're using the correct webhook secret
  • Check signature computation matches Verifying HMAC guide
  • Ensure request body is used as-is (no parsing before verification)
Webhook times out after 5 seconds

Problem: Your endpoint is too slow.

Solution:

  • Return 200 OK immediately after receiving the webhook
  • Process events asynchronously in a background job queue
  • Offload heavy work to workers (don't block the webhook handler)
Webhook delivers duplicate events

Problem: Retries are causing duplicate processing.

Solution:

  • Store event_id in your database
  • Check if event_id exists before processing
  • Make your event handlers idempotent
Webhook goes unhealthy frequently

Problem: Endpoint is unreliable or slow.

Solution:

  • Monitor endpoint uptime and latency
  • Use a load balancer with health checks
  • Scale your webhook handler horizontally
  • Consider using a message queue (SQS, RabbitMQ) for buffering

Next Steps