How the webhook URL works. It is a public address that records whatever is sent to it and
answers 200. An inbox keeps the 50 most recent requests, accepts bodies up to 256 KB, and is
deleted 24 hours after it was created — or sooner if you press Delete URL or our server
restarts, because requests are held in memory and never written to disk.
Anyone who has the URL can read what it received. Use sandbox and test-mode accounts, do not route production traffic or customer data through it, and delete it when you are done. Privacy details.
What is a GitHub webhook?
A GitHub webhook is an HTTP POST sent to a URL you configured on a repository, organisation or GitHub App when a subscribed event occurs — a push, a pull request, an issue comment, a release, a workflow run. The event name travels in the X-GitHub-Event header, not in the body, so a handler has to read the header to know what it is looking at.
When a webhook is first created GitHub sends a ping event containing a zen string and the hook's configuration. It is the quickest proof that the URL is reachable, and a handler that only expects push should still answer it with a 2xx.
What a GitHub webhook request looks like
The request line and headers as they reach your server, followed by the body. Ids, tokens and the signature are placeholders.
POST /your/webhook/path HTTP/1.1
Host: your-server.example
X-GitHub-Event: push
X-GitHub-Delivery: 72d3162e-cc78-11e3-81ab-4c9367dc0958
X-Hub-Signature-256: sha256=d57c68ca6f92289e6987922ff26938930f6e66a2d161ef06abdf1859230aa23c
X-Hub-Signature: sha1=7d38cdd689735b008b3c702edd92eea23791c5f6
X-GitHub-Hook-ID: 292430182
X-GitHub-Hook-Installation-Target-Type: repository
Content-Type: application/json
{
"ref": "refs/heads/main",
"before": "6113728f27ae82c7b1a177c8d03f9e96e0adf246",
"after": "59b20b8d5c6ff8d09518454d4dd8b7b30f095ab5",
"repository": {
"id": 1296269,
"name": "hello-world",
"full_name": "octocat/hello-world",
"private": false,
"default_branch": "main"
},
"pusher": {
"name": "octocat",
"email": "octocat@users.noreply.github.com"
},
"sender": {
"login": "octocat",
"id": 1,
"type": "User"
},
"created": false,
"deleted": false,
"forced": false,
"compare": "https://github.com/octocat/hello-world/compare/6113728f27ae...59b20b8d5c6f",
"commits": [
{
"id": "59b20b8d5c6ff8d09518454d4dd8b7b30f095ab5",
"message": "Fix retry backoff in webhook worker",
"timestamp": "2026-01-01T12:00:00Z",
"author": {
"name": "The Octocat",
"email": "octocat@users.noreply.github.com",
"username": "octocat"
},
"added": [],
"removed": [],
"modified": [
"worker/retry.js"
]
}
],
"head_commit": {
"id": "59b20b8d5c6ff8d09518454d4dd8b7b30f095ab5",
"message": "Fix retry backoff in webhook worker"
}
}
This is a trimmed push payload; a real one carries far more repository metadata. A branch deletion is also a push — deleted is true and after is forty zeros — and a push of more than a few thousand commits lists only part of them, so use the Compare API when you need every commit.
Headers GitHub sends
| Header | Example | What it is |
|---|---|---|
X-GitHub-Event | push | Name of the event. The body does not repeat it. |
X-GitHub-Delivery | 72d3162e-cc78-11e3-81ab-4c9367dc0958 | Unique id of this delivery. A manual redelivery reuses it — use it for de-duplication. |
X-Hub-Signature-256 | sha256=d57c68ca6f92289e6987922ff26938930f6e… | HMAC-SHA256 of the body, hex, prefixed sha256=. Present only when a secret is set. |
X-Hub-Signature | sha1=7d38cdd689735b008b3c702edd92eea23791c5f6 | Legacy HMAC-SHA1 of the same body. Ignore it and verify the SHA-256 one. |
X-GitHub-Hook-ID | 292430182 | Id of the webhook configuration that fired. |
X-GitHub-Hook-Installation-Target-Type | repository | Where the hook is installed: repository, organization or integration (a GitHub App). |
Content-Type | application/json | Chosen when the hook is created. The alternative, application/x-www-form-urlencoded, wraps the JSON in a payload field. |
Header names are case-insensitive. Your framework may show them lower-cased, and HTTP/2 always does.
How GitHub signs a webhook
When the webhook has a secret, GitHub computes an HMAC-SHA256 of the request body with that secret and sends it as X-Hub-Signature-256: sha256=<hex>. There is no timestamp in the signed data, so the signature proves origin and integrity but not freshness — de-duplicate on X-GitHub-Delivery if replays matter to you.
Compute it over the raw request body exactly as received — before any JSON parsing — and compare with a constant-time function. With the form-encoded content type, the signed bytes are the whole urlencoded body, not the JSON inside payload.
const crypto = require('crypto');
// rawBody: Buffer or string of the body exactly as received
function verifyGitHub(rawBody, header, secret) {
const expected = 'sha256=' +
crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(header || '');
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Timeouts, failures and redelivery
GitHub expects a 2xx response within ten seconds, per its documentation at the time of writing; a slower reply is recorded as a failed delivery even if your server eventually finishes. Acknowledge first and do the work on a queue.
GitHub does not automatically retry a failed delivery. This surprises people coming from payment providers. A delivery that fails stays failed until someone redelivers it — by hand from the webhook's Recent Deliveries tab, or with the REST API, which is how teams build their own "redeliver everything that failed in the last hour" job.
Catch a real GitHub webhook
- Press Create webhook URL at the top of this page and copy the address.
- In the repository, open Settings → Webhooks → Add webhook.
- Paste the address as the Payload URL, set the content type to
application/json, and enter a throwaway secret if you want to see the signature headers. - Choose the events, save, and watch the
pingdelivery arrive in the inbox above. - Push a commit or open an issue to see a real event payload.
To run a caught request against your own code, select it and press Open in API Tester: the method, headers and body are loaded into the request console, where you enter your server's public URL and send it. A body you edit will no longer match its signature — that is the signature doing its job.
Things that catch people out
- Read the event from the header. Two different events can have near-identical bodies;
X-GitHub-Eventplus the body'sactionfield is the real discriminator. - Payloads are capped. GitHub does not deliver a payload above its size limit, which mostly bites on huge pushes and
createevents with many refs. - An unset secret means no signature at all. A handler that skips verification when the header is missing can be driven by anyone who knows the URL.
api-tester.com is independent and is not affiliated with GitHub. Providers change header names, limits and retry schedules; treat the official GitHub documentation as the source of truth.
Related examples and tools
PayPal Webhook Example
The notification PayPal posts when a payment event happens — its envelope, the five PAYPAL- headers, and the certificate-based signature that works differently from everyone else's HMAC.
Shopify Webhook Example
What Shopify posts when a store event fires — the topic and shop headers, the resource JSON, and a base64 HMAC that trips up people expecting hex.
Zapier Webhook Example
Zapier works in both directions.
Slack Webhook Example
The JSON Slack's Events API posts to your Request URL, the one-time url_verification handshake, and the v0 signature that covers a timestamp as well as the body.
Discord Webhook Example
A Discord webhook runs the other way from most: you send the request, and Discord posts it in a channel.
GitHub API Tester
Call api.github.com from the browser — including the redelivery endpoints.
Curl POST JSON
Send a JSON body like this one from a terminal.
Webhook Tester
The inbox on its own page, plus a guide to signatures, retries and idempotent handlers.
All Webhook Examples
30 providers side by side: content type, signature scheme and retry behaviour.
Other webhooks signed with an HMAC of the body
References
- Official GitHub documentation — The provider's own guide to the requests described on this page.
- GitHub Docs: validating webhook deliveries — The signature check, with examples in several languages.
- GitHub Docs: webhook events and payloads — The payload reference for every event.
- GitHub Docs: handling failed deliveries — How to find and redeliver deliveries that did not succeed.
- RFC 2104: HMAC — The keyed-hash construction this signature is built on.
- Node.js: crypto.timingSafeEqual() — The constant-time comparison used in the code on this page.
These links leave api-tester.com, which is independent of the companies and projects they point to.
GitHub webhook FAQ
What headers does GitHub send with a webhook?
X-GitHub-Event with the event name, X-GitHub-Delivery with a unique delivery id, X-Hub-Signature-256 with the HMAC when a secret is set, the legacy X-Hub-Signature, X-GitHub-Hook-ID, installation target headers, and a User-Agent beginning GitHub-Hookshot/.
How do I verify a GitHub webhook signature?
Compute an HMAC-SHA256 of the raw request body using the webhook secret, hex-encode it, prefix it with sha256= and compare it to X-Hub-Signature-256 with a constant-time comparison.
Does GitHub retry failed webhook deliveries?
No. GitHub records the failure and does not redeliver automatically. You can redeliver manually from the Recent Deliveries tab or through the REST API.
Where is the event type in a GitHub webhook?
In the X-GitHub-Event header, for example push, pull_request or issues. Many events add an action field in the body, such as opened or closed, to narrow it further.
What is the ping event?
A test event GitHub sends when a webhook is created. It contains a zen string, the hook id and the hook configuration, and it confirms that the URL is reachable.