IntegrationsWebhook Subscriptions

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

EventDescription
email.sentEmail successfully queued for delivery
email.deliveredEmail delivered to recipient’s mailbox
email.bouncedEmail bounced (includes bounce type)
email.complainedRecipient marked email as spam
email.openedRecipient opened the email (if tracking enabled)
email.clickedRecipient 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

HeaderDescriptionExample
X-FormaMail-SignatureHMAC-SHA256 signaturev1=abc123...
X-FormaMail-TimestampUnix timestamp1732723200
X-FormaMail-Event-IdUnique event identifierevt_xxx
X-FormaMail-Event-TypeEvent typeemail.delivered
Content-TypeAlways JSONapplication/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:

  1. Construct the signed payload: {timestamp}.{json_body}
  2. Compute HMAC-SHA256 with your webhook secret
  3. 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:

AttemptDelay
1Immediate
21 minute
35 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

  1. Check subscription is enabled: View subscription in dashboard
  2. Verify URL is accessible: Can FormaMail reach your endpoint?
  3. Check firewall rules: Allow incoming connections
  4. Review delivery history: Check for failed deliveries

Signature Verification Failing

  1. Use raw body: Don’t parse JSON before verification
  2. Check secret: Make sure you’re using the correct secret
  3. Check timestamp: Ensure your server clock is accurate

Slow Processing

  1. Respond quickly: Return 200 before processing
  2. Use queue: Process events asynchronously
  3. 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:read scope
  • Create/Update/Delete: Requires webhooks:write scope
// 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']
  })
});