Gareth Wilson Gareth Wilson

Guide to Mailgun Webhooks: Features and Best Practices

Published


Mailgun has established itself as one of the most developer-friendly email service providers, powering transactional and marketing email for applications of all sizes. Beyond sending emails, Mailgun's webhook system enables real-time visibility into email delivery, engagement, and deliverability issues, making it essential for applications that need to respond instantly to email events.

This guide covers everything you need to know about Mailgun webhooks: their features, how to configure them, best practices for production deployments, and the common pain points developers face along with solutions to address them.

What are Mailgun webhooks?

Mailgun webhooks are HTTP callbacks that deliver real-time notifications to your application whenever events occur with your emails. Instead of polling the Events API repeatedly for updates, Mailgun pushes event data directly to your configured endpoints via POST requests. This enables instant notifications for deliveries, bounces, opens, clicks, complaints, and unsubscribes.

Webhooks allow you to programmatically handle events that happen with your messages sent through Mailgun. By configuring URLs in the Webhooks tab of the Control Panel, Mailgun can send HTTP/HTTPS POST requests to specified endpoints when certain events occur. Webhooks are configured at the domain level, enabling you to set unique endpoints for each domain.

Common use cases for Mailgun webhooks include:

  1. Bounce management - Automatically remove invalid addresses from your database when permanent failures occur
  2. Engagement tracking - Send open and click data to analytics tools to optimise email content and timing
  3. Complaint handling - Create support tickets or trigger workflows when users mark emails as spam
  4. Delivery confirmation - Update application state when critical transactional emails are delivered

Mailgun webhook features

FeatureDetails
Webhook configurationControl Panel UI or Webhooks API (v3/v4)
Payload formatJSON (new) or form-urlencoded (legacy)
Hashing algorithmHMAC-SHA256
TimeoutNot officially documented; respond quickly to avoid retries
Retry logic8-hour window: 10min, 10min, 15min, 30min, 1hr, 2hr, 4hr
Delivery webhook retryNot retried (exception to standard retry policy)
Manual retryNot available
Browsable logEvents visible in Control Panel Logs; webhook delivery status limited
Max URLs per event3 URLs per webhook type

Supported event types

Mailgun webhooks can notify you of the following email events:

EventDescription
acceptedMailgun accepted the message for delivery
deliveredEmail successfully delivered to recipient's mail server
openedRecipient opened the email (requires tracking enabled)
clickedRecipient clicked a link in the email (requires tracking enabled)
unsubscribedRecipient clicked the unsubscribe link
complainedRecipient marked the email as spam (via ISP feedback loop)
temporary_failSoft bounce; Mailgun will retry delivery
permanent_failHard bounce; Mailgun will not retry delivery

Open and click tracking must be enabled via the o:tracking, o:tracking-opens, or o:tracking-clicks parameters when sending messages.

Webhook payload structure

Mailgun's current webhook format (Webhooks 2.0) delivers a JSON payload with two main objects: signature for verification and event-data containing the event details.

{
  "signature": {
    "timestamp": "1529006854",
    "token": "a8ce0edb2dd8301dee6c2405235584e45aa91d1e9f979f3de0",
    "signature": "d2271d12299f6592d9d44cd9d250f0704e4674c30d79d07c47a66f95ce71cf55"
  },
  "event-data": {
    "event": "delivered",
    "timestamp": 1529006854.329574,
    "id": "DACSsAdVSeGpLid7TN03WA",
    "log-level": "info",
    "envelope": {
      "transport": "smtp",
      "sender": "sender@yourdomain.com",
      "sending-ip": "192.168.1.1",
      "targets": "recipient@example.com"
    },
    "flags": {
      "is-routed": false,
      "is-authenticated": true,
      "is-system-test": false,
      "is-test-mode": false
    },
    "delivery-status": {
      "tls": true,
      "mx-host": "smtp.example.com",
      "code": 250,
      "description": "",
      "session-seconds": 0.123,
      "utf8": true,
      "attempt-no": 1,
      "message": "OK"
    },
    "message": {
      "headers": {
        "to": "recipient@example.com",
        "message-id": "20180614123456.1.ABCDEF@yourdomain.com",
        "from": "sender@yourdomain.com",
        "subject": "Test Subject"
      },
      "attachments": [],
      "size": 1234
    },
    "recipient": "recipient@example.com",
    "recipient-domain": "example.com",
    "tags": ["transactional", "welcome"],
    "user-variables": {
      "user-id": "12345",
      "campaign": "onboarding"
    }
  }
}

Key payload fields

FieldDescription
signature.timestampUnix timestamp when the event was generated
signature.tokenRandomly generated 50-character string
signature.signatureHMAC-SHA256 signature for verification
event-data.eventEvent type (delivered, opened, clicked, etc.)
event-data.idUnique identifier for this specific event
event-data.timestampPrecise timestamp with milliseconds
event-data.recipientEmail address of the recipient
event-data.tagsArray of tags attached to the message
event-data.user-variablesCustom variables attached when sending
event-data.delivery-statusDelivery details including SMTP response code
event-data.message.headersEmail headers including Message-ID

Security with HMAC signatures

Mailgun signs each webhook request using HMAC-SHA256, allowing you to verify that requests genuinely originated from Mailgun. The signature is generated using your Webhook Signing Key (found in your Mailgun dashboard under Settings > API Keys).

Each webhook includes three signature parameters:

  • timestamp: Number of seconds since January 1, 1970 (Unix epoch)
  • token: A randomly generated 50-character string unique to each request
  • signature: The HMAC-SHA256 hexdigest of timestamp + token

To verify authenticity:

  1. Concatenate the timestamp and token values (no separator)
  2. Generate an HMAC-SHA256 hash using your Webhook Signing Key
  3. Compare the resulting hexdigest to the provided signature

Mailgun uses a dedicated Webhook Signing Key, separate from your API key. Find it in your dashboard under Settings > API Keys > Webhook Signing Key.

Custom variables and tags

Mailgun allows you to attach custom data to messages that will be included in webhook payloads:

Custom Variables: Attach key-value pairs using the v: prefix when sending:

curl -s --user 'api:YOUR_API_KEY' \
  https://api.mailgun.net/v3/YOUR_DOMAIN/messages \
  -F from='sender@yourdomain.com' \
  -F to='recipient@example.com' \
  -F subject='Hello' \
  -F text='Testing' \
  -F v:user-id='12345' \
  -F v:campaign='welcome'

Custom variables appear in the user-variables object in webhook payloads. Note: Variables exceeding 4KB will be truncated.

Tags: Categorise messages for analytics using the o:tag parameter:

curl -s --user 'api:YOUR_API_KEY' \
  https://api.mailgun.net/v3/YOUR_DOMAIN/messages \
  -F from='sender@yourdomain.com' \
  -F to='recipient@example.com' \
  -F subject='Hello' \
  -F text='Testing' \
  -F o:tag='transactional' \
  -F o:tag='welcome-series'

Tags appear in the tags array in webhook payloads and can be used for filtering in Mailgun's analytics.

Setting up Mailgun webhooks

Via the Mailgun Control Panel

  1. Log into your Mailgun dashboard at app.mailgun.com
  2. Navigate to Sending > Webhooks
  3. Select the domain you want to configure from the dropdown
  4. For each event type you want to track:
    • Click Add Webhook or Edit next to the event type
    • Enter your HTTPS endpoint URL
    • Click Save
  5. Use the Test Webhook button to verify connectivity

HTTPS endpoints must use a certificate signed by a trusted Certificate Authority. Self-signed certificates are not supported.

Via the Webhooks API

Create a webhook (v3 API):

curl -s --user 'api:YOUR_API_KEY' \
  https://api.mailgun.net/v3/domains/YOUR_DOMAIN/webhooks \
  -F id='delivered' \
  -F url='https://your-endpoint.com/webhooks/mailgun'

Create webhooks for multiple events (v4 API):

The v4 API allows associating one URL with multiple event types in a single request:

curl -s --user 'api:YOUR_API_KEY' \
  -X PUT \
  https://api.mailgun.net/v4/domains/YOUR_DOMAIN/webhooks \
  -F delivered='https://your-endpoint.com/webhooks/mailgun' \
  -F opened='https://your-endpoint.com/webhooks/mailgun' \
  -F clicked='https://your-endpoint.com/webhooks/mailgun'

Best practices when working with Mailgun webhooks

Verifying HMAC signatures

Always verify webhook signatures to ensure requests genuinely originated from Mailgun.

Process webhooks asynchronously

Mailgun recommends handling webhooks asynchronously to avoid timeouts and handle event spikes gracefully. If your endpoint takes too long to respond, Mailgun will retry the delivery, potentially causing duplicate processing.

Recommended pattern: Acknowledge immediately, then process in a background queue.

Use the event ID for idempotency

Each Mailgun event includes a unique id field. Use this to implement idempotent processing and prevent duplicate handling during retries.

Return appropriate HTTP status codes

Mailgun interprets response codes to determine retry behaviour:

Status CodeMailgun Behaviour
200 (Success)Delivery successful, no retry
406 (Not Acceptable)Request rejected, no retry
Any other codeRetry according to schedule

Return 406 when you want Mailgun to stop retrying (e.g., for events you intentionally don't process). Return 200 only after successfully acknowledging the webhook. Any 5xx error or timeout will trigger retries.

Cache tokens to prevent replay attacks

Beyond timestamp checking, you can cache the unique token from each request to prevent replay attacks.

Mailgun webhook limitations and pain points

Limited control over retry schedule

The Problem: Mailgun's retry schedule (10min, 10min, 15min, 30min, 1hr, 2hr, 4hr over 8 hours) is fixed and cannot be configured. For applications needing faster retries or different intervals, this inflexibility can cause issues.

Why It Happens: The retry policy is set at the Mailgun infrastructure level and isn't exposed for per-account or per-webhook customisation.

Workarounds:

  • Ensure your endpoint has high availability to minimise the need for retries
  • Implement your own queuing system to buffer events during outages
  • Use the Events API as a fallback to poll for missed events

How Hookdeck Can Help: Hookdeck provides fully configurable retry policies with customisable intervals, exponential backoff, and maximum retry limits, giving you complete control over delivery behaviour.

No manual retry capability

The Problem: Mailgun doesn't offer a way to manually trigger webhook retries. If events fail after exhausting automatic retries, there's no built-in mechanism to replay them.

Why It Happens: Mailgun's webhook system is designed for automatic delivery without manual intervention capabilities in the dashboard or API.

Workarounds:

  • Poll the Events API to retrieve events that may have been missed
  • Implement event logging on your receiving endpoint to track what was successfully processed
  • Build a reconciliation process that compares Events API data with your processed events

How Hookdeck Can Help: Hookdeck preserves all webhook deliveries in a browsable log, allowing you to inspect, debug, and manually replay any failed webhook with a single click.

Delivered webhook not retried

The Problem: Unlike other event types, the delivered webhook is explicitly excluded from Mailgun's retry logic. If your endpoint is down when a delivery event occurs, you will not receive that notification.

Why It Happens: This is documented behaviour, though the reasoning isn't explicitly stated. It may be due to the high volume of delivery events compared to other event types.

Workarounds:

  • Ensure extremely high availability for endpoints receiving delivery webhooks
  • Regularly poll the Events API to reconcile delivery events
  • Implement monitoring to detect gaps in expected delivery notifications

How Hookdeck Can Help: Hookdeck applies consistent retry behaviour across all event types, ensuring delivery events receive the same reliability guarantees as other webhooks.

Legacy and new webhook format complexity

The Problem: Mailgun supports two different webhook payload formats (Legacy and Webhooks 2.0), with different content types and structures. Applications may need to handle both formats during migration or when integrating with older configurations.

Why It Happens: Webhooks 2.0 was introduced in 2018 to improve the payload structure, but legacy webhooks remain supported for backward compatibility.

Workarounds:

  • Check the Content-Type header to determine the format
  • Implement parsers for both JSON and form-urlencoded payloads
  • Migrate all webhooks to the new format when possible

How Hookdeck Can Help: Hookdeck's transformation capabilities can normalise incoming payloads to a consistent format, eliminating the need to handle multiple structures in your application code.

Limited visibility into webhook delivery status

The Problem: Mailgun's dashboard shows email events but provides minimal visibility into webhook delivery status. You can't easily see which webhooks failed, when they were retried, or why they failed.

Why It Happens: Mailgun's logging focuses on email delivery rather than webhook infrastructure health.

Workarounds:

  • Implement comprehensive logging on your webhook endpoint
  • Monitor for gaps in expected event sequences
  • Set up external monitoring for your webhook endpoints

How Hookdeck Can Help: Hookdeck provides a complete dashboard showing all webhook deliveries, including status codes, response times, retry attempts, and full request/response bodies for debugging.

No built-in dead letter queue

The Problem: Webhooks that fail after exhausting all retries are permanently lost. Mailgun doesn't maintain a queue of failed deliveries for later inspection or replay.

Why It Happens: Mailgun's architecture prioritises email delivery infrastructure over webhook delivery persistence.

Workarounds:

  • Build your own dead letter queue by logging all received webhooks
  • Use the Events API to periodically fetch and reconcile events
  • Implement alerting when your endpoint experiences extended downtime

How Hookdeck Can Help: Hookdeck automatically preserves failed webhooks in a dead letter queue, allowing you to inspect issues, fix problems, and replay events once your endpoint is healthy.

Custom variables 4KB limit

The Problem: Custom variables (user-variables) attached to messages are truncated if they exceed 4KB in webhook payloads. Large metadata objects may arrive incomplete.

Why It Happens: This limit prevents excessively large webhook payloads and is a documented constraint.

Workarounds:

  • Store large data externally and include only reference IDs in custom variables
  • Compress data before attaching as variables
  • Use multiple smaller variables instead of one large object

How Hookdeck Can Help: Hookdeck can enrich webhook payloads using transformations, fetching additional data from your systems based on the reference IDs in the original payload.

Inconsistent retry schedule documentation

The Problem: Mailgun's documentation shows different retry schedules in different places (5min vs 10min for initial retry), creating confusion about actual behaviour.

Why It Happens: Documentation may not have been updated consistently across all sources.

Workarounds:

  • Test retry behaviour empirically with your own endpoints
  • Design your system to handle any reasonable retry interval
  • Don't rely on specific timing for business logic

How Hookdeck Can Help: Hookdeck provides explicit, configurable retry policies with clear documentation, removing ambiguity about delivery behaviour.

Testing Mailgun webhooks

Use bin.mailgun.net for initial testing

Mailgun provides a free webhook testing service at bin.mailgun.net. Create a temporary endpoint to inspect webhook payloads before building your handler:

  1. Visit bin.mailgun.net and create a new bin
  2. Copy the bin URL
  3. Configure it as a webhook endpoint in your Mailgun dashboard
  4. Send test emails to trigger events
  5. Inspect the received payloads in the bin interface

Use a request inspector in development

For local development, use tools like Hookdeck's CLI to expose your local endpoint.

Send test emails to trigger real events

Create a simple test flow to generate authentic webhook events:

const mailgun = require('mailgun-js')({
  apiKey: process.env.MAILGUN_API_KEY,
  domain: process.env.MAILGUN_DOMAIN
});

// Send test email to trigger webhooks
async function sendTestEmail() {
  const data = {
    from: 'test@yourdomain.com',
    to: 'real-email@example.com', // Use a real email you control
    subject: 'Webhook Test',
    text: 'Testing webhook delivery',
    'o:tag': ['test'],
    'v:test-id': 'webhook-test-001'
  };

  const result = await mailgun.messages().send(data);
  console.log('Test email sent:', result.id);
}

Validate signature verification

Test that your signature verification correctly rejects invalid requests:

describe('Mailgun webhook verification', () => {
  it('rejects requests with invalid signatures', async () => {
    const response = await request(app)
      .post('/webhooks/mailgun')
      .send({
        signature: {
          timestamp: '1234567890',
          token: 'invalid-token',
          signature: 'invalid-signature'
        },
        'event-data': { event: 'delivered' }
      });

    expect(response.status).toBe(401);
  });

  it('accepts requests with valid signatures', async () => {
    const timestamp = Math.floor(Date.now() / 1000).toString();
    const token = 'test-token-12345';
    const signature = crypto
      .createHmac('sha256', process.env.MAILGUN_WEBHOOK_SIGNING_KEY)
      .update(timestamp + token)
      .digest('hex');

    const response = await request(app)
      .post('/webhooks/mailgun')
      .send({
        signature: { timestamp, token, signature },
        'event-data': { event: 'delivered', id: 'test-123' }
      });

    expect(response.status).toBe(200);
  });
});

Conclusion

Mailgun webhooks provide a robust foundation for building real-time email tracking and automation into your applications. The JSON payload structure with custom variables and tags enables rich event context, while HMAC signature verification ensures security.

However, production deployments must account for Mailgun's limitations: the fixed retry schedule, lack of manual replay capabilities, and the notable exception that delivery webhooks aren't retried. Implementing proper signature verification, asynchronous processing, and idempotent handlers addresses most reliability concerns.

For applications with moderate webhook volumes and reliable endpoints, Mailgun's built-in webhook system combined with proper error handling works well. For high-volume email operations, mission-critical delivery tracking, or scenarios requiring configurable retries and delivery visibility, webhook infrastructure like Hookdeck can address Mailgun's limitations by providing customisable retry policies, automatic dead letter queuing, payload transformations, and comprehensive delivery monitoring without modifying your Mailgun configuration.

Additional resources


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.