Webhooks
Receive real-time delivery status updates via webhooks
Webhooks
Webhooks allow you to receive real-time HTTP callbacks when events occur in your NotiGrid account, such as notification delivery, bounces, or failures.
Overview
Instead of polling the API for updates, webhooks push events to your server as they happen:
- An event occurs (e.g., email delivered)
- NotiGrid sends POST request to your webhook URL
- Your server processes the event
- Your server responds with 200 OK
Setup
1. Create an Endpoint
Create an HTTPS endpoint on your server to receive webhook events:
// Node.js/Express example
app.post('/webhooks/notigrid', (req, res) => {
const event = req.body;
console.log('Received event:', event.type);
console.log('Event data:', event.data);
// Process the event
handleEvent(event);
// Respond with 200
res.status(200).send('Webhook received');
});2. Configure Webhook in Dashboard
- Go to Settings → Webhooks
- Click Add Webhook
- Enter your endpoint URL
- Select events to receive
- Copy the webhook secret (for verification)
- Click Save
3. Test Your Webhook
Click Send Test Event to verify your endpoint is working.
Event Types
Notification Events
notification.sent - Notification was sent to the channel provider
{
"type": "notification.sent",
"id": "evt_1234567890",
"created": "2025-01-16T10:30:00Z",
"data": {
"notification_id": "notif_abc123",
"channel": "email",
"to": "user@example.com",
"template": "welcome-email",
"status": "sent"
}
}notification.delivered - Notification successfully delivered
{
"type": "notification.delivered",
"id": "evt_1234567891",
"created": "2025-01-16T10:30:05Z",
"data": {
"notification_id": "notif_abc123",
"channel": "email",
"to": "user@example.com",
"delivered_at": "2025-01-16T10:30:05Z"
}
}notification.failed - Notification failed to send
{
"type": "notification.failed",
"id": "evt_1234567892",
"created": "2025-01-16T10:30:03Z",
"data": {
"notification_id": "notif_abc123",
"channel": "email",
"to": "invalid@domain",
"error": {
"code": "invalid_email",
"message": "Email address is invalid"
}
}
}notification.bounced - Email bounced
{
"type": "notification.bounced",
"id": "evt_1234567893",
"created": "2025-01-16T10:30:10Z",
"data": {
"notification_id": "notif_abc123",
"channel": "email",
"to": "user@example.com",
"bounce_type": "hard",
"bounce_reason": "mailbox_not_found"
}
}notification.opened - Email opened (requires tracking)
{
"type": "notification.opened",
"id": "evt_1234567894",
"created": "2025-01-16T11:00:00Z",
"data": {
"notification_id": "notif_abc123",
"opened_at": "2025-01-16T11:00:00Z",
"user_agent": "Mozilla/5.0...",
"ip_address": "192.0.2.1"
}
}notification.clicked - Link clicked in email
{
"type": "notification.clicked",
"id": "evt_1234567895",
"created": "2025-01-16T11:05:00Z",
"data": {
"notification_id": "notif_abc123",
"link_url": "https://example.com/product",
"clicked_at": "2025-01-16T11:05:00Z"
}
}Subscription Events
subscription.created - New push notification subscription
subscription.deleted - Push subscription removed
Template Events
template.created - New template created
template.updated - Template modified
template.deleted - Template removed
Webhook Signatures
All webhooks include a signature header to verify authenticity:
Verifying Signatures
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(payload).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(digest)
);
}
app.post('/webhooks/notigrid', (req, res) => {
const signature = req.headers['x-notigrid-signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhook(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
res.status(200).send('OK');
});Python Example
import hmac
import hashlib
def verify_webhook(payload, signature, secret):
digest = hmac.new(
secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, digest)
@app.route('/webhooks/notigrid', methods=['POST'])
def webhook():
signature = request.headers.get('X-Notigrid-Signature')
payload = request.get_data(as_text=True)
if not verify_webhook(payload, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.get_json()
# Process event
return 'OK', 200Handling Events
Basic Event Handler
function handleEvent(event) {
switch (event.type) {
case 'notification.delivered':
handleDelivered(event.data);
break;
case 'notification.failed':
handleFailed(event.data);
break;
case 'notification.bounced':
handleBounced(event.data);
break;
default:
console.log('Unhandled event type:', event.type);
}
}
function handleDelivered(data) {
// Update database
db.updateNotification(data.notification_id, {
status: 'delivered',
delivered_at: data.delivered_at
});
}
function handleFailed(data) {
// Log error and notify admin
logger.error('Notification failed:', data.error);
sendAdminAlert(data);
}
function handleBounced(data) {
// Remove email from mailing list if hard bounce
if (data.bounce_type === 'hard') {
db.markEmailInvalid(data.to);
}
}Idempotency
Webhooks may be delivered multiple times. Use the event ID to ensure idempotent processing:
const processedEvents = new Set();
function handleEvent(event) {
if (processedEvents.has(event.id)) {
console.log('Event already processed:', event.id);
return;
}
// Process event
processEvent(event);
// Mark as processed
processedEvents.add(event.id);
// Or store in database for persistence
db.markEventProcessed(event.id);
}Retry Logic
If your endpoint returns a non-2xx status code or times out, NotiGrid will retry:
- 1st retry: After 1 minute
- 2nd retry: After 5 minutes
- 3rd retry: After 15 minutes
- 4th retry: After 1 hour
- 5th retry: After 6 hours
After 5 failed attempts, the webhook is marked as failed and no further retries occur.
Retry Headers
Retry attempts include headers:
X-Notigrid-Delivery-Attempt: 3
X-Notigrid-Delivery-ID: dlv_xyz789Best Practices
1. Respond Quickly
Return 200 OK immediately, then process asynchronously:
app.post('/webhooks/notigrid', async (req, res) => {
// Verify signature
if (!verifyWebhook(req.body, req.headers['x-notigrid-signature'], secret)) {
return res.status(401).send('Invalid signature');
}
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
processWebhook(req.body).catch(console.error);
});2. Handle Duplicates
Use event IDs to detect and skip duplicate events.
3. Secure Your Endpoint
- Use HTTPS only
- Verify webhook signatures
- Rate limit requests
- Restrict access to NotiGrid IPs (enterprise)
4. Monitor Failures
Set up alerts for:
- High failure rates
- Signature verification failures
- Processing errors
5. Log Events
Keep a record of all webhook events for debugging:
function handleEvent(event) {
// Log to file or database
logger.info('Webhook received', {
event_id: event.id,
event_type: event.type,
timestamp: event.created
});
// Process event
processEvent(event);
}Testing
Local Development
Use ngrok or similar tools to expose your local server:
# Start ngrok
ngrok http 3000
# Use the HTTPS URL in webhook settings
https://abc123.ngrok.io/webhooks/notigridTest Events
Trigger test events from the dashboard:
- Go to Settings → Webhooks
- Click on your webhook
- Click Send Test Event
- Select event type
- Click Send
Webhook Logs
View webhook delivery logs in the dashboard:
- Go to Settings → Webhooks
- Click on your webhook
- View Recent Deliveries
- See status, response code, and retry attempts
Troubleshooting
Webhook Not Receiving Events
Check:
- Endpoint is publicly accessible
- Using HTTPS (not HTTP)
- Firewall allows NotiGrid IPs
- Endpoint returns 200 OK quickly
Signature Verification Failing
Check:
- Using correct webhook secret
- Not modifying payload before verification
- Comparing raw payload (not parsed JSON)
- Using correct hash algorithm (SHA-256)
High Failure Rate
Solutions:
- Increase timeout on your server
- Return 200 OK immediately
- Process events asynchronously
- Check server logs for errors
Example: Email Bounce Handler
Complete example of handling bounced emails:
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
function verifySignature(payload, signature) {
const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
const digest = hmac.update(JSON.stringify(payload)).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
app.post('/webhooks/notigrid', async (req, res) => {
const signature = req.headers['x-notigrid-signature'];
// Verify signature
if (!verifySignature(req.body, signature)) {
return res.status(401).send('Invalid signature');
}
// Respond immediately
res.status(200).send('OK');
// Process asynchronously
const event = req.body;
if (event.type === 'notification.bounced') {
const { to, bounce_type, bounce_reason } = event.data;
if (bounce_type === 'hard') {
// Hard bounce - email is invalid
await db.users.updateOne(
{ email: to },
{
$set: {
email_valid: false,
bounced_at: new Date(),
bounce_reason
}
}
);
// Remove from mailing list
await db.mailingList.deleteOne({ email: to });
console.log(`Removed hard-bounced email: ${to}`);
} else {
// Soft bounce - temporary issue
await db.users.updateOne(
{ email: to },
{
$inc: { soft_bounce_count: 1 },
$set: { last_bounce: new Date() }
}
);
}
}
});
app.listen(3000);Security
IP Whitelisting (Enterprise)
Restrict webhook requests to NotiGrid IP addresses:
35.123.45.67
52.234.56.78
18.345.67.89Contact support for the complete list.
Rate Limiting
Implement rate limiting to prevent abuse:
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 100 // Max 100 requests per minute
});
app.post('/webhooks/notigrid', webhookLimiter, handleWebhook);Support
Need help with webhooks?
- Documentation: docs.notigrid.com/webhooks
- Support: support@notigrid.com
- Community: community.notigrid.com