A GitHub webhook is a message GitHub posts to your server's URL whenever something happens in a repository, such as a push, a new issue or a release. You add it under the repository's Settings, Webhooks. Set a secret so you can check the X-Hub-Signature-256 header and reject fake deliveries.
| Item | What GitHub documents |
|---|---|
| Signature header | X-Hub-Signature-256, value starts with sha256= |
| Signature method | HMAC hex digest with SHA-256, your secret as the key |
| Response time | Over 10 seconds is recorded as a failed delivery |
| Automatic retries | None; failed deliveries are not redelivered automatically |
| Manual redelivery | Deliveries from the past 3 days |
| Payload size cap | 25 MB; larger payloads are not delivered |
Updated
Webhooks let GitHub tell your site or app about changes the moment they happen, so you can rebuild a site on each push, post a message when an issue opens, or update a changelog on release. The important part most quick guides skip is checking that a delivery really came from GitHub. The checker below computes the same signature GitHub sends, right in your browser.
Type a secret and a raw request body to see the X-Hub-Signature-256 value GitHub would send. It runs in your browser with the Web Crypto API; nothing is uploaded.
With GitHub's test values the result should equal: sha256=757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17
This Express route reads the raw body, computes the HMAC with your secret and compares it in constant time before trusting the payload.
express.raw keeps the exact bytes GitHub signed. The secret comes from an environment variable, never from the code.
import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.GITHUB_WEBHOOK_SECRET;
app.post("/webhooks/github", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("X-Hub-Signature-256") || "";
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
const a = Buffer.from(header);
const b = Buffer.from(expected);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send("Bad signature");
}
const event = req.get("X-GitHub-Event");
const payload = JSON.parse(req.body.toString("utf8"));
res.status(202).send("ok"); // answer first, then work
if (event === "push") console.log("Pushed to", payload.ref);
});
app.listen(3000);| Header | What it holds |
|---|---|
| X-GitHub-Event | The name of the event, such as push or issues |
| X-GitHub-Delivery | A unique ID (GUID) for this delivery |
| X-GitHub-Hook-ID | The ID of the webhook that sent it |
| X-Hub-Signature-256 | sha256= plus the HMAC of the body, only when a secret is set |
| User-Agent | Always starts with GitHub-Hookshot/ |
Every delivery is a POST with headers that describe it: X-GitHub-Event names the event, X-GitHub-Delivery is a unique ID, X-GitHub-Hook-ID identifies the webhook, and the User-Agent always begins with GitHub-Hookshot/.
The body is the event payload. For a push it lists the commits and the branch reference, so a deploy script can ignore pushes to branches other than the one it builds.
GitHub publishes a known input so you can prove your code is right before going live: the secret It's a Secret to Everybody and the payload Hello, World! should produce a signature that starts 757107ea.
The checker on this page is filled in with those values. If your server computes the same result from them, your verification code is correct.
No. GitHub's docs state it does not automatically redeliver failed deliveries. You can redeliver one by hand from Recent Deliveries for deliveries made in the past 3 days.
It is sent when the webhook has a secret. GitHub takes the request body, computes an HMAC with SHA-256 using your secret as the key, and sends the hex result prefixed with sha256=. Your server repeats the sum and compares.
Many, including push, pull_request, issues, release and workflow_run. The X-GitHub-Event header names the event that triggered each delivery, and ping arrives when you first create the hook.
Your endpoint must be reachable from the internet, because GitHub posts to it. For local work, run a tunnelling tool that gives your machine a temporary public URL, then use that as the Payload URL.
A webhook pushes each change to you as it happens; polling means asking the API on a timer. Webhooks use fewer requests and arrive sooner, which matters because API calls count toward rate limits.
Related: Webhook vs API, Stripe webhooks, Discord webhooks, GitLab webhooks, Rate limiting explained.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.