Docs/Webhooks/Security

Webhook Security

A public webhook endpoint can receive requests from anyone on the internet. Take these steps to verify that incoming webhook deliveries genuinely originate from EmailSendX.

Always validate incoming webhooks

Anyone who discovers your webhook URL can POST arbitrary payloads to it. At minimum, verify the workspaceId in the payload matches your workspace. Without validation, a malicious actor could fake events in your system.

Security Overview

EmailSendX provides several mechanisms to help you verify webhook authenticity:

  • HTTPS enforcement — all webhook URLs must use HTTPS
  • User-Agent verification — requests include a recognizable User-Agent header
  • workspaceId verification — verify the payload workspace matches yours
  • Secret token — add a secret token to your webhook URL and verify it in your handler
  • IP allowlisting — optionally restrict to known EmailSendX server IPs

HTTPS Requirement

EmailSendX only delivers webhooks to HTTPS endpoints. When you configure a webhook in the dashboard, plain HTTP URLs are rejected at save time. This ensures webhook payloads are always encrypted in transit.

Your SSL/TLS certificate must be valid. Self-signed certificates will cause delivery failures. Use a certificate from a trusted CA (Let's Encrypt is free and widely supported).

Validating Incoming Requests

Use a layered approach to verify that webhook requests are legitimate:

1. Verify the User-Agent header

All EmailSendX webhook requests include a User-Agent header containing EmailSendX. Reject requests that don't include it.

bash
User-Agent: EmailSendX-Webhook/1.0

2. Verify workspaceId in the payload

Every webhook payload includes a workspaceId field. Verify that it matches your workspace ID before processing the event.

js
// In your webhook handler:
const payload = JSON.parse(req.body);

if (payload.workspaceId !== process.env.EMAILSENDX_WORKSPACE_ID) {
  return res.status(401).json({ error: 'Unknown workspace' });
}
// Continue processing...

3. Add a secret token to your webhook URL

For stronger security, add a secret token as a query parameter when registering your webhook URL:

bash
# Register this URL in EmailSendX:
https://api.yourapp.com/webhooks/emailsendx?secret=your_random_secret_here

Then verify the token in your handler:

js
// Express.js example
app.post('/webhooks/emailsendx', (req, res) => {
  const secret = req.query.secret;

  if (secret !== process.env.WEBHOOK_SECRET) {
    return res.status(401).json({ error: 'Invalid secret' });
  }

  const { event, workspaceId, data } = req.body;

  if (workspaceId !== process.env.EMAILSENDX_WORKSPACE_ID) {
    return res.status(401).json({ error: 'Unknown workspace' });
  }

  // Process the event...
  console.log('Received event:', event, data);

  res.json({ received: true });
});

IP Allowlisting

If your infrastructure supports IP-level filtering (e.g., firewall rules, nginx allow directives), you can restrict your webhook endpoint to only accept requests from EmailSendX's server IPs.

Current EmailSendX outbound IPs for webhook delivery are listed in your workspace Settings → Webhooks → Delivery Info. Check the dashboard for the current list — IPs may change with infrastructure updates, so rely on the dashboard rather than hardcoding them.

Keep your IP allowlist current

EmailSendX may add or change outbound IPs over time. If you hardcode IPs, monitor the Delivery Info section for changes and update your allowlist to avoid missed deliveries.

Idempotency

In rare cases, a webhook event may be delivered more than once — for example, if your server returned a 5xx during the first delivery attempt and EmailSendX retried, but the first attempt was actually processed successfully.

To handle duplicates safely, make your webhook handler idempotent:

  • Use a combination of event type + data.emailId (or data.contactId) as a unique key.
  • Check your database before processing: if you've already handled this event, return 200 without re-processing.
  • Use a database unique constraint or Redis SET NX to prevent double-processing.
js
// Idempotency example
app.post('/webhooks/emailsendx', async (req, res) => {
  const { event, data } = req.body;

  // Build an idempotency key from event type + resource ID
  const idempotencyKey = `${event}:${data.emailId || data.contactId || data.campaignId}`;

  // Check if already processed (e.g., in Redis or DB)
  const alreadyProcessed = await redis.get(idempotencyKey);
  if (alreadyProcessed) {
    return res.json({ received: true, duplicate: true });
  }

  // Process the event
  await processWebhookEvent(event, data);

  // Mark as processed (with TTL to clean up old keys)
  await redis.setex(idempotencyKey, 86400, '1'); // expire after 24h

  res.json({ received: true });
});

Ready to test your webhook handler?

Use ngrok or webhook.site to test your endpoint locally before going live.