Webhooks

Receive real-time notifications about email events via webhooks.

Overview

Webhooks send HTTP POST requests to your server when email events occur. Use them to:

  • Update your database when emails are delivered
  • Trigger workflows when links are clicked
  • Handle bounces and complaints automatically
  • Sync email status with your CRM

Supported Events

EventDescription
sentEmail accepted for delivery
deliveredEmail delivered to recipient's server
openedRecipient opened the email
clickedRecipient clicked a link
bouncedEmail bounced (hard or soft)
complainedRecipient marked as spam
unsubscribedRecipient unsubscribed

Setting Up Webhooks

Via Dashboard

  1. Go to Email > Servers > [Server] > Webhooks
  2. Click Add Webhook
  3. Configure the webhook:
    • URL - Your endpoint URL
    • Events - Which events to receive
    • Secret - For signature verification
  4. Click Create

Via API

const webhook = await client.webhooks.create({
  serverId: 'srv_123',
  url: 'https://yoursite.com/webhooks/email',
  events: ['delivered', 'bounced', 'complained'],
  secret: 'your-webhook-secret',
});

Webhook Payloads

Delivery Event

{
  "event": "delivered",
  "timestamp": "2024-01-15T10:30:00Z",
  "messageId": "msg_abc123",
  "serverId": "srv_123",
  "streamId": "str_456",
  "from": "hello@yourapp.com",
  "to": "user@example.com",
  "subject": "Welcome to our platform"
}

Bounce Event

{
  "event": "bounced",
  "timestamp": "2024-01-15T10:30:00Z",
  "messageId": "msg_abc123",
  "serverId": "srv_123",
  "streamId": "str_456",
  "from": "hello@yourapp.com",
  "to": "invalid@example.com",
  "bounceType": "hard",
  "bounceCode": "550",
  "bounceDescription": "User not found"
}

Click Event

{
  "event": "clicked",
  "timestamp": "2024-01-15T10:35:00Z",
  "messageId": "msg_abc123",
  "serverId": "srv_123",
  "streamId": "str_456",
  "recipient": "user@example.com",
  "link": "https://yoursite.com/pricing",
  "ipAddress": "192.168.1.1",
  "userAgent": "Mozilla/5.0..."
}

Verifying Webhooks

Always verify webhook signatures to ensure requests come from Transactional.

Signature Header

Each webhook includes a signature in the X-Webhook-Signature header:

X-Webhook-Signature: sha256=abc123...

Verification Code

import crypto from 'crypto';
 
function verifyWebhookSignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
 
  return `sha256=${expectedSignature}` === signature;
}
 
// Express.js example
app.post('/webhooks/email', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-webhook-signature'] as string;
  const payload = req.body.toString();
 
  if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }
 
  const event = JSON.parse(payload);
  // Process event...
 
  res.status(200).send('OK');
});

Handling Webhooks

Respond Quickly

Return a 2xx response within 5 seconds. Process events asynchronously:

app.post('/webhooks/email', async (req, res) => {
  // Acknowledge receipt immediately
  res.status(200).send('OK');
 
  // Process asynchronously
  await processWebhookEvent(req.body);
});

Idempotency

Webhooks may be delivered multiple times. Handle duplicates:

async function processWebhookEvent(event: WebhookEvent) {
  // Check if already processed
  const exists = await db.webhookEvents.findUnique({
    where: { messageId_event: { messageId: event.messageId, event: event.event } }
  });
 
  if (exists) {
    return; // Already processed
  }
 
  // Process and record
  await db.webhookEvents.create({ data: event });
  await handleEvent(event);
}

Error Handling

Log errors but still return 200 to prevent retries:

app.post('/webhooks/email', async (req, res) => {
  try {
    await processWebhookEvent(req.body);
  } catch (error) {
    console.error('Webhook processing failed:', error);
    // Still return 200 to acknowledge receipt
  }
 
  res.status(200).send('OK');
});

Retry Policy

If your endpoint returns an error, we retry with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
68 hours
724 hours

After 7 failed attempts, the webhook is marked as failed.

Testing Webhooks

Using the Dashboard

  1. Go to your webhook settings
  2. Click Send Test Event
  3. Select an event type
  4. Check your endpoint received the event

Local Development

Use a tunnel service like ngrok for local testing:

ngrok http 3000
# Use the provided URL for your webhook endpoint

Webhook Logs

View webhook delivery logs in the dashboard:

  • Request and response details
  • Delivery status and timing
  • Failed attempt history

Best Practices

  1. Verify signatures - Always validate webhook authenticity
  2. Respond quickly - Return 200 within 5 seconds
  3. Handle duplicates - Make processing idempotent
  4. Log everything - Keep records for debugging
  5. Monitor failures - Alert on repeated failures

Next Steps