Skip to content
TryWebhook
New URL

Developer tools · Free · No signup

Test and debug GitHub webhooks

Add a throwaway URL to a repository's webhook settings, push something, and read the delivery exactly as GitHub sent it — event name, delivery id, signature, and the parts of the payload you actually need.

No signup · No email · Ready in about a second

At a glance

GitHub webhooks in brief

Category
Developer tools
Where to configure
Repository → Settings → Webhooks → Add webhook
Signature header
X-Hub-Signature-256
Secret
Secret
Retries
None. GitHub delivers once. Failures are listed under Recent Deliveries and can be redelivered by hand or through the REST API — that is the whole retry story.
Response deadline
10 seconds. GitHub is stricter than most and will not wait.
Treated as success
Any 2xx. The response body is ignored.

Cross-checked against GitHub's own webhook documentation. Providers change these; if something here is stale, tell us.

The payload

What GitHub actually sends

Shape-accurate, with obviously fake identifiers. These are the same samples the inspector's Send test webhook panel fires, so you can read one here and then send the identical bytes to your own URL.

POST/w/{your-bin-id}667 B

Includes the delivery id and event headers GitHub sends.

5 headers
user-agent
GitHub-Hookshot/044aadd
x-github-event
push
x-github-delivery
00000000-1111-2222-3333-444444444444
x-github-hook-id
123456789
x-github-hook-installation-target-type
repository
Body · application/json · 1 line
{"ref":"refs/heads/main","before":"0000000000000000000000000000000000000000","after":"a1b2c3d4e5f60718293a4b5c6d7e8f9012345678","repository":{"id":987654321,"name":"example-app","full_name":"acme/example-app","private":true,"default_branch":"main"},"pusher":{"name":"ada","email":"ada@example.com"},"commits":[{"id":"a1b2c3d4e5f60718293a4b5c6d7e8f9012345678","message":"Fix webhook retry backoff","timestamp":"2026-06-15T10:04:00Z","author":{"name":"Ada Lovelace","email":"ada@example.com","username":"ada"},"added":[],"removed":[],"modified":["src/webhooks.ts"]}],"head_commit":{"id":"a1b2c3d4e5f60718293a4b5c6d7e8f9012345678","message":"Fix webhook retry backoff"}}
POST/w/{your-bin-id}503 B

Action-dispatched event; note the `action` discriminator.

3 headers
user-agent
GitHub-Hookshot/044aadd
x-github-event
pull_request
x-github-delivery
11111111-2222-3333-4444-555555555555
Body · application/json · 1 line
{"action":"opened","number":42,"pull_request":{"id":1122334455,"number":42,"state":"open","title":"Add signature verification","user":{"login":"ada","id":1001,"type":"User"},"head":{"ref":"feature/hmac","sha":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"},"base":{"ref":"main","sha":"cafebabecafebabecafebabecafebabecafebabe"},"draft":false,"merged":false,"additions":214,"deletions":12,"changed_files":6},"repository":{"full_name":"acme/example-app","private":true},"sender":{"login":"ada","type":"User"}}

No sample carries a signature header. A valid one can only be produced with your own signing secret, and a fabricated one would make the Signature tab report a mismatch that isn't real. Send a genuine delivery when you want to test verification.

Verification

Verifying a GitHub signature

Signed payload

The literal string `sha256=` followed by a hex HMAC-SHA256 of the raw request body, keyed with the secret you typed into the webhook form.

Implemented as: HMAC-SHA256 over the raw body, hex, prefixed `sha256=`.

Built in. Open any captured request, switch to the Signature tab, pick GitHub, and paste your secret. The HMAC is computed in your browser with the Web Crypto API over the raw bytes that arrived — the secret is never sent to us, and never written to disk.

Events

Events worth subscribing to first

Not the full catalogue — the handful that carry most integrations. Subscribing to everything is the fastest way to a handler that times out.

EventFires when
pushCommits were pushed, or a branch or tag was created or deleted. A deletion arrives as a push with `after` set to all zeros.
pull_requestA pull request changed. Read `action` — `opened`, `synchronize`, `closed`, and about twenty more share this one event name.
issuesAn issue was opened, edited, labelled, closed, or reopened. Same `action` pattern as pull requests.
issue_commentSomeone commented on an issue or a pull request. Pull request review comments are a different event, `pull_request_review_comment`.
workflow_runA GitHub Actions workflow was queued, started, or finished. The `conclusion` field is null until it completes.
releaseA release was published, edited, or deleted. `action: published` is the one that means ship it.

Adding a test URL to a repository

Create a URL above, then go to Settings → Webhooks → Add webhook on any repository you own. Paste it into Payload URL, leave the content type as application/json, type any string into Secret, and choose the events. GitHub sends a ping immediately, so you will see a request appear before you do anything else.

For an organisation-wide hook the path is Organisation settings → Webhooks; for a GitHub App it is Developer settings → GitHub Apps → your app → Webhook URL. The payloads differ in detail but the mechanics on this page are identical.

The ping event

The first delivery is not the event you subscribed to:

{"zen":"Non-blocking is better than blocking.","hook_id":123456789,"hook":{...}}

X-GitHub-Event: ping. It is GitHub’s way of proving the URL resolves. Handle it by returning 200 and nothing else — but do handle it, because a handler that reads payload.repository.full_name unconditionally will 500 on the very first request and the webhook will look broken before it has delivered anything real.

Routing on the header

The body shape tells you almost nothing about which event you have. push has ref and commits; pull_request has action and pull_request; issue_comment has action, issue, and comment. The reliable discriminator is the header:

const event = request.headers.get('x-github-event');
const body = await request.json();

if (event === 'push' && body.ref === 'refs/heads/main') { ... }
if (event === 'pull_request' && body.action === 'synchronize') { ... }

Look at the two samples above with that in mind. The pull_request sample carries action: "opened", and the same event name will arrive again as synchronize on every subsequent push to that branch — which is why CI integrations that only listen for opened never re-run.

Branch deletions look like pushes

A deleted branch arrives as push with after set to forty zeros and an empty commits array. There is a deleted: true flag as well, but the zeros are the part that breaks naive code: anything that does git checkout ${after} will try to check out a commit that does not exist.

Verifying X-Hub-Signature-256

The signed payload is the raw body and nothing else — no timestamp, no delimiter:

X-Hub-Signature-256: sha256=8f3b2c1d...

Strip the sha256= prefix, compute an HMAC-SHA256 of the body bytes with your secret, hex-encode, and compare in constant time. In the Signature tab above, choose GitHub, paste the secret from the webhook form, and it will do the same thing over the bytes that actually arrived.

Because there is no timestamp in the signature, GitHub webhooks are replayable in principle: a captured delivery stays valid forever. If that matters for your endpoint, deduplicate on X-GitHub-Delivery and reject ids you have seen.

When deliveries fail

GitHub does not retry, which makes a failed delivery a lost event rather than a delayed one. The Recent Deliveries tab shows the status your server returned; if it shows a timeout, your handler took more than ten seconds.

The pattern that survives this: verify the signature, write the raw event to a queue or a table, return 200, and process asynchronously. A push to a monorepo can carry hundreds of commits, and the difference between parsing that inline and parsing it in a worker is the difference between an integration that works and one that silently drops every large push.

Failure modes

Where GitHub integrations usually break

The event name is in a header, not the body

`X-GitHub-Event` carries the type. The body has no field that reliably tells you which event you are looking at — some have `action`, some have `ref`, none have `event`. Route on the header, then narrow on `action`.

Two content types, two body formats

The webhook form defaults to `application/json`, but `application/x-www-form-urlencoded` is still offered, and it wraps the whole event in a `payload=` form field. If your handler is finding an empty body with a JSON content type, check which one the webhook was created with.

The SHA-1 header is a trap

`X-Hub-Signature` still ships alongside `X-Hub-Signature-256` for old integrations. Verify the SHA-256 one and ignore the other; SHA-1 HMAC is not where you want your authentication boundary.

A ping arrives first

Creating a webhook sends one `ping` event with a `zen` field and nothing else useful. Handlers that assume every delivery has a `repository` key fail on their very first request.

10 seconds means 10 seconds

GitHub's timeout is tighter than Stripe's or Shopify's. Anything that clones, builds, or calls another API belongs behind a queue, not inside the request.

Answers

GitHub webhook questions

Can I test a GitHub webhook without pushing code?+

Yes. Open Settings → Webhooks → your webhook → Recent Deliveries and press Redeliver on any past delivery — it re-sends the identical payload and signature. The Send test webhook panel here also fires a shape-accurate `push` event with no repository involved.

Why does my signature check fail on GitHub but not on Stripe?+

GitHub prefixes the digest with `sha256=` and signs the body alone, with no timestamp. Stripe signs `timestamp.body` and puts the digest in a `v1=` field. Copying one verifier onto the other provider is the usual cause.

What is X-GitHub-Delivery for?+

It is a UUID unique to the delivery attempt, and it is your idempotency key. Because a redelivery reuses the same id, storing it also protects you from processing a manual redelivery twice.

Does this work for GitHub App webhooks?+

Yes. App webhooks go to a single URL configured in the App settings rather than per repository, and they add `X-GitHub-Hook-Installation-Target-Type` and an `installation` object to the payload. Everything else, including the signature scheme, is the same.

Can I see what a failing delivery returned?+

GitHub shows the response status and body under Recent Deliveries. What it does not show clearly is what your server received — which is what this page is for. Point the webhook here, compare, then point it back.

Other providers

Testing something else?

One click

Point GitHub at a URL and watch the payload land.

Repository → Settings → Webhooks → Add webhook — paste the URL, trigger an event, and read exactly what arrived.

No signup · No email · Ready in about a second