Setting Up Webhooks
Learn how to set up and handle webhooks for real-time event notifications.
Setting Up Webhooks
Webhooks allow you to receive real-time notifications when events occur in your Transactional account. This guide covers setting up, securing, and handling webhooks.
Overview
When you configure a webhook, Transactional sends HTTP POST requests to your specified URL whenever subscribed events occur. This enables real-time integrations without polling the API.
Creating a Webhook
Via Dashboard
- Navigate to Settings > Webhooks
- Click Add Webhook
- Enter your endpoint URL
- Select the events you want to receive
- Click Create
Via API
import { Transactional } from 'transactional-sdk';
const client = new Transactional({
apiKey: process.env.TRANSACTIONAL_API_KEY,
});
const webhook = await client.webhooks.create({
url: 'https://your-app.com/webhooks/transactional',
events: [
'email.delivered',
'email.bounced',
'email.opened',
'email.clicked',
'sms.delivered',
'sms.failed',
'form.submitted',
],
});
console.log('Webhook ID:', webhook.id);
console.log('Signing Secret:', webhook.signingSecret);Available Events
Email Events
| Event | Description |
|---|---|
email.queued | Email accepted and queued |
email.sent | Email sent to recipient server |
email.delivered | Email delivered to inbox |
email.bounced | Email bounced (hard or soft) |
email.opened | Recipient opened the email |
email.clicked | Recipient clicked a link |
email.unsubscribed | Recipient unsubscribed |
email.spam_complaint | Marked as spam |
SMS Events
| Event | Description |
|---|---|
sms.queued | SMS queued for sending |
sms.sent | SMS sent to carrier |
sms.delivered | SMS delivered to recipient |
sms.failed | SMS delivery failed |
sms.inbound | Inbound SMS received |
Form Events
| Event | Description |
|---|---|
form.submitted | Form submission received |
form.partial | Partial submission saved |
Webhook Payload
All webhooks follow this structure:
{
"id": "evt_abc123",
"type": "email.delivered",
"timestamp": "2024-01-15T10:30:00Z",
"data": {
"messageId": "msg_xyz789",
"recipient": "user@example.com",
"subject": "Welcome!",
"deliveredAt": "2024-01-15T10:30:00Z"
}
}Handling Webhooks
Basic Handler (Express.js)
import express from 'express';
import crypto from 'crypto';
const app = express();
// Use raw body for signature verification
app.post('/webhooks/transactional',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-transactional-signature'];
const timestamp = req.headers['x-transactional-timestamp'];
const payload = req.body.toString();
// Verify signature
if (!verifySignature(payload, signature, timestamp)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
// Process the event
switch (event.type) {
case 'email.delivered':
handleEmailDelivered(event.data);
break;
case 'email.bounced':
handleEmailBounced(event.data);
break;
case 'sms.delivered':
handleSmsDelivered(event.data);
break;
case 'form.submitted':
handleFormSubmission(event.data);
break;
default:
console.log('Unhandled event type:', event.type);
}
// Respond quickly with 200 OK
res.sendStatus(200);
}
);
function verifySignature(
payload: string,
signature: string,
timestamp: string
): boolean {
const signingSecret = process.env.WEBHOOK_SIGNING_SECRET;
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', signingSecret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}Using the SDK Webhook Helper
import { verifyWebhookSignature } from 'transactional-sdk';
app.post('/webhooks/transactional',
express.raw({ type: 'application/json' }),
(req, res) => {
try {
const event = verifyWebhookSignature(
req.body.toString(),
req.headers['x-transactional-signature'],
req.headers['x-transactional-timestamp'],
process.env.WEBHOOK_SIGNING_SECRET
);
// Event is verified and parsed
handleEvent(event);
res.sendStatus(200);
} catch (error) {
console.error('Webhook verification failed:', error);
res.status(401).send('Invalid signature');
}
}
);Event Handlers
Email Events
async function handleEmailDelivered(data: EmailDeliveredEvent) {
const { messageId, recipient, deliveredAt } = data;
// Update your database
await db.emailLogs.update({
where: { messageId },
data: { status: 'DELIVERED', deliveredAt },
});
// Update analytics
await analytics.track('Email Delivered', {
messageId,
recipient,
deliveredAt,
});
}
async function handleEmailBounced(data: EmailBouncedEvent) {
const { messageId, recipient, bounceType, bounceMessage } = data;
if (bounceType === 'HARD') {
// Remove from mailing list
await db.subscribers.update({
where: { email: recipient },
data: { status: 'BOUNCED', bounceReason: bounceMessage },
});
}
// Log the bounce
await db.emailLogs.update({
where: { messageId },
data: { status: 'BOUNCED', bounceType, bounceMessage },
});
}
async function handleEmailOpened(data: EmailOpenedEvent) {
const { messageId, recipient, openedAt, userAgent, ipAddress } = data;
// Track open event
await db.emailEvents.create({
data: {
messageId,
eventType: 'OPENED',
timestamp: openedAt,
metadata: { userAgent, ipAddress },
},
});
}SMS Events
async function handleSmsDelivered(data: SmsDeliveredEvent) {
const { messageId, to, deliveredAt } = data;
await db.smsLogs.update({
where: { messageId },
data: { status: 'DELIVERED', deliveredAt },
});
}
async function handleSmsFailed(data: SmsFailedEvent) {
const { messageId, to, errorCode, errorMessage } = data;
await db.smsLogs.update({
where: { messageId },
data: { status: 'FAILED', errorCode, errorMessage },
});
// Alert on failures
if (errorCode === 'CARRIER_REJECTED') {
await alerting.notify('SMS delivery failed', { messageId, to, errorMessage });
}
}Form Events
async function handleFormSubmission(data: FormSubmittedEvent) {
const { formId, submissionId, data: formData } = data;
// Store submission
await db.submissions.create({
data: {
formId,
submissionId,
data: formData,
receivedAt: new Date(),
},
});
// Send notification email
await client.emails.send({
from: 'noreply@yourapp.com',
to: 'sales@yourapp.com',
subject: 'New Form Submission',
html: `<p>New submission received:</p><pre>${JSON.stringify(formData, null, 2)}</pre>`,
});
// Add to CRM
await crm.createLead({
email: formData.email,
name: formData.name,
source: 'Website Form',
});
}Best Practices
1. Respond Quickly
Return a 200 response immediately, then process asynchronously:
app.post('/webhooks/transactional', (req, res) => {
// Respond immediately
res.sendStatus(200);
// Process asynchronously
processWebhook(req.body).catch(console.error);
});
async function processWebhook(event) {
// Long-running processing here
}2. Use a Queue
For high-volume webhooks, use a message queue:
import { Queue } from 'bullmq';
const webhookQueue = new Queue('webhooks');
app.post('/webhooks/transactional', async (req, res) => {
// Verify signature
const event = verifyWebhookSignature(req.body, req.headers);
// Add to queue
await webhookQueue.add('process', event);
res.sendStatus(200);
});
// Worker processes events
const worker = new Worker('webhooks', async (job) => {
const event = job.data;
await handleEvent(event);
});3. Handle Retries
Transactional retries failed webhooks with exponential backoff:
- 1st retry: 30 seconds
- 2nd retry: 5 minutes
- 3rd retry: 30 minutes
- 4th retry: 2 hours
- 5th retry: 24 hours
Ensure your handlers are idempotent:
async function handleEmailDelivered(data: EmailDeliveredEvent) {
const { messageId } = data;
// Check if already processed
const existing = await db.emailEvents.findUnique({
where: { messageId_eventType: { messageId, eventType: 'DELIVERED' } },
});
if (existing) {
console.log('Event already processed:', messageId);
return;
}
// Process the event
await db.emailEvents.create({
data: { messageId, eventType: 'DELIVERED', ... },
});
}4. Monitor Webhook Health
Check webhook delivery status:
const webhook = await client.webhooks.get(webhookId);
console.log('Success rate:', webhook.stats.successRate);
console.log('Last delivery:', webhook.stats.lastDeliveryAt);
console.log('Failed count:', webhook.stats.failedCount);Testing Webhooks
Local Development with ngrok
# Start ngrok tunnel
ngrok http 3000
# Use the ngrok URL for your webhook
# https://abc123.ngrok.io/webhooks/transactionalTest Events
Send a test event from the dashboard or API:
await client.webhooks.test(webhookId, {
eventType: 'email.delivered',
});Next Steps
- Email Webhooks - Email-specific webhook events
- SMS Webhooks - SMS webhook events
- Forms Webhooks - Form submission webhooks
On This Page
- Setting Up Webhooks
- Overview
- Creating a Webhook
- Via Dashboard
- Via API
- Available Events
- Email Events
- SMS Events
- Form Events
- Webhook Payload
- Handling Webhooks
- Basic Handler (Express.js)
- Using the SDK Webhook Helper
- Event Handlers
- Email Events
- SMS Events
- Form Events
- Best Practices
- 1. Respond Quickly
- 2. Use a Queue
- 3. Handle Retries
- 4. Monitor Webhook Health
- Testing Webhooks
- Local Development with ngrok
- Test Events
- Next Steps