Docs/Webhooks/Testing Webhooks

Testing Webhooks

Webhook testing requires a publicly accessible HTTPS endpoint. Here are the fastest ways to get one for local development, plus how to use the dashboard to re-test without triggering real sends.

Local Testing with ngrok

ngrok creates a secure tunnel from a public HTTPS URL to your local machine. It's the fastest way to receive webhook events during development without deploying anything.

bash
# Install ngrok (if not already installed)
brew install ngrok/ngrok/ngrok   # macOS
# or download from https://ngrok.com/download

# Expose your local server running on port 3000
ngrok http 3000

# Output will show something like:
# Forwarding  https://a1b2c3d4.ngrok.io -> http://localhost:3000

# Copy the https://... URL and paste it as your webhook URL in EmailSendX:
# Settings → Webhooks → Add Webhook → URL: https://a1b2c3d4.ngrok.io/webhooks/emailsendx

The ngrok terminal will show every incoming request in real time, including headers and payload, which is useful for debugging.

ngrok URL changes on restart

The free tier of ngrok generates a new URL each time you restart it. You'll need to update your webhook URL in EmailSendX each time. ngrok Pro provides stable custom subdomains.

Webhook Testing Tools

These tools give you an instant public endpoint that logs all incoming requests, with no setup required:

webhook.site

Free. Open the site and you immediately get a unique URL. Paste it into EmailSendX as your webhook endpoint. All requests are logged with full headers, body, and timestamps. Best for quick one-off inspection.

RequestBin

Free with account. Similar to webhook.site with a cleaner UI. Shows formatted JSON payloads. Supports real-time streaming mode.

Pipedream Inspector

Free tier available. Not only logs requests but lets you write serverless functions to process them. Good for testing actual processing logic before building your own handler.

Dashboard Resend

Once you have a real webhook configured, you can re-trigger past webhook deliveries from the dashboard without needing to send a real campaign:

  1. Go to Settings → Webhooks and click your webhook endpoint.
  2. In the Delivery Log tab, find a past delivery.
  3. Click the Resend button next to any delivery.
  4. EmailSendX re-POSTs the original payload to your endpoint immediately.

This is invaluable for debugging handler errors — fix your code, redeploy, then resend the same payload without touching campaigns or contacts.

Resend is idempotent for you to handle

Resending a delivery sends the exact same payload (including original timestamps). Your handler should be idempotent to avoid double-processing re-sent events. See the idempotency section in the security guide.

Example Webhook Handlers

A minimal but production-quality webhook handler that validates the request and routes to event-specific handlers:

const express = require('express');
const app = express();

// Parse JSON bodies
app.use(express.json());

app.post('/webhooks/emailsendx', (req, res) => {
  // 1. Verify secret token
  const secret = req.query.secret;
  if (secret !== process.env.WEBHOOK_SECRET) {
    return res.status(401).json({ error: 'Invalid secret' });
  }

  // 2. Verify User-Agent
  const userAgent = req.headers['user-agent'] || '';
  if (!userAgent.includes('EmailSendX')) {
    return res.status(401).json({ error: 'Invalid User-Agent' });
  }

  // 3. Verify workspaceId
  const { event, workspaceId, timestamp, data } = req.body;
  if (workspaceId !== process.env.EMAILSENDX_WORKSPACE_ID) {
    return res.status(401).json({ error: 'Unknown workspace' });
  }

  // 4. Respond immediately (process async if needed)
  res.json({ received: true });

  // 5. Handle event asynchronously
  setImmediate(() => {
    try {
      switch (event) {
        case 'email.delivered':
          console.log(`Delivered to ${data.contactEmail}`);
          break;
        case 'email.opened':
          console.log(`Opened by ${data.contactEmail}`);
          break;
        case 'email.clicked':
          console.log(`Clicked ${data.url} by ${data.contactEmail}`);
          break;
        case 'email.bounced':
          console.log(`Bounce (${data.bounceType}): ${data.contactEmail}`);
          // Update your CRM, remove from billing lists, etc.
          break;
        case 'email.complained':
          console.log(`Complaint from ${data.contactEmail}`);
          // Alert your support team
          break;
        case 'email.unsubscribed':
          console.log(`Unsubscribed: ${data.contactEmail} via ${data.method}`);
          break;
        case 'contact.created':
          console.log(`New contact: ${data.contactEmail} (source: ${data.source})`);
          break;
        case 'campaign.sent':
          console.log(`Campaign sent: ${data.campaignName} to ${data.totalRecipients} recipients`);
          break;
        default:
          console.log('Unknown event:', event);
      }
    } catch (err) {
      console.error('Webhook processing error:', err);
    }
  });
});

app.listen(3000, () => {
  console.log('Webhook handler listening on port 3000');
});

Handler ready?

Register your endpoint in Settings → Webhooks and subscribe to the events you care about.