Webhooks send real-time HTTP notifications to your application when email events occur. Instead of polling for updates, get instant notifications.

Table of Contents

What Are Webhooks?

Webhooks are HTTP callbacks triggered by events. When something happens with your emails (sent, delivered, bounced, etc.), TrackPost sends a POST request to your URL.

Example Flow:

  1. You send an email via API
  2. Email is delivered to recipient
  3. TrackPost sends webhook to your endpoint
  4. Your application processes the event
  5. You update your database/send notification/etc.

vs. Polling:

MethodProsCons
WebhooksReal-time, efficientRequires endpoint
PollingSimpleDelayed, wasteful

Setting Up Webhooks

Via Dashboard

  1. Log in to TrackPost Dashboard
  2. Navigate to Webhooks
  3. Click Add Webhook
  4. Configure:
    • URL: Your endpoint (must be HTTPS)
    • Events: Select which events to receive
    • Secret: Optional, for signature verification
  5. Click Create Webhook

Via API

curl -X POST https://api.trackpost.de/v1/webhooks \
  -H "Authorization: Bearer tp_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/trackpost",
    "events": [
      "email.delivered",
      "email.bounced",
      "email.opened",
      "email.clicked"
    ],
    "secret": "your_webhook_secret"
  }'

Via CLI

trackpost webhooks create \
  --url https://yourapp.com/webhooks/trackpost \
  --events email.delivered,email.bounced \
  --secret your_webhook_secret

Event Types

Email Events

EventDescriptionWhen It Fires
email.sentEmail queued for deliveryImmediately after API call
email.deliveredEmail delivered to recipient serverWhen recipient accepts email
email.openedRecipient opened emailWhen tracking pixel loads
email.clickedRecipient clicked a linkWhen tracked link clicked
email.bouncedEmail bouncedHard or soft bounce
email.complainedRecipient marked as spamSpam complaint received
email.droppedEmail not sentInvalid address or suppressed

Template Events

EventDescription
template.createdNew template created
template.updatedTemplate modified
template.deletedTemplate removed

System Events

EventDescription
domain.verifiedDomain verification completed
api_key.createdNew API key generated
api_key.revokedAPI key deactivated

Webhook Payloads

Email Sent

{
  "id": "evt_abc123",
  "type": "email.sent",
  "created_at": "2025-01-15T10:30:00Z",
  "data": {
    "email_id": "msg_def456",
    "to": "[email protected]",
    "from": "[email protected]",
    "subject": "Welcome!",
    "template_id": "welcome_email",
    "tags": ["onboarding", "welcome"]
  }
}

Email Delivered

{
  "id": "evt_abc124",
  "type": "email.delivered",
  "created_at": "2025-01-15T10:30:02Z",
  "data": {
    "email_id": "msg_def456",
    "to": "[email protected]",
    "delivered_at": "2025-01-15T10:30:02Z",
    "smtp_response": "250 2.0.0 OK"
  }
}

Email Opened

{
  "id": "evt_abc125",
  "type": "email.opened",
  "created_at": "2025-01-15T10:35:15Z",
  "data": {
    "email_id": "msg_def456",
    "to": "[email protected]",
    "opened_at": "2025-01-15T10:35:15Z",
    "ip_address": "192.168.1.100",
    "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
    "location": {
      "country": "US",
      "city": "New York"
    }
  }
}

Email Clicked

{
  "id": "evt_abc126",
  "type": "email.clicked",
  "created_at": "2025-01-15T10:36:20Z",
  "data": {
    "email_id": "msg_def456",
    "to": "[email protected]",
    "clicked_at": "2025-01-15T10:36:20Z",
    "url": "https://yourapp.com/activate?token=abc123",
    "ip_address": "192.168.1.100",
    "user_agent": "Mozilla/5.0...",
    "location": {
      "country": "US",
      "city": "New York"
    }
  }
}

Email Bounced

{
  "id": "evt_abc127",
  "type": "email.bounced",
  "created_at": "2025-01-15T10:30:05Z",
  "data": {
    "email_id": "msg_def456",
    "to": "[email protected]",
    "bounced_at": "2025-01-15T10:30:05Z",
    "bounce_type": "hard",
    "reason": "User not found",
    "smtp_response": "550 5.1.1 The email account does not exist"
  }
}

Bounce Types:

  • hard - Permanent failure (bad address)
  • soft - Temporary failure (mailbox full, server down)

Email Complained

{
  "id": "evt_abc128",
  "type": "email.complained",
  "created_at": "2025-01-15T14:20:00Z",
  "data": {
    "email_id": "msg_def456",
    "to": "[email protected]",
    "complained_at": "2025-01-15T14:20:00Z",
    "complaint_type": "abuse",
    "feedback_id": "feedback_123"
  }
}

Implementing Webhook Handlers

Basic Express.js Example

const express = require('express');
const crypto = require('crypto');
const app = express();

app.use(express.json());

// Verify webhook signature (optional but recommended)
function verifySignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  return signature === expected;
}

app.post('/webhooks/trackpost', async (req, res) => {
  // Verify signature if using secret
  const signature = req.headers['x-trackpost-signature'];
  if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
  
  const event = req.body;
  
  try {
    switch (event.type) {
      case 'email.delivered':
        await handleDelivered(event.data);
        break;
        
      case 'email.bounced':
        await handleBounced(event.data);
        break;
        
      case 'email.opened':
        await handleOpened(event.data);
        break;
        
      case 'email.clicked':
        await handleClicked(event.data);
        break;
        
      case 'email.complained':
        await handleComplained(event.data);
        break;
    }
    
    // Always return 200 to acknowledge receipt
    res.status(200).send('OK');
  } catch (error) {
    console.error('Webhook error:', error);
    // Still return 200 to prevent retries
    res.status(200).send('OK');
  }
});

async function handleDelivered(data) {
  // Update your database
  await db.emails.update(data.email_id, { 
    status: 'delivered',
    delivered_at: data.delivered_at 
  });
  
  // Maybe trigger a follow-up action
  console.log(`Email ${data.email_id} delivered to ${data.to}`);
}

async function handleBounced(data) {
  if (data.bounce_type === 'hard') {
    // Remove from mailing list
    await db.users.updateEmailStatus(data.to, 'bounced');
    await db.emails.update(data.email_id, { 
      status: 'bounced',
      bounce_reason: data.reason 
    });
  }
  
  // Alert admin if bounce rate spikes
  const recentBounces = await db.getRecentBounceCount(24);
  if (recentBounces > 100) {
    await alertAdmin('High bounce rate detected');
  }
}

async function handleOpened(data) {
  await db.emails.update(data.email_id, { 
    opened_at: data.opened_at,
    $inc: { opens_count: 1 }
  });
  
  // Track engagement score
  await db.users.updateEngagement(data.to, 'email_opened');
}

async function handleClicked(data) {
  await db.clicks.create({
    email_id: data.email_id,
    url: data.url,
    clicked_at: data.clicked_at,
    ip_address: data.ip_address
  });
}

async function handleComplained(data) {
  // Immediately suppress this email
  await db.users.updateEmailStatus(data.to, 'complained');
  await db.suppressions.add(data.to, 'complaint');
  
  // Alert team
  await alertAdmin(`Spam complaint from ${data.to}`);
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Python (Flask) Example

from flask import Flask, request, jsonify
import hmac
import hashlib
import json

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"

def verify_signature(payload, signature, secret):
    expected = hmac.new(
        secret.encode(),
        json.dumps(payload).encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

@app.route('/webhooks/trackpost', methods=['POST'])
def handle_webhook():
    # Verify signature
    signature = request.headers.get('X-Trackpost-Signature')
    if not verify_signature(request.json, signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401
    
    event = request.json
    event_type = event['type']
    data = event['data']
    
    if event_type == 'email.delivered':
        handle_delivered(data)
    elif event_type == 'email.bounced':
        handle_bounced(data)
    elif event_type == 'email.opened':
        handle_opened(data)
    elif event_type == 'email.clicked':
        handle_clicked(data)
    
    return 'OK', 200

def handle_delivered(data):
    print(f"Email {data['email_id']} delivered to {data['to']}")
    # Update database, trigger actions...

def handle_bounced(data):
    if data['bounce_type'] == 'hard':
        print(f"Hard bounce from {data['to']}: {data['reason']}")
        # Remove from list
    else:
        print(f"Soft bounce from {data['to']}: {data['reason']}")
        # Retry later

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

Serverless (AWS Lambda)

const crypto = require('crypto');

exports.handler = async (event) => {
  const body = JSON.parse(event.body);
  const signature = event.headers['x-trackpost-signature'];
  
  // Verify signature
  if (!verifySignature(body, signature, process.env.WEBHOOK_SECRET)) {
    return {
      statusCode: 401,
      body: 'Invalid signature'
    };
  }
  
  // Process webhook
  await processWebhook(body);
  
  return {
    statusCode: 200,
    body: 'OK'
  };
};

function verifySignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
  return signature === expected;
}

async function processWebhook(event) {
  switch (event.type) {
    case 'email.delivered':
      await handleDelivered(event.data);
      break;
    case 'email.bounced':
      await handleBounced(event.data);
      break;
  }
}

Webhook Security

Signature Verification

Always verify webhook signatures to ensure requests are from TrackPost:

const crypto = require('crypto');

function verifyWebhook(body, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(body))
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Best Practices

  1. Use HTTPS Only - Never accept webhooks over HTTP
  2. Verify Signatures - Always validate the webhook signature
  3. Return 200 Quickly - Acknowledge receipt immediately, process async
  4. Idempotency - Handle duplicate webhooks gracefully (use event.id)
  5. IP Allowlisting - Optional: restrict to TrackPost IP ranges
  6. Timeout Handling - TrackPost waits 30 seconds for response
  7. Retry Logic - TrackPost retries failed webhooks 3 times

IP Addresses

If you need to allowlist TrackPost webhook IPs:

Production: 104.21.0.0/16, 172.64.0.0/16

Warning

IP addresses may change. Prefer signature verification over IP allowlisting.

Testing Webhooks

Local Development

Use ngrok to test locally:

# Install ngrok
npm install -g ngrok

# Start your webhook server
node webhook-server.js

# Expose to internet
ngrok http 3000

# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
# Use this as your webhook URL in TrackPost

Webhook Testing Tools

Test Events

Send test webhooks from dashboard:

  1. Dashboard → Webhooks
  2. Click on your webhook
  3. Click Send Test Event
  4. Select event type
  5. Click Send

Troubleshooting

Webhooks Not Received

Checklist:

  • URL is HTTPS (required)
  • URL is publicly accessible
  • No firewall blocking requests
  • Webhook is active (not paused)

401 Unauthorized

  • Signature verification failing
  • Wrong secret configured
  • Signature header not being read

Timeouts

  • Your server takes too long to respond
  • Return 200 immediately, process asynchronously
  • Check server logs for errors

Duplicate Events

TrackPost may send duplicate webhooks on retries. Handle with idempotency:

// Check if already processed
const existing = await db.webhooks.findOne({ 
  event_id: event.id 
});

if (existing) {
  return res.status(200).send('Already processed');
}

// Process and record
await processEvent(event);
await db.webhooks.insert({ 
  event_id: event.id, 
  processed_at: new Date() 
});

Advanced Topics

Batch Processing

For high volume, batch webhook processing:

const webhookQueue = [];

app.post('/webhooks/trackpost', (req, res) => {
  webhookQueue.push(req.body);
  res.status(200).send('OK');
});

// Process every 5 seconds
setInterval(async () => {
  if (webhookQueue.length === 0) return;
  
  const batch = webhookQueue.splice(0, 100);
  await processBatch(batch);
}, 5000);

Filtering Events

Only subscribe to events you need:

// Instead of subscribing to all events,
// be selective to reduce noise:

const importantEvents = [
  'email.bounced',      // Clean your lists
  'email.complained',   // Handle spam reports
  'email.delivered'     // Confirm delivery
];

Webhook Versioning

TrackPost may update webhook payloads. Check version:

const event = req.body;

if (event.version !== '1.0') {
  console.warn(`Unknown webhook version: ${event.version}`);
  // Handle accordingly or ignore
}

Next Steps