← Back to Webhook Academy

GitHub Webhook Guide

How to set up, verify, test, and monitor GitHub webhooks — with code examples and troubleshooting.

Last updated March 2026. Covers GitHub webhooks API v3 and GitHub Apps webhook events.

1

What Are GitHub Webhooks?

GitHub webhooks let your application receive real-time HTTP POST notifications whenever events happen in a repository or organization — a push to main, a pull request opened, an issue created, a release published. Instead of polling the GitHub API, your server gets notified the moment something happens.

Common GitHub webhook event types:

push pull_request issues release workflow_run star fork create delete deployment_status issue_comment ping

GitHub sends a ping event when you first create a webhook, which is useful for verifying your endpoint is reachable. The X-GitHub-Event header tells you which event type was delivered.

2

Creating a Webhook in GitHub

Setting up a GitHub webhook takes about 30 seconds:

  1. Go to your repository on GitHub and click Settings.
  2. In the left sidebar, click Webhooks.
  3. Click Add webhook.
  4. Paste your Payload URL — this is the public endpoint that will receive events (e.g. your SDNBros tester URL or your production server).
  5. Set Content type to application/json.
  6. Enter a Secret — a random string used to sign payloads (generate one with openssl rand -hex 32).
  7. Choose "Let me select individual events" and pick the events you need, or select "Send me everything".
  8. Click Add webhook.

GitHub will immediately send a ping event to verify your endpoint is reachable. If it fails, check that your URL is publicly accessible and returns a 200 response.

3

Verifying Signatures with X-Hub-Signature-256

GitHub signs every webhook payload using HMAC-SHA256 with your secret. The signature is sent in the X-Hub-Signature-256 header. Always verify it to ensure the request genuinely came from GitHub.

const crypto = require('crypto');
const express = require('express');
const app = express();

const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;

app.post('/webhook/github',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-hub-signature-256'];
    if (!signature) {
      return res.status(401).send('Missing signature header');
    }

    // Compute expected signature
    const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);
    hmac.update(req.body);
    const expected = 'sha256=' + hmac.digest('hex');

    // Timing-safe comparison to prevent timing attacks
    if (!crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expected)
    )) {
      return res.status(401).send('Invalid signature');
    }

    // Signature verified — safe to process
    const event = req.headers['x-github-event'];
    const payload = JSON.parse(req.body);

    switch (event) {
      case 'push':
        console.log('Push to', payload.ref, 'by', payload.pusher.name);
        break;
      case 'pull_request':
        console.log('PR', payload.action, ':', payload.pull_request.title);
        break;
      case 'ping':
        console.log('Webhook ping received. Zen:', payload.zen);
        break;
      default:
        console.log('Event:', event, 'Action:', payload.action);
    }

    res.status(200).json({ received: true });
  }
);

app.listen(3000, () => console.log('GitHub webhook server on port 3000'));

Critical: use the raw request body for HMAC computation, not parsed JSON. Use crypto.timingSafeEqual to prevent timing attacks. Never use a simple === comparison for signatures.

4

Testing with SDNBros

During development, you need a public URL that GitHub can reach. Here's the fastest way:

  1. Open the SDNBros Webhook Tester — you get a public endpoint URL instantly, no signup required.
  2. In your GitHub repository, go to Settings > Webhooks > Add webhook. Paste your SDNBros URL as the Payload URL.
  3. Set content type to application/json and select the events you want.
  4. Click Add webhook — GitHub sends a ping event immediately.
  5. Push a commit, open a PR, or create an issue to trigger real events.
  6. Switch to SDNBros — you'll see every event arrive in real time with full headers, body, and timing. SDNBros auto-detects GitHub webhook events and shows the event type and action.

For local development, use sdnbros tunnel to forward webhooks from your SDNBros endpoint directly to localhost:3000.

Common GitHub Webhook Errors

ErrorCauseFix
401 UnauthorizedSignature verification failing — wrong secret or using parsed body instead of raw bodyVerify your secret matches what's configured in GitHub. Use the raw request body for HMAC computation, not JSON.parse() output.
Timeout (no response)Endpoint takes too long to respond — GitHub times out after 10 secondsReturn 200 immediately and process the event asynchronously. Use a message queue or background worker for heavy processing.
Payload too largeGitHub webhook payloads can be up to 25 MB. Your server or reverse proxy may reject large bodies.Increase your body size limit (e.g. in nginx: client_max_body_size 30m). For very large payloads, fetch details from the API instead.
Webhook disabledToo many consecutive delivery failures (GitHub disables after sustained errors)Fix the underlying issue, then go to Settings > Webhooks and re-enable the webhook. Check "Recent Deliveries" for error details.
Duplicate deliveriesGitHub retries on timeout or non-2xx response. Your handler processes events more than once.Use the X-GitHub-Delivery header (unique per delivery) to deduplicate. Make your handler idempotent.

Test GitHub webhooks free

Create a public endpoint and inspect GitHub events. No signup is needed for the tester.

Open Webhook Tester Monitor in Production

Related guides: What is a Webhook? · Webhook vs API · Webhook Security · Retry Strategies · Stripe · Shopify · Slack

More: What is a Webhook? | Webhook Security Guide | Stripe Webhook Guide | Webhook vs API | Compare Tools