Test Sentry Webhooks Locally

Test Sentry webhooks locally and receive them on localhost without deploying. Inspect the real payload, forward to your handler, and verify the Sentry-Hook-Signature.

Test Sentry Webhooks Locally (Receive Sentry Webhooks on localhost)

Sentry will only deliver webhooks to a public URL. If you're building an integration — say, a small service that opens Jira tickets from new Sentry issues, or pages someone when a metric alert fires — that's the first wall you hit, because the handler you're actually working on runs on localhost:8080 and Sentry can't see it.

The workarounds people reach for are all bad in their own way. Deploying to staging on every change turns a ten-second edit into a five-minute wait. Copying a sample payload from the docs into curl feels productive, but the sample is a guess: it goes stale, and it carries none of the signature headers, so the verification code you care most about never gets exercised.

Here is the setup I use to test Sentry webhooks locally with real events. It takes about ten minutes.

Which Sentry webhook mechanism are you on?

Sentry has grown three ways of sending webhooks, and they behave differently, so pin down which one you're using before wiring anything up:

  • Integration webhooks. Create an Internal or Public integration under Settings → Integrations → Custom Integrations and give it a Webhook URL. You get structured JSON for installation, issue, error, event_alert and metric_alert resources, and the requests are signed. This is the one to use.
  • Alert rule webhook actions. Inside an issue or metric alert rule you can add a webhook action that posts to an integration. Those payloads arrive with an "action": "triggered" shape wrapping the event data.
  • Legacy Webhooks, the old per-project plugin. It still works, but the requests aren't signed, and Sentry itself steers new setups away from it.

Everything below assumes an Internal Integration, since that's what produces the Sentry-Hook-Signature and Sentry-Hook-Resource headers.

Step 1: look at what Sentry actually sends

Before writing handler code, capture a real request. Open a free Webhook Bin (no signup), copy the bin URL, and paste it into the Webhook URL field of a new integration: Settings → Integrations → Create New Integration → Internal Integration. Enable the resources you care about, save, then throw a test error from your app or resolve an existing issue.

The request shows up in the bin within a second or two. Here's a trimmed issue webhook from a test project:

{
  "action": "created",
  "installation": { "uuid": "a8e5d37a-696c-4c54-adb5-b3f28d64c7de" },
  "data": {
    "issue": {
      "id": "1170820242",
      "shortId": "PYTHON-E",
      "title": "ZeroDivisionError: division by zero",
      "culprit": "api.views in get",
      "level": "error",
      "status": "unresolved",
      "project": { "id": 1, "slug": "python" }
    }
  },
  "actor": { "type": "application", "id": "sentry", "name": "Sentry" }
}

The headers matter as much as the body:

Content-Type: application/json
Request-ID: 3e885ff0f5f84e0eb2c4f96b1a3a4e5f
Sentry-Hook-Resource: issue
Sentry-Hook-Timestamp: 1749472405
Sentry-Hook-Signature: 8676dd2f8c78f7dbb9d129b9f2a879ee1a4d4a771261813e17d2dbaee7e2acb1

Sentry-Hook-Resource tells you which resource fired, and your handler should branch on it, because the body shape differs for each one. An issue webhook wraps data.issue with actions like created or resolved; an alert-rule webhook puts the event under data with "action": "triggered". If webhooks are new territory, What is a webhook and How to test webhooks cover the ground rules.

Step 2: receive Sentry webhooks on localhost

Once you know the shape, point the same events at your local code. Sign up for Webhook Relay, create a bucket (mine is called sentry), and start the agent:

relay forward --bucket sentry http://localhost:8080/webhook

The bucket has a public input URL that never changes. Set it as the integration's Webhook URL and you won't touch the Sentry config again — restart the agent, reboot, come back next week, the endpoint is still there. The agent holds an outbound connection to Webhook Relay and streams incoming requests down to localhost:8080, so nothing needs a public IP and no firewall port opens. It works the same from a laptop behind a corporate proxy, and the webhookrelay/webhookrelayd Docker image runs the same forwarding if your handler lives in a container. Details are in the localhost forwarding docs.

Trigger another error and watch it land on your local handler.

Sentry quirks worth knowing

  • Enabling the issue or error resource means a webhook fires on every issue create, resolve, assign and archive across the organization — noisy on a busy org. If you only want events from specific conditions, leave the resource toggles off and attach a webhook action to an alert rule instead.
  • Installing an internal integration immediately fires an installation webhook. That's a convenient reachability check before you trigger any real errors.
  • The integration's Client Secret is what signs every request. You'll need it for verification below.
  • The legacy plugin lives somewhere else entirely: Project Settings → Legacy Integrations. If your requests arrive without a Sentry-Hook-Signature header, you're probably on the legacy path.

Step 3: verify the signature

Sentry computes an HMAC-SHA256 over the raw request body with your Client Secret and sends the hex digest in Sentry-Hook-Signature. Recompute it and compare before trusting anything:

const crypto = require("crypto");

function validSentrySignature(rawBody, signatureHeader, clientSecret) {
  const digest = crypto
    .createHmac("sha256", clientSecret)
    .update(rawBody, "utf8")
    .digest("hex");
  return (
    digest.length === signatureHeader.length &&
    crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signatureHeader))
  );
}

The classic mistake here is computing the HMAC over a re-serialized body after your framework's JSON middleware has already parsed it. Key order shifts, the bytes change, and the digest never matches — hash the raw bytes you received. If the digest still disagrees with the header, paste the captured body, your Client Secret and the received signature into the free HMAC signature verifier to see which side is wrong. Language-specific examples and the other common pitfalls are in Verify a webhook signature.

Replay instead of re-triggering

Requests that pass through the bucket are stored, so when the handler misbehaves you can replay the same delivery from the Webhook Relay dashboard rather than causing another error in your app. Edit code, replay, check the result, repeat. No commits or deploys involved. In practice this is where most of the time savings come from: the forwarding gets Sentry to your laptop, but replay is what makes the iteration loop fast.

Get started

  1. Capture a real payload in a free Webhook Bin.
  2. Create a Webhook Relay account and run relay forward --bucket sentry http://localhost:8080/webhook.
  3. Point your Sentry integration's Webhook URL at the bucket endpoint and trigger an error.

From there you're testing live Sentry events against local code, with a webhook URL you configure exactly once.