Receive Mailgun Webhooks Locally (Test Mailgun Webhooks on localhost)
Test Mailgun webhooks locally on localhost without deploying. Inspect the real event payload, forward delivered/opened/bounced events to your handler, and verify the signature.
You are building an email integration on top of Mailgun — updating a contact's status when a message bounces, recording opens and clicks, flagging complaints — and you need to see your handler react to a real event. The problem is immediate: Mailgun will only POST to a public URL, and your handler is running on localhost:8080. Mailgun has no way to reach it.
The usual workarounds are painful. Deploying to staging for every code change is slow, and pasting sample payloads from the docs into curl gives you a guess at the real request — not the actual headers and JSON Mailgun sends for a delivered versus an opened event. What you want is to receive Mailgun webhooks locally — real events hitting your local handler, with a URL that does not change every time you restart.
Why testing Mailgun webhooks locally is tricky
A Mailgun webhook is just an HTTP request that Mailgun sends to a URL when something happens to a message you sent. Mailgun sits on the public internet; your dev machine usually does not. It is behind a router, a corporate firewall, or both, with no public IP and no inbound ports open.
So you need something in the middle: a public endpoint Mailgun can hit, that relays each request down to your laptop without you opening a single firewall port. That is what Webhook Relay does — and unlike a random tunnel URL, the endpoint is stable, so you configure Mailgun once and never touch it again.
Step 1: Inspect the real payload with Webhook Bin
Before you write any handler code, find out what Mailgun actually sends. Open the free Webhook Bin — no signup — and you get an instant public URL.
- Copy the Webhook Bin URL.
- In your Mailgun dashboard, go to Sending → Webhooks.
- Select your sending domain, then add a webhook for an event type — for example Delivered Messages — and paste the Webhook Bin URL.
- Save it. Mailgun lets you send a test request from the dashboard, or you can send a real message and trigger the event naturally.
The captured request shows you everything: the full JSON body, the query string, and every header. A modern Mailgun payload is JSON with two top-level objects — a signature object (containing timestamp, token, and signature) and an event-data object (containing the event type, the message metadata, recipient, and timestamps).
Now you know the exact shape of the data before writing a line of code. For more on this approach, see How to test webhooks and What is a webhook.
Step 2: Forward the events to localhost with the relay agent
Once you know the payload, route those same events into your local handler. Sign up for Webhook Relay, install the relay agent (CLI or Docker), and create a bucket — say mailgun. The bucket gives you a stable public input endpoint.
Start forwarding to your local server:
relay forward --bucket mailgun http://localhost:8080/webhook
The agent opens an outbound connection to Webhook Relay and streams every incoming request down to http://localhost:8080/webhook. Because the connection is outbound, there are no firewall ports to open and no public IP needed — this works from your laptop, behind a corporate proxy, or inside a Kubernetes cluster. Running in Docker? The same command works in the official webhookrelay/webhookrelayd image. Full details are in the localhost forwarding docs.
Now point your Mailgun webhook at the Webhook Relay endpoint (or just create it there from the start), send a test message, and watch the event arrive on localhost.
Mailgun-specific configuration and quirks
A few Mailgun details worth knowing:
- Webhooks are per event type. In Sending → Webhooks you register a URL against each event you care about:
delivered,opened,clicked,bounced(permanent failures),temporary_fail,complained,unsubscribed, and more. You can point every event type at the same Webhook Relay endpoint and branch in your handler on theeventfield ofevent-data. - Configure it in the dashboard or via the API. The same webhooks can be created programmatically through the Mailgun webhooks API if you prefer infrastructure-as-code.
- Test from the dashboard. Each webhook has a Test webhook button that sends a sample payload — handy for a first round trip.
- Retries. If your endpoint does not return a 2xx, Mailgun retries on a back-off schedule, so a flaky local handler gets a second chance rather than silently dropping the event.
- Opened and clicked require tracking. Those events only fire when open/click tracking is enabled.
Need to deliver the same events to more than one place — say your local handler and a Slack channel? That is a server-side fan-out, covered in Mailgun webhook fanout. You can also reshape the payload in flight with webhook transformations.
Step 3: Verify the Mailgun webhook signature
Mailgun signs every webhook so you can confirm it genuinely came from Mailgun. Inside the payload's signature object you get three values: timestamp, token, and signature.
To verify, concatenate the timestamp and the token (in that order, with no separator) and compute an HMAC-SHA256 over that string, keyed with your Mailgun HTTP webhook signing key (found in your dashboard's API security / signing key settings). Render the result as hex and compare it in constant time to the signature value. Note the important detail: the HMAC covers timestamp + token, not the request body — a common point of confusion if you have implemented other providers that sign the raw body. As an extra guard, reject requests whose timestamp is too old and cache the one-time token to block replays.
To sanity-check your implementation, paste the values and your signing key into the free HMAC signature verifier. For language-specific code and the common pitfalls, read Verify a webhook signature.
Replay and iterate
This is where local development gets fast:
- Replay from Mailgun by re-sending a test webhook from the dashboard, or by triggering the event again.
- Replay from Webhook Relay — past requests are stored on your bucket, so you can resend a captured
bouncedorcomplainedevent without touching Mailgun at all. - Iterate on your handler by editing code and replaying the same delivery until it behaves correctly. No commits, no pushes, no deploys just to test one code path.
Because the Webhook Relay endpoint is stable, you can stop and restart the agent, reboot your machine, or come back next week — the Mailgun configuration never needs to change.
Get started
- Inspect the real payload in the free Webhook Bin — no signup needed.
- Create a Webhook Relay account, install the agent, and run
relay forward --bucket mailgun http://localhost:8080/webhook. - Point your Mailgun webhook at the stable endpoint, send a test message, and watch the event hit
localhost.
You will be testing real Mailgun events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once.
