BlogMailgun Webhooks

How to Test Mailgun Webhooks

Mailgun webhooks report delivery and engagement events so you can monitor email deliverability, spam complaints, and subscriber actions.

Looking for the broader picture? See the 7 best webhook testing tools (2026), or if you're already on Webhook.site, the 60-second migration to HookRay.

Mailgun Official Webhook Docs

1. Mailgun Webhook Events

Mailgun can send the following webhook events to your endpoint:

delivered
opened
clicked
permanent_fail
temporary_fail
complained
unsubscribed

2. Set Up a Test Endpoint with HookRay

Follow these steps to start receiving Mailgun webhooks for testing:

  1. Go to HookRay and click "Start Testing — Free" to get your unique webhook URL.
  2. Copy the URL (e.g., https://h.hookray.com/abc123).
  3. In your Mailgun dashboard, navigate to the webhook settings and paste the HookRay URL as your endpoint.
  4. Select the events you want to receive (see list above).
  5. Trigger a test event — HookRay will show the incoming webhook in real-time.

3. Sample Mailgun Webhook Payload

Here's an example of what a Mailgun webhook payload looks like:

payload.json
{
  "signature": {
    "timestamp": "1763583600",
    "token": "77b1d2a3c4",
    "signature": "9ad98b88077c12af0abc12345def6789"
  },
  "event-data": {
    "event": "delivered",
    "timestamp": 1763583600,
    "recipient": "customer@example.com",
    "message": {
      "headers": {
        "subject": "Your order has shipped"
      }
    }
  }
}

4. How to Verify Mailgun Webhook Signatures

Signing details
Algorithm
HMAC-SHA256
Header
(in body — not a header)
Encoding
hex

Mailgun puts `signature.timestamp`, `signature.token`, and `signature.signature` in the **request body** (not headers). The signed string is `{timestamp}{token}` concatenated, signed with your Webhook Signing Key (different from your API key), output as lowercase hex.

Node.js (Express)

import crypto from 'node:crypto';
import express from 'express';

const app = express();

app.post(
  '/webhooks/mailgun',
  express.json(), // Mailgun sends JSON
  (req, res) => {
    const { timestamp, token, signature } = req.body.signature ?? {};
    if (!timestamp || !token || !signature) {
      return res.status(401).send('missing signature fields');
    }

    const expected = crypto
      .createHmac('sha256', process.env.MAILGUN_WEBHOOK_SIGNING_KEY!)
      .update(timestamp + token)
      .digest('hex');

    if (
      signature.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    ) {
      return res.status(403).send('invalid signature');
    }
    res.json({ ok: true, event: req.body['event-data']?.event });
  },
);

Python (FastAPI)

import hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()

@app.post("/webhooks/mailgun")
async def mailgun_webhook(request: Request):
    payload = await request.json()
    sig = payload.get('signature', {})
    timestamp = sig.get('timestamp')
    token = sig.get('token')
    signature = sig.get('signature')
    if not all([timestamp, token, signature]):
        raise HTTPException(status_code=401, detail='missing signature fields')

    expected = hmac.new(
        os.environ['MAILGUN_WEBHOOK_SIGNING_KEY'].encode(),
        msg=(timestamp + token).encode(),
        digestmod=hashlib.sha256,
    ).hexdigest()
    if not hmac.compare_digest(signature, expected):
        raise HTTPException(status_code=403, detail='invalid signature')
    return {'ok': True}
Watch out: Mailgun is unusual — the signature lives in the JSON request body, not in headers. Use the dedicated Webhook Signing Key (Settings → API Security), not your Mailgun API Key. The signed-string formula is just `timestamp + token` — the body content itself is not part of the signature, only the (timestamp, token) anti-replay pair.

Capture a real Mailgun webhook with HookRay first, then replay the captured request against your verifier locally — that way you can iterate on the verification code without re-triggering events in Mailgun. Read Mailgun's official signing docs for the canonical reference, or see the cross-service signature verification guide for Ruby and timing-safe comparison patterns.

5. Frequently Asked Questions

How do I test Mailgun webhooks without deploying?

Use HookRay to get an instant public webhook URL. Paste it into your Mailgun dashboard's webhook configuration, trigger an event, and watch the payload arrive in real time. No code, no ngrok, no deployment required. The free tier captures 100 requests per month and works on all Mailgun event types.

Why aren't my Mailgun webhooks arriving?

The four most common causes: (1) the endpoint URL isn't publicly accessible — Mailgun can't reach localhost; (2) the wrong events are subscribed in your Mailgun dashboard; (3) signature verification is rejecting the request before your handler runs; (4) Mailgun can't reach your server because of a firewall, expired SSL certificate, or wrong DNS. Use HookRay's URL to isolate which of these four is failing — if HookRay receives the webhook, the problem is in your handler. If HookRay doesn't, the problem is in Mailgun configuration.

Why am I getting 400 or 500 errors from my Mailgun webhook?

Mailgun reports the response status your endpoint returned. HookRay accepts any payload and returns 200 OK by default, so if you see 400/500 in your Mailgun dashboard while pointing at HookRay, the issue is in Mailgun's configuration (wrong event, malformed signing secret, etc.). If you point at your own endpoint and get 400/500, the issue is in your handler — capture the request with HookRay, replay it locally, and debug from the captured payload.

How do I verify Mailgun webhook signatures?

Mailgun signs each webhook request with a shared secret. Capture the raw headers and body using HookRay, then verify the signature in your application using Mailgun's SDK or a standard HMAC library. Once verification works against HookRay-captured data, you can safely deploy. Mailgun's docs (linked above) cover the exact signing algorithm.

Can I replay a captured Mailgun webhook?

Yes — HookRay's replay feature re-sends any captured webhook to a different endpoint with one click. This is the fastest way to fix a buggy handler: capture the payload once, fix your code, and replay until it works. No need to re-trigger the event in Mailgun.

6. Next Steps

  • Use HookRay's webhook replay feature to re-send captured webhooks while building your handler
  • Enable smart parsing (Pro plan) to see Mailgun-specific fields highlighted automatically
  • Check the Mailgun webhook documentation for the complete event reference

Ready to test Mailgun webhooks?

Get a free webhook URL in 5 seconds. No signup required.

Start Testing Mailgun Webhooks — Free

Free PDF: Webhook Testing Cheat Sheet 2026

One-page reference for 50+ APIs — canonical events, signing methods, sample payloads. Print it, pin it, share it.

📄 Download the cheat sheet (PDF, 180KB)