FeaturesGuidesBlogDocumentationFreeGet Started

Test GitHub Webhooks on Localhost

GitHub App and repo webhook deliveries to localhost — HMAC signature, Recent Deliveries, and tunnel URL registration.

GitHub has no official local forwarder like Stripe CLI — a tunnel is usually the fastest way to exercise Recent Deliveries and signature verification together. See the webhook hub for the CLI-vs-tunnel decision tree.

Try it in 60 seconds

GitHub cannot POST to localhost. Before debugging signature code, confirm delivery works:

npm run dev
npx localtolink --port 3000

Register https://k7m2x9p4q1.localtolink.com/api/github/webhook in your repo's webhook settings. GitHub sends a ping — your handler returns 200. If Recent Deliveries shows green, your networking is solved; any remaining issues are application logic.

See why webhooks fail on localhost for the general diagnosis flow.

Why GitHub webhooks need a public URL

GitHub sends webhook POST requests when repository events occur — pushes, pull requests, issues, releases, and dozens more. Your handler runs on localhost, which GitHub's servers cannot reach. A development tunnel provides a public HTTPS URL that forwards deliveries to your local machine.

Unlike Stripe, GitHub does not ship a CLI that forwards webhooks to localhost. A tunnel is the standard approach.

Setup

Terminal 1 — your app with a webhook handler:

npm run dev

Ensure you have a route that accepts POST requests, e.g. /api/github/webhook.

Terminal 2 — tunnel:

npx localtolink --port 3000

Copy the subdomain from the output.

Configure GitHub webhook

In your repository → Settings → Webhooks → Add webhook:

FieldValue
Payload URLhttps://k7m2x9p4q1.localtolink.com/api/github/webhook
Content typeapplication/json
SecretA random string you choose (used for verification)
SSL verificationEnable (default)
EventsChoose individual events or "Send me everything"

Replace k7m2x9p4q1 with the 10-character subdomain LocalToLink prints.

GitHub sends a ping event immediately after creation. Your handler should respond with 200 to confirm delivery.

Verify signatures

GitHub signs payloads with HMAC-SHA256. Always verify before processing:

import crypto from "crypto";

function verifySignature(payload, signature, secret) {
  if (!signature) return false;
  const hmac = crypto.createHmac("sha256", secret);
  const digest = "sha256=" + hmac.update(payload).digest("hex");
  try {
    return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature));
  } catch {
    return false;
  }
}

// In your handler — use raw body, not parsed JSON
const sig = req.headers["x-hub-signature-256"];
if (!verifySignature(rawBody, sig, process.env.GITHUB_WEBHOOK_SECRET)) {
  return new Response("Invalid signature", { status: 401 });
}

Set GITHUB_WEBHOOK_SECRET to the secret you configured in GitHub webhook settings.

Test delivery

After creating the webhook:

  1. Check Recent Deliveries in webhook settings for the ping event
  2. Green checkmark = your handler returned 2xx
  3. Red X = click for request/response details
  4. Use Redeliver to replay a failed delivery

Trigger real events by pushing a commit, opening a PR, or using GitHub's webhook test features.

Event types worth testing locally

EventWhen to subscribe
pushCI triggers, deployment hooks
pull_requestReview automation, status checks
issuesProject management integrations
releaseChangelog bots, release notifications
workflow_runActions orchestration (GitHub Apps)

Start with one event type, verify delivery, then add more. Subscribing to "Send me everything" generates noise during active development.

GitHub App vs repository webhook

Repository webhooks (this guide) — configured per repo under Settings → Webhooks.

GitHub Apps — webhooks configured in the app settings with a different delivery format. The tunnel URL pattern is the same; verification uses the app's webhook secret.

Important notes

Troubleshooting

ProblemFix
Ping delivery failsConfirm handler route exists and returns 200
Signature verification failsUse raw body; confirm secret matches GitHub settings
Session endedRestart tunnel and update Payload URL
Events not firingCheck you subscribed to the right event types
Duplicate eventsGitHub retries on timeout — make handler idempotent

FAQ

Can I test organization webhooks?
Yes. Configure at the organization level with the same tunnel URL pattern.

Does this work with GitHub Enterprise Server?
Self-hosted GHE needs to reach your tunnel URL from its network. Confirm outbound access is allowed.

Should I use a different tunnel for request inspection?
If you need visual capture and replay, use logging plus GitHub Recent Deliveries first. Add specialized inspection tooling only when delivery logs are insufficient.

See Test Webhooks Locally for the general webhook hub.

Try it now

Start a tunnel from your project directory — no account required.

$ npx --yes localtolink@latest

See the full CLI reference for flags and exit codes.

LocalToLink CLI 1.0.1 — verified 2026-09-20