---
title: "Test SendGrid Webhooks Locally | WebhookRelay"
meta:
  "og:description": "Test the SendGrid Event Webhook locally on localhost without deploying. Inspect the real JSON event array, forward to your handler, and verify the signed webhook signature."
  "og:title": "Test SendGrid Webhooks Locally"
  description: "Test the SendGrid Event Webhook locally on localhost without deploying. Inspect the real JSON event array, forward to your handler, and verify the signed webhook signature."
---

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

# **Test SendGrid Webhooks Locally**

Test the SendGrid Event Webhook locally on localhost without deploying. Inspect the real JSON event array, forward to your handler, and verify the signed webhook signature.

![Test SendGrid Webhooks Locally (SendGrid Event Webhook on localhost)](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/images/blog/heroes/localhost.jpg)

You are building a SendGrid integration — updating delivery status, tracking opens and clicks, suppressing addresses that bounce — and you need to see your handler react to a real batch of email events. The problem is immediate: SendGrid will only POST to a **public URL**, and your handler is running on `localhost:8080`. SendGrid has no way to reach it.

The usual workarounds are painful. Deploying to a staging server for every code change is slow. Pasting payloads from the docs into curl gives you a _guess_ at the real request, not the real headers and body SendGrid actually sends. What you want is to **test SendGrid webhooks locally** — real events, hitting your local handler, with a URL that does not change every time you restart.

This guide shows how to do exactly that.

## [Why testing SendGrid webhooks locally is tricky](#why-testing-sendgrid-webhooks-locally-is-tricky)

The SendGrid Event Webhook is just an HTTP request that SendGrid sends to a URL whenever your email moves through its pipeline — processed, delivered, opened, clicked, bounced. SendGrid 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 SendGrid 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 SendGrid 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 SendGrid actually sends. Open the free [Webhook Bin](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/webhook-bin/) — no signup — and you get an instant public URL.

1. Copy the Webhook Bin URL.
2. In the SendGrid dashboard, go to **Settings → Mail Settings → Event Webhook** (in some accounts this lives under **Settings → Tracking**).
3. Paste the URL into the **HTTP POST URL** field and select the events you care about — `processed`, `delivered`, `open`, `click`, `bounce`, `dropped`, `deferred`, `spamreport`, `unsubscribe`.
4. Toggle the Event Webhook **on** and save.

SendGrid provides a **Test Your Integration** button that POSTs a sample batch immediately — you will see it land in Webhook Bin right away. Inspect the captured request and note the most important detail: the body is a **JSON array** of event objects, not a single object. Each object has fields like `email`, `event`, `timestamp`, `sg_event_id`, `sg_message_id`, and (for engagement events) a `category`. The headers include `X-Twilio-Email-Event-Webhook-Signature` and `X-Twilio-Email-Event-Webhook-Timestamp` when signing is enabled.

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-sendgrid-webhooks-locally/blog/how-to-test-webhooks/) and [What is a webhook](https://webhookrelay.com/blog/receive-sendgrid-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 `sendgrid`. The bucket gives you a stable public input endpoint.

Start forwarding to your local server:

```
relay forward --bucket sendgrid 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-sendgrid-webhooks-locally/docs/webhooks/internal/localhost/).

Now update the Event Webhook's **HTTP POST URL** to your Webhook Relay endpoint (or just create it there from the start). Click **Test Your Integration** again — or send yourself a real email — and watch the event batch arrive on `localhost`.

## [SendGrid-specific configuration and quirks](#sendgrid-specific-configuration-and-quirks)

A few SendGrid details worth knowing:

- **Where to add it:** the Event Webhook is configured under **Settings → Mail Settings → Event Webhook** (or **Settings → Tracking** in older accounts). Newer accounts can also create multiple Event Webhooks via the API.
- **Batched JSON array:** SendGrid groups multiple events into a **single POST as a JSON array**. Your handler must iterate the array — a parser that expects one object per request will silently drop events.
- **Event types:** subscribe only to what you handle. Delivery events (`processed`, `delivered`, `bounce`, `dropped`, `deferred`) and engagement events (`open`, `click`, `unsubscribe`, `spamreport`) are toggled separately.
- **Deduplicate on `sg_event_id`:** SendGrid retries on non-`2xx` responses, so the same event can arrive more than once. Use `sg_event_id` for idempotency and return `2xx` quickly.
- **Inbound Parse is different:** the [Inbound Parse webhook](https://www.twilio.com/docs/sendgrid/for-developers/parsing-email/setting-up-the-inbound-parse-webhook) is a separate feature for _receiving_ inbound email — it POSTs `multipart/form-data` to your endpoint, not the JSON event array. You can forward it through the same Webhook Relay bucket, but parse it differently.

## [Step 3: Verify the Signed Event Webhook signature](#step-3-verify-the-signed-event-webhook-signature)

SendGrid's signature feature is **ECDSA, not HMAC** — an important distinction if you have only ever verified HMAC webhooks. When you enable the **Signed Event Webhook** in Mail Settings, SendGrid generates an elliptic-curve key pair, signs each request with the **private** key, and gives you a **public verification key** in the dashboard.

Each request carries the signature in the **`X-Twilio-Email-Event-Webhook-Signature`** header and a timestamp in **`X-Twilio-Email-Event-Webhook-Timestamp`**. To verify, you reconstruct the signed payload as the **timestamp concatenated with the raw request body**, then check the ECDSA signature against your public key. As always, use the **raw** bytes of the body — re-serialized JSON will not match. SendGrid's official libraries ship an `eventwebhook` helper that wraps this for you.

Because this is ECDSA rather than a shared-secret HMAC, the generic [HMAC signature verifier](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/hmac-verification/) does not apply directly to SendGrid's signed webhook — but it remains a handy tool for the many HMAC-based providers. For the broader concepts, language-specific code, and the common pitfalls (reading the body after a JSON parser has consumed it, forgetting the timestamp, timing-safe comparison), read [Verify a webhook signature](https://webhookrelay.com/blog/receive-sendgrid-webhooks-locally/blog/verify-webhook-signature/).

## [Replay and iterate](#replay-and-iterate)

This is where local development gets fast:

- **Replay from SendGrid** by hitting **Test Your Integration** again, or by sending a fresh test email.
- **Replay from Webhook Relay** — past requests are stored on your bucket, so you can resend a captured event batch without touching SendGrid 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 a 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 SendGrid configuration never needs to change.

## [Get started](#get-started)

1. Inspect the real payload in the free [Webhook Bin](https://webhookrelay.com/blog/receive-sendgrid-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 sendgrid http://localhost:8080/webhook`.
3. Point your SendGrid Event Webhook's HTTP POST URL at the stable endpoint, click Test Your Integration, and watch the event batch hit `localhost`.

You will be testing real SendGrid events against your local handler in a few minutes — no deploys, no open firewall ports, and a URL you configure exactly once.