Loading...
Loading...
Receive real-time notifications when email events occur. Configure webhooks in your dashboard settings to get notified about deliveries, opens, clicks and more.
Go to Settings → Webhooks
Navigate to the webhooks tab in your dashboard settings
Create a new webhook endpoint
Enter your endpoint URL and select the events you want to receive
Copy your signing secret
Save the secret securely - it's only shown once and is used to verify webhook signatures
Test your endpoint
Use the "Test" button to send a test event to your endpoint
email.sentEmail was sent to the delivery provider
email.deliveredEmail was delivered to recipient's inbox
email.openedRecipient opened the email
email.clickedRecipient clicked a link in the email
email.bouncedEmail bounced (hard or soft)
email.complainedRecipient marked email as spam
contact.createdNew contact was added
contact.updatedContact was modified
contact.unsubscribedContact unsubscribed from emails
All webhook payloads follow this structure:
{
"id": "evt_abc123def456",
"type": "email.delivered",
"created": "2024-01-15T10:30:00.000Z",
"data": {
"emailId": "em_xyz789",
"to": "user@example.com",
"subject": "Welcome to our app!",
"timestamp": "2024-01-15T10:30:00.000Z",
// Additional event-specific data
}
}// email.opened
{
"emailId": "em_xyz789",
"to": "user@example.com",
"subject": "Welcome!",
"userAgent": "Mozilla/5.0...",
"ipAddress": "192.168.1.1",
"timestamp": "2024-01-15T10:35:00.000Z"
}// email.clicked
{
"emailId": "em_xyz789",
"to": "user@example.com",
"url": "https://example.com/signup",
"timestamp": "2024-01-15T10:36:00.000Z"
}// email.bounced
{
"emailId": "em_xyz789",
"to": "invalid@example.com",
"bounceType": "hard",
"bounceReason": "Mailbox does not exist",
"timestamp": "2024-01-15T10:30:05.000Z"
}Every webhook request includes a signature header to verify authenticity. Always verify signatures in production to prevent webhook spoofing.
| X-Notifyn-Signature | t=timestamp,v1=signature |
| X-Notifyn-Event | The event type (e.g., email.delivered) |
| X-Notifyn-Delivery-ID | Unique ID for this delivery attempt |
const crypto = require('crypto');function verifyWebhookSignature(payload, signature, secret) {
// Parse the signature header
const parts = signature.split(',').reduce((acc, part) => {
const [key, value] = part.split('=');
acc[key] = value;
return acc;
}, {}); const timestamp = parts['t'];
const expectedSignature = parts['v1']; // Check timestamp is within 5 minutes
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
throw new Error('Timestamp too old');
} // Compute expected signature
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const computedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex'); // Compare signatures
if (computedSignature !== expectedSignature) {
throw new Error('Invalid signature');
} return true;
}// Express.js middleware
app.post('/webhooks', express.json(), (req, res) => {
const signature = req.headers['x-notifyn-signature'];
try {
verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);
// Process the webhook
const { type, data } = req.body;
console.log(`Received ${type} event`, data);
res.status(200).send('OK');
} catch (error) {
console.error('Webhook verification failed:', error);
res.status(401).send('Invalid signature');
}
}); import hmac
import hashlib
import json
import timedef verify_webhook_signature(payload, signature, secret):
parts = dict(p.split('=') for p in signature.split(','))
timestamp = parts['t']
expected_sig = parts['v1']
# Check timestamp
if abs(time.time() - int(timestamp)) > 300:
raise ValueError('Timestamp too old')
# Compute signature
signed_payload = f"{timestamp}.{json.dumps(payload)}"
computed_sig = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(computed_sig, expected_sig):
raise ValueError('Invalid signature')
return TrueIf your endpoint returns a non-2xx status or times out, we'll retry with exponential backoff:
After all retries are exhausted, the webhook is marked as failed. You can view failed deliveries in your dashboard.