Webhook Guides
GitHub Webhooks Guide
GitHub webhooks notify your systems when repository and organization activity happens. Teams use them for CI/CD triggers, deploy gates, pull request automation, issue routing, release workflows, security scans, and internal developer tools.
FastHook gives GitHub a stable source URL, verifies X-Hub-Signature-256 at ingress, records the inbound request, routes accepted events through connection rules, forwards GitHub provider headers, and keeps request, event, attempt, retry, and replay evidence together. The examples below match the current FastHook source HMAC, request inspection, connection filter, and destination signature behavior.
How GitHub webhook delivery works
GitHub sends an HTTP POST request for events like push, pull_request, issues, release, deployment, and workflow_run. The request includes delivery headers, a JSON payload, and, when a secret is set, an X-Hub-Signature-256 header. GitHub signs the exact raw request body with HMAC-SHA256 and prefixes the hex digest with sha256=.
Treat GitHub webhooks as at-least-once signals. Store the delivery id and a business key before triggering automation, because redelivery, manual replay, FastHook retries, or receiver timeouts can make the same repository event appear more than once.
GitHub webhook headers
GitHub delivery headers are the fastest way to identify the event family, trace a provider delivery, and verify the request. FastHook stores these headers with the request record and forwards provider headers after removing FastHook control, hop-by-hop, and edge network headers.
| Header | Example | Use |
|---|---|---|
X-GitHub-Event | push | Route by event family before inspecting deep payload fields. |
X-GitHub-Delivery | 72d3162e-cc78-11ec-9d64-0242ac120002 | Track the provider delivery id for logs and idempotency. |
X-Hub-Signature-256 | sha256=<hex-hmac> | Verify the raw request body with the GitHub webhook secret. |
X-GitHub-Hook-ID | 123456789 | Identify which GitHub webhook configuration sent the request. |
User-Agent | GitHub-Hookshot/* | Useful for debugging, not a trust boundary. |
FastHook setup for GitHub webhooks
The FastHook-specific part is a source with HMAC auth. Configure the source secret to match the GitHub webhook secret, use x-hub-signature-256 as the signature header, and use sha256= as the prefix. Do not configure a timestamp header for GitHub signatures, because GitHub signs only the raw payload body for this header.
Create a GitHub source with source HMAC verification
curl -X POST "https://api.fasthook.io/v1/sources" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "GitHub repository events",
"description": "GitHub push and pull request webhook source",
"type": "WEBHOOK",
"status": "enabled",
"config": {
"auth_type": "HMAC",
"auth": {
"secret": "github_webhook_secret",
"signature_header": "x-hub-signature-256",
"prefix": "sha256="
},
"allowed_http_methods": ["POST"],
"custom_response": {
"content_type": "json",
"body": "{\"ok\":true}"
}
}
}'Configure the webhook in GitHub
Paste the generated FastHook source URL into the repository, organization, or GitHub App webhook settings. Select only the events the integration consumes, and use the same secret value that you configured on the FastHook source.
Payload URL: https://hook-xxxxxx.fasthook.io/
Content type: application/json
Secret: github_webhook_secret
SSL verification: Enable SSL verification
Events: Pushes, Pull requests, Issues, or the exact events your workflow consumes
Active: checkedCreate the automation destination
The destination example enables FastHook destination signatures. That signature is separate from GitHub source verification: GitHub signs the request into FastHook, then FastHook signs the delivery into your destination.
curl -X POST "https://api.fasthook.io/v1/destinations" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "Repository automation API",
"type": "HTTP",
"config": {
"url": "https://api.example.com/webhooks/github",
"http_method": "POST",
"path_forwarding_disabled": false,
"auth_type": "FASTHOOK_SIGNATURE",
"auth": {}
}
}'Route push and pull request events
Use header filters for the event family and body filters for repository, branch, PR target, or workflow fields. FastHook evaluates connection filters after the request has been accepted by the source.
curl -X POST "https://api.fasthook.io/v1/connections" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
-H "Content-Type: application/json" \
-d '{
"name": "github-main-automation",
"source_id": "src_q6z62b6py5o79b",
"destination_id": "des_TU9ioCk5EHUU",
"rules": [
{
"type": "filter",
"headers": {
"x-github-event": { "$in": ["push", "pull_request"] }
},
"body": {
"$and": [
{ "repository": { "full_name": "acme/payments-api" } },
{
"$or": [
{ "ref": "refs/heads/main" },
{ "pull_request": { "base": { "ref": "main" } } }
]
}
]
}
},
{
"type": "retry",
"strategy": "exponential",
"count": 5,
"interval": 60000,
"response_status_codes": ["429", "500-599"]
}
]
}'GitHub signature and payload examples
A valid GitHub signature is calculated from the exact raw request body. The Node smoke test below signs a sample payload the same way GitHub signs webhook deliveries, so it can exercise FastHook source HMAC locally without pretending to be a real GitHub delivery.
Send a signed GitHub-style push event
import crypto from "node:crypto";
const secret = process.env.GITHUB_WEBHOOK_SECRET;
const sourceUrl = "https://hook-xxxxxx.fasthook.io/";
const payload = JSON.stringify({
ref: "refs/heads/main",
before: "3b5f2c6d1a1f2b8e0d4a9c7f5e6d3c2b1a0f9e8d",
after: "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
repository: {
id: 987654321,
name: "payments-api",
full_name: "acme/payments-api"
},
pusher: { name: "dev-user" },
sender: { login: "dev-user" },
head_commit: {
id: "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
message: "Add checkout webhook retry guard"
}
});
const signature = "sha256=" + crypto
.createHmac("sha256", secret)
.update(payload)
.digest("hex");
const response = await fetch(sourceUrl, {
method: "POST",
headers: {
"content-type": "application/json",
"x-github-event": "push",
"x-github-delivery": crypto.randomUUID(),
"x-hub-signature-256": signature,
"user-agent": "GitHub-Hookshot/test"
},
body: payload
});
console.log(response.status, await response.text());GitHub push payload example
{
"ref": "refs/heads/main",
"before": "3b5f2c6d1a1f2b8e0d4a9c7f5e6d3c2b1a0f9e8d",
"after": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"repository": {
"id": 987654321,
"name": "payments-api",
"full_name": "acme/payments-api",
"default_branch": "main"
},
"pusher": {
"name": "dev-user"
},
"sender": {
"login": "dev-user"
},
"head_commit": {
"id": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"message": "Add checkout webhook retry guard",
"timestamp": "2026-05-28T09:42:00Z"
}
}GitHub pull request payload example
{
"action": "opened",
"number": 42,
"repository": {
"full_name": "acme/payments-api"
},
"pull_request": {
"id": 1789443201,
"number": 42,
"title": "Add retry guard to checkout webhook",
"state": "open",
"base": {
"ref": "main",
"repo": {
"full_name": "acme/payments-api"
}
},
"head": {
"ref": "retry-guard",
"sha": "5f6b7a8c9d0e1f23456789abcdef0123456789ab"
}
},
"sender": {
"login": "dev-user"
}
}How GitHub HMAC verification works
This snippet mirrors the HMAC shape that FastHook source auth verifies. FastHook performs this check at the source when auth_type is HMAC; your downstream receiver can still verify GitHub provider signatures if you forward the original body and headers without transformation.
import crypto from "node:crypto";
function verifyGitHubSignature({ rawBody, signature, secret }) {
if (!signature?.startsWith("sha256=")) return false;
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const actualBytes = Buffer.from(signature);
const expectedBytes = Buffer.from(expected);
return actualBytes.length === expectedBytes.length &&
crypto.timingSafeEqual(actualBytes, expectedBytes);
}
// FastHook source HMAC can do this at ingress by configuring:
// auth_type: "HMAC"
// auth.signature_header: "x-hub-signature-256"
// auth.prefix: "sha256="
// Do not configure a timestamp header for GitHub signatures.Verify FastHook destination signatures
Use FastHook destination signatures when your receiver needs to prove that a delivery came from FastHook, especially after transformations or internal replays where GitHub's original signature is no longer the right verification boundary.
import crypto from "node:crypto";
function verifyFastHookSignature({ rawBody, timestamp, signature, signingSecret }) {
const timestampSeconds = Number(timestamp);
const nowSeconds = Math.floor(Date.now() / 1000);
if (!Number.isSafeInteger(timestampSeconds)) return false;
if (Math.abs(nowSeconds - timestampSeconds) > 5 * 60) return false;
if (!signature?.startsWith("v1=")) return false;
const expected = "v1=" + crypto
.createHmac("sha256", signingSecret)
.update(timestamp + "." + rawBody)
.digest("hex");
const actualBytes = Buffer.from(signature || "");
const expectedBytes = Buffer.from(expected);
return actualBytes.length === expectedBytes.length &&
crypto.timingSafeEqual(actualBytes, expectedBytes);
}
// FastHook destination signatures use:
// x-fasthook-timestamp: Unix timestamp in seconds
// x-fasthook-signature: v1=<hex-hmac-sha256>Inspect GitHub webhook traffic in FastHook
When GitHub shows a recent delivery but your automation did not run, start with the request record. Accepted requests prove the source URL received and verified the webhook. Rejected requests explain source auth, method, or source state failures. Request events show which connections created destination deliveries.
Inspect accepted GitHub requests
curl "https://api.fasthook.io/v1/requests?source_id=src_q6z62b6py5o79b&status=accepted&verified=true&from=now-24h&to=now&limit=20&include=data&q=push" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Inspect rejected GitHub requests
curl "https://api.fasthook.io/v1/requests?source_id=src_q6z62b6py5o79b&status=rejected&from=now-24h&to=now&limit=20&include=data" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Open one request with stored headers and payload
curl "https://api.fasthook.io/v1/requests/req_8bd1e3f2a4c94201" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"List events created from a request
curl "https://api.fasthook.io/v1/requests/req_8bd1e3f2a4c94201/events?limit=20" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Inspect failed destination events
curl "https://api.fasthook.io/v1/events?source_id=src_q6z62b6py5o79b&status=FAILED&limit=20&q=github" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Retry a failed destination event
curl -X POST "https://api.fasthook.io/v1/events/evt_01JYGITHUB/retry" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Accepted request with stored GitHub data
{
"id": "req_8bd1e3f2a4c94201",
"source_id": "src_q6z62b6py5o79b",
"status": "accepted",
"verified": true,
"rejection_cause": null,
"event_count": 1,
"data": {
"method": "POST",
"path": "/",
"headers": {
"content-type": "application/json",
"x-github-event": "push",
"x-github-delivery": "72d3162e-cc78-11ec-9d64-0242ac120002",
"x-hub-signature-256": "sha256=***redacted***",
"user-agent": "GitHub-Hookshot/*"
},
"body": {
"ref": "refs/heads/main",
"after": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"repository": {
"full_name": "acme/payments-api"
},
"sender": {
"login": "dev-user"
}
}
}
}Rejected request from a bad GitHub signature
{
"id": "req_9b2e4f9a12d84021",
"source_id": "src_q6z62b6py5o79b",
"status": "rejected",
"verified": false,
"rejection_cause": "SOURCE_AUTH_FAILED",
"event_count": 0,
"data": {
"method": "POST",
"path": "/",
"headers": {
"x-github-event": "push",
"x-github-delivery": "b4f6d316-cc78-11ec-9d64-0242ac120002",
"x-hub-signature-256": "sha256=wrong"
},
"body": {
"ref": "refs/heads/main",
"repository": {
"full_name": "acme/payments-api"
}
}
}
}GitHub webhook production checklist
- Use a separate FastHook source for each environment and GitHub producer boundary.
- Configure GitHub with
application/json, a webhook secret, SSL verification, and only needed event types. - Verify
X-Hub-Signature-256against the raw body before routing the request. - Filter by
x-github-event, repository, branch, PR base, workflow name, or deployment environment. - Use
X-GitHub-Deliveryand a business id before triggering builds, deploys, or mutations. - Return success only after the destination has durably recorded the minimum work it needs.
- Use GitHub redelivery for provider-side tests and FastHook retry or replay for downstream destination recovery.
- Use FastHook request records with
include=datato inspect rejected signatures and accepted payloads.
GitHub webhook FAQ
How do GitHub webhooks work?
GitHub sends an HTTP POST request to a webhook URL when a selected event occurs, such as push, pull_request, issues, deployment, workflow_run, or release. The request includes delivery headers, a JSON payload, and X-Hub-Signature-256 when a secret is configured.
Can FastHook verify GitHub X-Hub-Signature-256?
Yes. FastHook source HMAC auth verifies an HMAC-SHA256 hex digest over the raw request body. Configure signature_header as x-hub-signature-256 and prefix as sha256=, using the same secret configured in GitHub.
Should a GitHub webhook receiver process duplicate deliveries?
No. Treat GitHub webhook delivery as at-least-once. Use X-GitHub-Delivery plus a business key such as repository full name, commit SHA, pull request id, or workflow run id before triggering deploys or mutating state.
Does GitHub automatically redeliver failed webhook deliveries?
GitHub lets admins redeliver recent webhook deliveries from the GitHub UI or API, but failed deliveries should not be treated as automatic provider retries. FastHook retries and replay apply to the downstream destination delivery after FastHook has accepted the request.
GitHub documentation references
GitHub documents delivery headers in its webhook events and payloads reference, signature validation in validating webhook deliveries, and manual redelivery behavior in redelivering webhooks.
Try FastHook with GitHub
Create a GitHub source, paste the source URL into GitHub, send a signed test event, inspect the accepted request, route it to an automation destination, and retry failed delivery attempts after the receiver is fixed.