Launch special — let's split the check with SPLITCHECK for 50% off
8 min read

GitHub Webhooks: Complete Setup, Payload & Security Guide

Complete GitHub webhooks tutorial: setup steps, push & pull_request payload examples, HMAC X-Hub-Signature-256 verification in Node, and ready-to-copy CI/CD automation. Test live with HookSense.

GitHubWebhooksGuide
O

Ozer

Developer & Founder of HookSense

GitHub webhooks let you build automated workflows that respond to repository events in real time. When someone pushes code, opens a pull request, creates an issue, or performs any of dozens of other actions, GitHub sends an HTTP POST request to your server with details about what happened. This is the foundation of CI/CD pipelines, chat notifications, deployment triggers, and the GitHub Actions workflow_run integrations every modern developer tool relies on.

This guide walks you through setting up GitHub webhooks from scratch, covers the most important event types, shows you how to parse the payload, and shares real-world automation patterns. Updated for 2026 with the current dashboard, the new fine-grained webhook secrets, and a working Node.js example you can copy into production today.

Setting Up a GitHub Webhook

Repository-Level Webhooks

To create a webhook for a specific repository:

  1. Navigate to your repository on GitHub.
  2. Click Settings → Webhooks → Add webhook.
  3. Enter your Payload URL — the endpoint that will receive events.
  4. Set Content type to application/json (recommended over form-encoded).
  5. Enter a Secret — a random string used for HMAC signature verification.
  6. Choose which events to receive: "Just the push event," "Send me everything," or select individual events.
  7. Click Add webhook.

GitHub immediately sends a ping event to verify your endpoint is reachable. If your endpoint returns a 2xx status, the webhook shows a green checkmark.

Organization-Level Webhooks

Organization webhooks work the same way but receive events from all repositories in the organization. Navigate to Organization Settings → Webhooks to set them up. This is useful for company-wide CI/CD or audit logging.

Using HookSense During Setup

If you do not have a server ready yet, you can use HookSense to capture and inspect GitHub webhooks immediately:

  1. Create a free endpoint at hooksense.com.
  2. Use the HookSense URL as your Payload URL in GitHub settings.
  3. Trigger an event (e.g., push a commit) and see the payload appear in real time.
  4. When your server is ready, use the CLI to forward events: npx hooksense listen -p 3000

Key GitHub Webhook Events

GitHub supports over 40 event types. Here are the ones developers use most frequently:

push

Triggered when commits are pushed to a branch. This is the most common event and powers most CI/CD pipelines.

{
  "ref": "refs/heads/main",
  "before": "abc123...",
  "after": "def456...",
  "commits": [
    {
      "id": "def456...",
      "message": "Fix login bug",
      "author": { "name": "Ozer", "email": "[email protected]" },
      "added": ["src/auth.ts"],
      "modified": ["src/login.ts"],
      "removed": []
    }
  ],
  "repository": { "full_name": "ozers/myapp" },
  "pusher": { "name": "ozers" }
}

pull_request

Triggered when a pull request is opened, closed, merged, labeled, or updated. The action field tells you what happened:

{
  "action": "opened",
  "number": 42,
  "pull_request": {
    "title": "Add dark mode support",
    "user": { "login": "ozers" },
    "head": { "ref": "feature/dark-mode" },
    "base": { "ref": "main" },
    "merged": false
  }
}

issues

Triggered when an issue is opened, closed, labeled, assigned, or edited. Useful for project management integrations and chat notifications.

release

Triggered when a release is published, edited, or deleted. Common trigger for deployment pipelines and changelog generation.

workflow_run

Triggered when a GitHub Actions workflow is requested, in progress, or completed. Useful for chaining workflows or external monitoring.

star and fork

Triggered when someone stars or forks your repository. Useful for growth tracking and community notifications.

Payload Structure

Every GitHub webhook request includes these headers:

HeaderDescription
X-GitHub-EventThe event type (e.g., push, pull_request)
X-GitHub-DeliveryA unique GUID for this delivery
X-Hub-Signature-256HMAC-SHA256 signature for verification
Content-Typeapplication/json
User-AgentGitHub-Hookshot/...

The body contains the event-specific payload as JSON. Payloads can be large — a push event with many commits can be several KB.

Securing Your Endpoint

Always verify the X-Hub-Signature-256 header to ensure requests genuinely come from GitHub:

const crypto = require('crypto');

function verifyGitHubSignature(payload, signature, secret) {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret)
      .update(payload)
      .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

app.post('/webhooks/github', express.raw({ type: '*/*' }), (req, res) => {
  const sig = req.headers['x-hub-signature-256'];

  if (!sig || !verifyGitHubSignature(req.body, sig, WEBHOOK_SECRET)) {
    return res.status(401).send('Unauthorized');
  }

  const event = req.headers['x-github-event'];
  const payload = JSON.parse(req.body);

  // Route by event type
  handleEvent(event, payload);
  res.status(200).send('OK');
});

Use crypto.timingSafeEqual() to prevent timing-based attacks. Never compare signatures with ===.

Real-World Automation Patterns

Auto-Deploy on Push to Main

if (event === 'push' && payload.ref === 'refs/heads/main') {
  exec('cd /app && git pull && npm run build && pm2 restart app');
}

Slack Notification on New PR

if (event === 'pull_request' && payload.action === 'opened') {
  await slack.post('#dev', {
    text: `New PR: ${payload.pull_request.title} by ${payload.pull_request.user.login}`,
    url: payload.pull_request.html_url
  });
}

Auto-Label Issues

if (event === 'issues' && payload.action === 'opened') {
  const title = payload.issue.title.toLowerCase();
  const labels = [];
  if (title.includes('bug')) labels.push('bug');
  if (title.includes('feature')) labels.push('enhancement');
  await github.issues.addLabels({
    owner, repo,
    issue_number: payload.issue.number,
    labels
  });
}

Trigger External CI on Pull Request

if (event === 'pull_request' && ['opened', 'synchronize'].includes(payload.action)) {
  await triggerCIPipeline({
    branch: payload.pull_request.head.ref,
    sha: payload.pull_request.head.sha,
    prNumber: payload.number
  });
}

Debugging GitHub Webhooks

GitHub provides a "Recent Deliveries" tab in webhook settings where you can see every delivery attempt, the response code, and resend events. However, this interface is limited — you cannot search, filter, or forward events locally.

For a better debugging experience, point your GitHub webhook at a HookSense endpoint. You get real-time inspection, full payload viewing with syntax highlighting, signature verification badges, and the ability to replay any event. Use the CLI to forward events to your local server while developing.

Common Gotchas

  • Payload size limits: GitHub truncates payloads larger than 25 MB. For push events with many commits, only the first 20 commits are included.
  • Timeouts: GitHub expects a response within 10 seconds. Use a queue for heavy processing.
  • Branch deletion: A push event with deleted: true has after set to all zeros. Do not try to fetch the commit.
  • Duplicate deliveries: GitHub may retry if your response is slow. Always handle idempotency.
  • IP allowlisting: GitHub publishes webhook IP ranges at api.github.com/meta under the hooks key.

Summary

GitHub webhooks are straightforward to set up but require careful attention to security and error handling. Always verify signatures, handle events idempotently, respond quickly, and use a tool like HookSense for development and debugging. With these practices in place, you can build reliable automations that respond to your repository events in real time.

Frequently Asked Questions

How do I set up a GitHub webhook?

Go to your repository on GitHub, click Settings → Webhooks → Add webhook, enter a payload URL, set content type to application/json, choose which events to receive (push, pull request, issues, etc.), set a secret for HMAC verification, and save. GitHub immediately sends a ping event to confirm delivery.

What is the difference between a repository webhook and an organization webhook?

A repository webhook receives events only from a single repository. An organization webhook receives events from every repository in the organization plus organization-level events like member additions. Use organization webhooks for centralized auditing or compliance systems.

How do I verify a GitHub webhook signature?

GitHub sends an X-Hub-Signature-256 header containing an HMAC SHA-256 hash of the raw request body using your configured secret. Compute the same hash on your server with a timing-safe comparison (e.g. crypto.timingSafeEqual in Node.js) and reject any request whose signature does not match.

Why is my GitHub webhook not firing?

Common causes: your endpoint returned a non-2xx status, the payload URL is not publicly reachable (localhost is blocked), the event type is not subscribed, the content type is mismatched, or GitHub disabled delivery after too many failures. Check Settings → Webhooks → Recent Deliveries for the exact response.

How do I test GitHub webhooks locally?

Point your GitHub webhook at a public inspection URL like HookSense, then use npx hooksense listen -p 3000 to forward captured events to your local server. You can replay any past delivery while you iterate, without triggering new commits.

What is the GitHub webhook payload size limit?

GitHub truncates webhook payloads larger than 25 MB. For push events with many commits, only the first 20 commits are included in the payload. Use the GitHub API to fetch the full commit list if you need more.

Related

Related posts

Try HookSense Free

Inspect, debug, and replay webhooks in real-time. No credit card required.

Get Started Free