Docs/Webhooks

Webhooks

Webhooks send real-time HTTP POST notifications to your server when email events occur. All payloads are signed with HMAC-SHA256 for verification.

Endpoints

GET/api/webhooks
POST/api/webhooks
DELETE/api/webhooks/:id

Create a Webhook

ParameterTypeRequiredDescription
urlstringRequiredHTTPS endpoint URL to receive events
eventsstring[]RequiredArray of event types to subscribe to
Create webhook
curl -X POST https://fwd.sarthak.online/api/webhooks \
  -H "Cookie: your-session-cookie" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourapp.com/webhooks/fwd",
    "events": ["email.delivered", "email.opened", "email.bounced"]
  }'

The response includes a secret (prefixed with whsec_) used to verify webhook signatures.

Event Types

EventDescription
email.sentEmail accepted and queued for delivery
email.deliveredEmail successfully delivered to the recipient's mail server
email.openedRecipient opened the email (via tracking pixel)
email.clickedRecipient clicked a tracked link in the email
email.bouncedEmail bounced — address added to suppression list
email.complainedRecipient marked the email as spam
email.unsubscribedRecipient clicked the unsubscribe link

Webhook Payload

Each webhook delivery includes these headers and a JSON body:

HeaderDescription
X-Fwd-EventThe event type (e.g., email.delivered)
X-Fwd-SignatureHMAC-SHA256 signature for verification
Content-Typeapplication/json
Example payload
{
  "emailId": "email_abc123",
  "to": "user@example.com",
  "timestamp": "2025-01-15T12:00:00.000Z"
}

Signature Verification

The X-Fwd-Signature header contains a timestamp and HMAC-SHA256 hash in the format: t={timestamp},v1={hash}. Verify it to ensure the webhook is authentic.

Node.js verification example
const crypto = require('crypto');

function verifyWebhookSignature(req, secret) {
  const signature = req.headers['x-fwd-signature'];
  const [tPart, vPart] = signature.split(',');
  const timestamp = tPart.replace('t=', '');
  const receivedHash = vPart.replace('v1=', '');

  const payload = `${timestamp}.${JSON.stringify(req.body)}`;
  const expectedHash = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  if (expectedHash !== receivedHash) {
    throw new Error('Invalid webhook signature');
  }

  // Optional: reject old webhooks (>5 min)
  const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
  if (age > 300) {
    throw new Error('Webhook timestamp too old');
  }

  return true;
}

Limits

Maximum 5 webhooks per account on the Free plan, 10 on Pro. Localhost URLs are blocked in production for security.