NotiGrid

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:

  1. An event occurs (e.g., email delivered)
  2. NotiGrid sends POST request to your webhook URL
  3. Your server processes the event
  4. 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

  1. Go to Settings → Webhooks
  2. Click Add Webhook
  3. Enter your endpoint URL
  4. Select events to receive
  5. Copy the webhook secret (for verification)
  6. 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', 200

Handling 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_xyz789

Best 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/notigrid

Test Events

Trigger test events from the dashboard:

  1. Go to Settings → Webhooks
  2. Click on your webhook
  3. Click Send Test Event
  4. Select event type
  5. Click Send

Webhook Logs

View webhook delivery logs in the dashboard:

  1. Go to Settings → Webhooks
  2. Click on your webhook
  3. View Recent Deliveries
  4. 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.89

Contact 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?