daimon.email
Api referenceWebhooks

Testing Webhooks

How to test webhook delivery and verify HMAC signatures

Webhook Requirements

All webhook endpoints must:

  1. Use HTTPS - Plain HTTP endpoints are rejected
  2. Return HTTP 200 within 5 seconds to acknowledge receipt
  3. Accept POST requests with JSON body
  4. Verify HMAC signatures to prevent spoofing

Event Payload Structure

All webhook events follow this structure:

{
  "event": "message.received",
  "webhook_id": "wh_xyz789",
  "inbox_id": "inb_abc123",
  "timestamp": "2024-03-11T15:30:00Z",
  "data": {
    // Event-specific payload
  }
}

message.received Event

{
  "event": "message.received",
  "webhook_id": "wh_xyz789",
  "inbox_id": "inb_abc123",
  "timestamp": "2024-03-11T15:30:00Z",
  "data": {
    "message": {
      "id": "msg_abc123",
      "from": "sender@example.com",
      "to": "my-agent@daimon.email",
      "subject": "Test Email",
      "body_text": "Hello from the webhook!",
      "body_html": "<p>Hello from the webhook!</p>",
      "reply_body": "Hello from the webhook!",
      "links": ["https://example.com/verify?token=xyz"],
      "cta_links": ["https://example.com/verify?token=xyz"],
      "received_at": "2024-03-11T15:30:00Z"
    }
  }
}

HMAC Signature Verification

Every webhook delivery includes an X-Daimon-Signature header containing an HMAC-SHA256 signature. Always verify this signature to prevent spoofed requests.

import crypto from 'crypto';

function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const hmac = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(hmac)
  );
}

// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-daimon-signature'] as string;
  const payload = req.body.toString();

  if (!verifyWebhookSignature(payload, signature, webhookSecret)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);

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

  res.status(200).send('OK');
});
import hmac
import hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    computed = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, computed)

# Flask example
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Daimon-Signature')
    payload = request.get_data()

    if not verify_webhook_signature(payload, signature, webhook_secret):
        return 'Invalid signature', 401

    event = request.get_json()

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

    return 'OK', 200
from fastapi import FastAPI, Request, Response, HTTPException
import hmac
import hashlib

app = FastAPI()

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    computed = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(signature, computed)

@app.post('/webhook')
async def handle_webhook(request: Request):
    signature = request.headers.get('x-daimon-signature')
    payload = await request.body()

    if not verify_webhook_signature(payload, signature, webhook_secret):
        raise HTTPException(status_code=401, detail='Invalid signature')

    event = await request.json()

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

    return Response(status_code=200, content='OK')

Testing with ngrok

For local development, use ngrok to expose your webhook endpoint:

# Start ngrok tunnel
ngrok http 3000

# Use the HTTPS URL in your webhook
curl -X POST https://api.daimon.email/v1/webhooks \
  -H "Authorization: Bearer dm_free_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://abc123.ngrok.io/webhook",
    "events": ["message.received"]
  }'

Webhook Health & Failure Handling

Webhooks are marked unhealthy after 5 consecutive failures:

  • Timeout (no response within 5 seconds)
  • Non-2xx HTTP status code
  • Connection errors (DNS, TLS, etc.)

After 10 consecutive failures, webhooks are automatically disabled.

Monitoring Webhook Health

const webhook = await client.webhooks.get('wh_xyz789');

if (webhook.status === 'unhealthy') {
  console.warn(`Webhook unhealthy: ${webhook.failure_count} failures`);
  console.log(`Last success: ${webhook.last_delivered_at}`);

  // Re-enable after fixing endpoint
  await client.webhooks.update('wh_xyz789', { status: 'active' });
}

Retry Behavior

  • Failed deliveries are retried with exponential backoff
  • Retry intervals: 1s, 5s, 30s, 2m, 10m
  • After 5 failures, webhook is marked unhealthy but retries continue
  • After 10 failures, webhook is disabled and retries stop

Best Practices

Return 200 immediately

Process webhooks asynchronously. Respond with HTTP 200 within 5 seconds, then handle the event in a background job.

app.post('/webhook', async (req, res) => {
  // Verify signature
  if (!verifySignature(req.body, req.headers['x-daimon-signature'])) {
    return res.status(401).send('Invalid signature');
  }

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

  // Process asynchronously
  await queue.add('process-webhook', req.body);
});
Implement idempotency

Webhooks may be delivered more than once. Use the message.id to deduplicate events.

const messageId = event.data.message.id;

if (await redis.exists(`processed:${messageId}`)) {
  return; // Already processed
}

await processMessage(event.data.message);
await redis.setex(`processed:${messageId}`, 86400, '1'); // 24hr TTL
Always verify signatures

Never process unverified webhooks. Attackers can spoof webhook events.

if (!verifyWebhookSignature(payload, signature, secret)) {
  return res.status(401).send('Invalid signature');
}
Monitor webhook health

Set up alerts for unhealthy webhooks. Check the /v1/webhooks endpoint regularly.

const webhooks = await client.webhooks.list({ status: 'unhealthy' });

if (webhooks.length > 0) {
  await alertOps(`${webhooks.length} unhealthy webhooks`);
}

Testing Signature Verification

You can test signature verification with this example payload:

# Secret from webhook creation
SECRET="whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"

# Example payload
PAYLOAD='{"event":"message.received","webhook_id":"wh_xyz789"}'

# Generate signature
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)

echo "X-Daimon-Signature: $SIGNATURE"

# Test your endpoint
curl -X POST https://your-endpoint.com/webhook \
  -H "Content-Type: application/json" \
  -H "X-Daimon-Signature: $SIGNATURE" \
  -d "$PAYLOAD"

Info

Debugging tip: Log the raw request body and computed signature. Common issues include: body parsing (use raw body), character encoding, and timing attacks (use constant-time comparison).