Webhook Subscriptions
Receive real-time HTTP notifications when email events occur. Webhooks eliminate the need to poll for status updates.
Overview
When you create a webhook subscription, FormaMail will send HTTP POST requests to your specified URL whenever matching events occur.
Supported Events
| Event | Description |
|---|---|
email.sent | Email successfully queued for delivery |
email.delivered | Email delivered to recipient’s mailbox |
email.bounced | Email bounced (includes bounce type) |
email.complained | Recipient marked email as spam |
email.opened | Recipient opened the email (if tracking enabled) |
email.clicked | Recipient clicked a link (if tracking enabled) |
Getting Started
Create a Webhook Endpoint
Create an HTTPS endpoint on your server that accepts POST requests:
// Express.js example
app.post('/webhooks/formamail', express.json(), (req, res) => {
const event = req.body;
console.log('Received event:', event.event);
console.log('Email ID:', event.data.emailLogId);
// Process the event
switch (event.event) {
case 'email.delivered':
// Update your database
break;
case 'email.bounced':
// Handle bounce
break;
case 'email.complained':
// Remove from mailing list
break;
}
// Always respond with 200 OK quickly
res.status(200).send('OK');
});Your endpoint must respond with 2xx status within 30 seconds, or the delivery will be marked as failed and retried.
Create a Webhook Subscription
Use the API or dashboard to create a subscription:
const response = await fetch('https://api.formamail.com/api/v1/webhook-subscriptions', {
method: 'POST',
headers: {
'Authorization': 'Bearer your-api-key',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://yourapp.com/webhooks/formamail',
events: ['email.delivered', 'email.bounced', 'email.complained'],
description: 'Production webhook for email events'
})
});
const subscription = await response.json();
// Save subscription.secret - you'll need it to verify signatures
console.log('Secret:', subscription.secret);Verify Webhook Signatures
Always verify webhook signatures to ensure requests are from FormaMail:
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return signature === `v1=${expectedSignature}`;
}
app.post('/webhooks/formamail', express.json(), (req, res) => {
const signature = req.headers['x-formamail-signature'];
const timestamp = req.headers['x-formamail-timestamp'];
if (!verifyWebhookSignature(req.body, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process verified webhook
// ...
res.status(200).send('OK');
});Test Your Webhook
Send a test event to verify your endpoint:
await fetch(`https://api.formamail.com/api/v1/webhook-subscriptions/${subscriptionId}/test`, {
method: 'POST',
headers: {
'Authorization': 'Bearer your-api-key',
'Content-Type': 'application/json'
}
});Webhook Payload Format
Every webhook delivery includes these headers and a JSON body:
Headers
| Header | Description | Example |
|---|---|---|
X-FormaMail-Signature | HMAC-SHA256 signature | v1=abc123... |
X-FormaMail-Timestamp | Unix timestamp | 1732723200 |
X-FormaMail-Event-Id | Unique event identifier | evt_xxx |
X-FormaMail-Event-Type | Event type | email.delivered |
Content-Type | Always JSON | application/json |
Body Structure
{
"event": "email.delivered",
"timestamp": "2024-11-27T12:00:00.000Z",
"data": {
"emailLogId": "550e8400-e29b-41d4-a716-446655440000",
"recipient": "user@example.com",
"subject": "Your Order Confirmation",
"status": "delivered",
"templateId": "order-confirmation",
"metadata": {
"orderId": "12345"
}
}
}Event Payloads
email.sent
{
"event": "email.sent",
"timestamp": "2024-11-27T12:00:00.000Z",
"data": {
"emailLogId": "uuid",
"recipient": "user@example.com",
"subject": "Welcome!",
"status": "sent",
"templateId": "welcome-email"
}
}email.delivered
{
"event": "email.delivered",
"timestamp": "2024-11-27T12:00:05.000Z",
"data": {
"emailLogId": "uuid",
"recipient": "user@example.com",
"subject": "Welcome!",
"status": "delivered",
"deliveredAt": "2024-11-27T12:00:05.000Z"
}
}email.bounced
{
"event": "email.bounced",
"timestamp": "2024-11-27T12:00:10.000Z",
"data": {
"emailLogId": "uuid",
"recipient": "invalid@example.com",
"subject": "Welcome!",
"status": "bounced",
"bounceType": "hard",
"bounceSubType": "NoEmail",
"bounceMessage": "Address does not exist"
}
}email.complained
{
"event": "email.complained",
"timestamp": "2024-11-27T12:05:00.000Z",
"data": {
"emailLogId": "uuid",
"recipient": "user@example.com",
"subject": "Weekly Newsletter",
"status": "complained",
"complainedAt": "2024-11-27T12:05:00.000Z",
"feedbackType": "abuse"
}
}email.opened
{
"event": "email.opened",
"timestamp": "2024-11-27T12:10:00.000Z",
"data": {
"emailLogId": "uuid",
"recipient": "user@example.com",
"subject": "Welcome!",
"openedAt": "2024-11-27T12:10:00.000Z",
"userAgent": "Mozilla/5.0...",
"ipAddress": "192.168.1.1"
}
}email.clicked
{
"event": "email.clicked",
"timestamp": "2024-11-27T12:12:00.000Z",
"data": {
"emailLogId": "uuid",
"recipient": "user@example.com",
"subject": "Welcome!",
"clickedAt": "2024-11-27T12:12:00.000Z",
"url": "https://yourapp.com/get-started",
"userAgent": "Mozilla/5.0...",
"ipAddress": "192.168.1.1"
}
}API Reference
List Subscriptions
GET /api/v1/webhook-subscriptions
curl https://api.formamail.com/api/v1/webhook-subscriptions \
-H "Authorization: Bearer your-api-key"Create Subscription
POST /api/v1/webhook-subscriptions
{
"url": "https://yourapp.com/webhooks/formamail",
"events": ["email.delivered", "email.bounced"],
"description": "Production webhook",
"enabled": true
}Response includes the signing secret:
{
"id": "wh_xxxxx",
"url": "https://yourapp.com/webhooks/formamail",
"events": ["email.delivered", "email.bounced"],
"secret": "whsec_xxxxxxxxxxxxx",
"enabled": true,
"createdAt": "2024-11-27T12:00:00.000Z"
}Get Subscription
GET /api/v1/webhook-subscriptions/:id
Update Subscription
PATCH /api/v1/webhook-subscriptions/:id
{
"events": ["email.delivered", "email.bounced", "email.opened"],
"enabled": true
}Delete Subscription
DELETE /api/v1/webhook-subscriptions/:id
Send Test Webhook
POST /api/v1/webhook-subscriptions/:id/test
Sends a test email.delivered event to your endpoint.
Get Delivery History
GET /api/v1/webhook-subscriptions/:id/deliveries
View recent delivery attempts:
{
"deliveries": [
{
"id": "del_xxxxx",
"event": "email.delivered",
"status": "success",
"statusCode": 200,
"responseTime": 150,
"attemptedAt": "2024-11-27T12:00:00.000Z"
},
{
"id": "del_yyyyy",
"event": "email.bounced",
"status": "failed",
"statusCode": 500,
"error": "Internal Server Error",
"attemptedAt": "2024-11-27T11:55:00.000Z",
"nextRetryAt": "2024-11-27T11:56:00.000Z"
}
]
}Retry Failed Delivery
POST /api/v1/webhook-subscriptions/:id/deliveries/:deliveryId/retry
Manually retry a failed delivery.
Signature Verification
Algorithm
FormaMail uses HMAC-SHA256 for webhook signatures:
- Construct the signed payload:
{timestamp}.{json_body} - Compute HMAC-SHA256 with your webhook secret
- Compare with the signature header (after removing
v1=prefix)
Code Examples
const crypto = require('crypto');
function verifySignature(payload, signature, timestamp, secret) {
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return `v1=${expectedSig}` === signature;
}Timestamp Validation
Prevent replay attacks by validating the timestamp:
function isTimestampValid(timestamp, toleranceSeconds = 300) {
const now = Math.floor(Date.now() / 1000);
const eventTime = parseInt(timestamp, 10);
return Math.abs(now - eventTime) <= toleranceSeconds;
}Retry Policy
Failed deliveries are automatically retried with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 (final) | 30 minutes |
After 4 failed attempts, the delivery is marked as failed. You can manually retry from the delivery history.
What Counts as Failure
- Non-2xx response status
- Connection timeout (30 seconds)
- Connection refused
- SSL/TLS errors
Best Practices
Respond Quickly
Return 200 OK as fast as possible. Process events asynchronously:
app.post('/webhooks/formamail', express.json(), async (req, res) => {
// Verify signature first
if (!verifySignature(req.body, req.headers['x-formamail-signature'], req.headers['x-formamail-timestamp'], secret)) {
return res.status(401).send('Invalid signature');
}
// Acknowledge receipt immediately
res.status(200).send('OK');
// Process asynchronously
processWebhookAsync(req.body).catch(console.error);
});
async function processWebhookAsync(event) {
// Your actual processing logic
switch (event.event) {
case 'email.bounced':
await handleBounce(event.data);
break;
// ... other events
}
}Handle Duplicates
Webhooks may be delivered more than once. Use the X-FormaMail-Event-Id header for idempotency:
const processedEvents = new Set(); // Use Redis in production
app.post('/webhooks/formamail', async (req, res) => {
const eventId = req.headers['x-formamail-event-id'];
if (processedEvents.has(eventId)) {
return res.status(200).send('Already processed');
}
// Process event
// ...
processedEvents.add(eventId);
res.status(200).send('OK');
});Use HTTPS
Webhook URLs must use HTTPS. Self-signed certificates are not supported.
Rotate Secrets
If you suspect your webhook secret is compromised, create a new subscription and delete the old one.
Troubleshooting
Not Receiving Webhooks
- Check subscription is enabled: View subscription in dashboard
- Verify URL is accessible: Can FormaMail reach your endpoint?
- Check firewall rules: Allow incoming connections
- Review delivery history: Check for failed deliveries
Signature Verification Failing
- Use raw body: Don’t parse JSON before verification
- Check secret: Make sure you’re using the correct secret
- Check timestamp: Ensure your server clock is accurate
Slow Processing
- Respond quickly: Return 200 before processing
- Use queue: Process events asynchronously
- Batch updates: Don’t hit your database for every event
OAuth Authentication
Webhook subscriptions can also be created via OAuth-authenticated requests:
- Read: Requires
webhooks:readscope - Create/Update/Delete: Requires
webhooks:writescope
// Using OAuth access token
const response = await fetch('https://api.formamail.com/api/v1/webhook-subscriptions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${oauthAccessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://yourapp.com/webhooks/formamail',
events: ['email.delivered']
})
});