GitHub webhooks turn repository activity into HTTP requests: a push kicks off a build, an opened pull request runs a policy check, a completed workflow updates a deployment dashboard. They are the backbone of most CI/CD glue and ChatOps, and when they fail the symptom is usually silent — a build that never starts, a status check stuck pending forever. This guide covers consuming GitHub events reliably: verifying them, routing on event type, and recovering the ones that fail.
What GitHub webhooks are
A GitHub webhook is an HTTP POST GitHub sends when a subscribed event happens in a
repository, organisation, or app. The event type travels in the X-GitHub-Event header (push, pull_request, workflow_run), a unique delivery id in X-GitHub-Delivery, and the payload is a JSON body describing what changed. The same
URL receives many event types, so the header — not the body — is how you route.
Common use cases
- CI/CD triggers — start a build or deploy on
pushto a branch, or on aworkflow_runcompleting. - Pull request automation — label, assign, or gate merges from
pull_requestevents; enforce checks before merge. - Release and deployment tracking — record
release,deployment, anddeployment_statusevents against your own systems. - ChatOps and notifications — surface
issues,push, orworkflow_runoutcomes in chat.
Configuring the webhook in GitHub
Webhooks are configured per repository or organisation under Settings → Webhooks, or programmatically for a GitHub App. At a high level you provide the payload URL, set the content type (JSON is the sensible default), optionally set a secret used to sign deliveries, and choose which events to receive — everything, a curated selection, or just pushes. GitHub records every delivery with its request, response, and duration so you can inspect and redeliver from the UI.
Exact screens and options evolve, so confirm current details against GitHub’s webhook documentation rather than
memorising a particular layout. Set a secret — an unsigned GitHub webhook is an
unauthenticated public endpoint anyone can POST to.
Recommended endpoint setup
GitHub expects a prompt response and records slow endpoints as failures, so acknowledge first and work later:
- Capture the raw request body for signature verification before parsing.
- Verify
X-Hub-Signature-256. - Read
X-GitHub-Eventand ignore types you do not handle. - Enqueue the work keyed by
X-GitHub-Delivery, and return2xx. - Run the build, check, or update asynchronously.
Filtering on X-GitHub-Event early keeps a busy repository’s noise out of your
processing path. A HookWatch endpoint’s event-type allowlist can do this filtering at
the edge so your service only receives the types it acts on.
Verifying authenticity
When you configure a secret, GitHub signs each delivery with the X-Hub-Signature-256 header: the literal prefix sha256= followed by the
hex-encoded HMAC-SHA256 of the raw request body, keyed by your secret.
X-Hub-Signature-256: sha256=7d38cdd689735b008b3c702edd92eea23791c5f6d3f3d8b7c... To verify, recompute HMAC-SHA256(secret, raw_body), hex-encode it, prefix it with sha256=, and compare against the header in constant time. Two rules matter: use the
raw bytes (a re-serialised body breaks the hash), and compare in constant time to
avoid leaking timing information. GitHub also sends an older SHA-1 X-Hub-Signature for compatibility — prefer the SHA-256 header and ignore the
SHA-1 one. Worked examples are in how to verify webhook signatures.
HookWatch’s custom inbound signature scheme maps cleanly onto this: header name X-Hub-Signature-256, algorithm SHA-256, hex encoding, prefix sha256=. Configure
that and invalid deliveries are rejected before your handler runs.
Common failure scenarios
- Deliveries showing a red check — GitHub’s Recent Deliveries list marks failed attempts; each one shows the response your endpoint returned, which usually names the cause outright.
- Signature mismatches — a body parser running before verification, or a
mismatched secret after a rotation, produces
401s on otherwise valid events. - Routing on the wrong signal — branching on payload fields instead of
X-GitHub-Eventleads to events silently falling through. - Timeouts on heavy handlers — kicking off a synchronous build in the request causes GitHub to record a timeout and the check to hang.
- Ping confusion — GitHub sends a one-off
pingevent when a webhook is created; a handler that assumes every delivery is a real event can error on it.
Testing workflow
GitHub’s Recent Deliveries page is the primary tool: it shows each delivery’s headers,
payload, and response, and has a Redeliver button that resends the exact payload —
invaluable for reproducing a failure after a fix without waiting for the event to
happen again. Redelivery reuses the original X-GitHub-Delivery id, so your dedupe
logic sees it as the same event.
For local development, expose your handler with a tunnel and point a repository’s
webhook at it so real GitHub deliveries hit your machine. See testing webhooks locally; hookwatch tunnel forwards captured deliveries to a local process, letting GitHub reach your laptop
through a HookWatch endpoint over public TLS.
Retry behaviour
Unlike payment providers, GitHub does not automatically retry a failed webhook delivery — each delivery is attempted once, and recovery is manual. Practically:
- A
2xxis success; anything else (or a timeout) marks the delivery as failed in Recent Deliveries, and that is where it stays. - A failed delivery is not re-sent unless you redeliver it yourself, from the Recent Deliveries UI or the API — so failures need attention, not patience.
- Because redelivery repeats the same
X-GitHub-Delivery, your handler will see duplicates when you do redeliver.
This makes your own recovery story matter more with GitHub than with providers
that retry: a missed deployment or workflow_run event stays missed until
someone notices. The design principles behind retry policies for webhook consumers still
apply — they just have to live on your side of the wire (or in a capture layer
that can replay).
Idempotency
Dedupe on X-GitHub-Delivery, which is unique per delivery and stable across
redeliveries and retries of that delivery. Record handled ids and make the effect
idempotent — starting the same build twice for one push wastes runners and can race.
A duplicate you have already processed should still return 2xx. The patterns are in deduplicating webhook events.
Debugging checklist
- Open Recent Deliveries for the webhook and find the failed attempt.
- Read the response body and status your endpoint returned on that attempt.
- Confirm the handler reads the raw body before any parsing.
- Verify the secret matches and
X-Hub-Signature-256recomputes correctly. - Confirm you route on
X-GitHub-Eventand handle thepingevent gracefully. - Check the handler responds inside GitHub’s timeout with work deferred.
- Match application logs to the delivery by
X-GitHub-Delivery. - Confirm duplicate deliveries are deduped and still answered
2xx. - If the fix is deployed, use Redeliver to reproduce, then verify the effect ran once.
See also GitHub webhooks and a GitHub webhook delivery that failed for GitHub-specific detail, and the general debugging walkthrough.
How HookWatch helps
Route GitHub through a HookWatch endpoint and every delivery is captured whole —
headers including X-GitHub-Event and X-GitHub-Delivery, the body, your response,
and each attempt — so a failed workflow_run still shows exactly what GitHub sent
and what your handler said back. After a fix, replay the captured delivery instead of
hunting for the Redeliver button, pause an endpoint during maintenance and resume it,
and filter to just the event types you act on. Sustained failures roll up into an
incident, so a broken CI hook pages you once with the evidence attached.