---
title: "Receive Mailgun Webhooks Locally | WebhookRelay"
meta:
  "og:description": "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."
  "og:title": "Receive Mailgun Webhooks Locally"
  description: "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."
---

![Stripes](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/images/stripes.svg)

# **Receive Mailgun Webhooks Locally**

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.

![Receive Mailgun Webhooks Locally (Test Mailgun Webhooks on localhost)](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/images/blog/heroes/localhost.jpg)

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](#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](https://my.webhookrelay.com/register) 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](#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](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL.

1. Copy the Webhook Bin URL.
2. In your Mailgun dashboard, go to **Sending → Webhooks**.
3. Select your sending domain, then add a webhook for an event type — for example **Delivered Messages** — and paste the Webhook Bin URL.
4. 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](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/blog/what-is-webhook/).

## [Step 2: Forward the events to localhost with the relay agent](#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](https://my.webhookrelay.com/register), 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](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/docs/webhooks/internal/localhost/).

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](#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 the `event` field of `event-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](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/blog/mailgun-webhook-fanout/). You can also reshape the payload in flight with [webhook transformations](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/features/transform-webhooks/).

## [Step 3: Verify the Mailgun webhook signature](#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](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/hmac-verification/). For language-specific code and the common pitfalls, read [Verify a webhook signature](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/blog/verify-webhook-signature/).

## [Replay and iterate](#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 `bounced` or `complained` event 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](#get-started)

1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-mailgun-webhooks-locally/webhook-bin/) — no signup needed.
2. [Create a Webhook Relay account](https://my.webhookrelay.com/register), install the agent, and run `relay forward --bucket mailgun http://localhost:8080/webhook`.
3. 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.