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:
- You send an email via API
- Email is delivered to recipient
- TrackPost sends webhook to your endpoint
- Your application processes the event
- You update your database/send notification/etc.
vs. Polling:
| Method | Pros | Cons |
|---|---|---|
| Webhooks | Real-time, efficient | Requires endpoint |
| Polling | Simple | Delayed, wasteful |
Setting Up Webhooks
Via Dashboard
- Log in to TrackPost Dashboard
- Navigate to Webhooks
- Click Add Webhook
- Configure:
- URL: Your endpoint (must be HTTPS)
- Events: Select which events to receive
- Secret: Optional, for signature verification
- 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
| Event | Description | When It Fires |
|---|---|---|
email.sent | Email queued for delivery | Immediately after API call |
email.delivered | Email delivered to recipient server | When recipient accepts email |
email.opened | Recipient opened email | When tracking pixel loads |
email.clicked | Recipient clicked a link | When tracked link clicked |
email.bounced | Email bounced | Hard or soft bounce |
email.complained | Recipient marked as spam | Spam complaint received |
email.dropped | Email not sent | Invalid address or suppressed |
Template Events
| Event | Description |
|---|---|
template.created | New template created |
template.updated | Template modified |
template.deleted | Template removed |
System Events
| Event | Description |
|---|---|
domain.verified | Domain verification completed |
api_key.created | New API key generated |
api_key.revoked | API 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
- Use HTTPS Only - Never accept webhooks over HTTP
- Verify Signatures - Always validate the webhook signature
- Return 200 Quickly - Acknowledge receipt immediately, process async
- Idempotency - Handle duplicate webhooks gracefully (use event.id)
- IP Allowlisting - Optional: restrict to TrackPost IP ranges
- Timeout Handling - TrackPost waits 30 seconds for response
- 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
- ngrok - Local tunneling
- RequestBin - Inspect HTTP requests
- Postman - Test your handler
Test Events
Send test webhooks from dashboard:
- Dashboard → Webhooks
- Click on your webhook
- Click Send Test Event
- Select event type
- 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
- Tracking - View analytics in dashboard
- Templates - Optimize email content
- API Reference - Full webhook API
- Troubleshooting - Common webhook issues