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.
# 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/emailsendxThe ngrok terminal will show every incoming request in real time, including headers and payload, which is useful for debugging.
ngrok URL changes on restart
Webhook Testing Tools
These tools give you an instant public endpoint that logs all incoming requests, with no setup required:
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.
Free with account. Similar to webhook.site with a cleaner UI. Shows formatted JSON payloads. Supports real-time streaming mode.
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:
- Go to Settings → Webhooks and click your webhook endpoint.
- In the Delivery Log tab, find a past delivery.
- Click the Resend button next to any delivery.
- 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
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.