Test GitHub webhooks without deploying
Capture push, pull request and release events from a repository or organisation, and verify X-Hub-Signature-256.
Get a URL that accepts the webhook#
A capture URL takes any method, any body, and records exactly what arrived: method, headers, query string, body and source IP. You do not need an account to make one, and an anonymous URL is deleted after 24 hours.
curl -X POST https://mocklane.com/api/v1/anon/endpoints \
-H "Content-Type: application/json" -d '{}'
# The response carries a capture URL of the form:
# https://mocklane.com/h/<endpoint-id>Paste that URL into GitHub at Repository or organisation Settings, Webhooks, Add webhook, then trigger something. Events worth starting with:
pushpull_requestissuesreleaseworkflow_run
Verify the X-Hub-Signature-256 header#
GitHub signs each request so you can tell a real delivery from anyone who has guessed your URL. The digest is HMAC-SHA256 over the raw body, hex-encoded, keyed with your secret, set on the webhook itself.
- Header
X-Hub-Signature-256- Value format
sha256=<hex digest>- Algorithm
- HMAC-SHA256, hex
- Signed content
- the raw body
const crypto = require("crypto");
// The digest is prefixed, so build the whole header value and compare that.
const expected =
"sha256=" +
crypto
.createHmac("sha256", process.env.GITHUB_WEBHOOK_SECRET)
.update(rawBody)
.digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(req.headers["x-hub-signature-256"]),
);crypto.timingSafeEqual. A plain === returns as soon as two bytes differ, and that timing difference is enough to recover a valid signature one byte at a time.What catches people out#
- X-Hub-Signature, without the -256, still exists and uses SHA-1. It is kept for legacy consumers only. Verify the SHA-256 header.
- The value includes the sha256= prefix. Comparing a bare hex digest against the header will never match.
- A webhook with no secret configured is not signed at all, and GitHub will not warn you about it.
- The payload must be hashed exactly as received, as UTF-8, with no reformatting.
Replay it until your handler is right#
A captured request can be re-sent to your own endpoint, with the body or headers edited first. That turns a bug that needed a real GitHub event into something you can reproduce on demand, which matters most for events that are awkward to trigger twice.
Assert on it in CI#
A scenario chains the steps into one run that passes or fails, so a change that breaks your GitHub handling fails the build rather than reaching production.
steps:
- type: send_webhook
body: { event: "push" }
- type: wait_for_webhook
timeout_seconds: 30
assert:
- status_code equals 200Triggered from CI with an API token. See the scenarios documentation for the full step list.
Signature details checked against GitHub’s documentation on 22 October 2026. Schemes change: if this page and their docs disagree, believe theirs and tell us.