Webhook Guides
GitHub Webhook Payload Examples
GitHub webhook payloads are the JSON bodies behind repository automation. They describe what changed, where it changed, who triggered it, and which branch, commit, pull request, issue, release, deployment, or workflow run your receiver should handle.
FastHook stores the payload together with delivery headers, method, path, query, source verification result, routed events, destination attempts, and retry state. That makes payload debugging much easier than scanning receiver logs and guessing whether GitHub sent the request at all.
GitHub payload contract
Every GitHub delivery has two parts you should treat as one contract: provider headers and the event-specific body. Use X-GitHub-Event to choose the payload shape, use X-GitHub-Delivery for provider tracing, and use the body fields for routing and business idempotency.
Common GitHub webhook headers
| Header | Example | Payload use |
|---|---|---|
X-GitHub-Event | push | Choose the payload parser and route family. |
X-GitHub-Delivery | 72d3162e-cc78-11ec-9d64-0242ac120002 | Trace one provider delivery through request, event, attempt, retry, and replay records. |
X-Hub-Signature-256 | sha256=<hex-hmac> | Verify the exact raw request body when a webhook secret is configured. |
X-GitHub-Hook-ID | 292430182 | Identify the webhook configuration that sent the payload. |
Content-Type | application/json | Keep JSON delivery enabled so FastHook filters can match body fields. |
Sample headers from the public GitHub hook
{
"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/*"
}Payload fields worth logging and filtering
Start with a small set of stable fields. Deep payloads are useful, but routing by every possible property makes handlers brittle when GitHub adds fields or sends an action your workflow does not consume.
| Event | Field | Use |
|---|---|---|
all events | repository.full_name | Separate repositories when one endpoint receives many projects. |
all events | sender.login | Record who or what triggered the provider event. |
push | ref | Route branch and tag pushes, such as refs/heads/main or refs/tags/v1.2.3. |
push | before / after | Identify the commit range and build an idempotency key. |
push | head_commit | Inspect the final commit; it can be null for some deletion cases. |
pull_request | action | Route opened, reopened, synchronize, closed, labeled, and review-ready workflows. |
pull_request | pull_request.base.ref | Target branch for preview, checks, and deployment gates. |
pull_request | pull_request.head.sha | Commit that CI, scans, or preview environments should evaluate. |
issues | action | Route opened, edited, labeled, assigned, closed, and reopened issue automation. |
GitHub push event payload example
A push payload identifies the ref, the commit range, whether the ref was created, deleted, or force-pushed, the repository, the pusher, the sender, and the commit list. GitHub caps the commits array in push payloads, so fetch extra commit data from the GitHub API when very large pushes matter.
{
"ref": "refs/heads/main",
"before": "3b5f2c6d1a1f2b8e0d4a9c7f5e6d3c2b1a0f9e8d",
"after": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"base_ref": null,
"created": false,
"deleted": false,
"forced": false,
"compare": "https://github.com/acme/payments-api/compare/3b5f2c6d1a1f...5f6b7a8c9d0e",
"repository": {
"id": 987654321,
"name": "payments-api",
"full_name": "acme/payments-api",
"default_branch": "main"
},
"pusher": {
"name": "dev-user",
"email": "dev@example.com"
},
"sender": {
"login": "dev-user",
"id": 1234567
},
"head_commit": {
"id": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"message": "Add checkout webhook retry guard",
"timestamp": "2026-05-28T09:42:00Z",
"url": "https://github.com/acme/payments-api/commit/5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"author": {
"name": "Dev User",
"email": "dev@example.com"
},
"added": [
"src/webhooks/retry-guard.ts"
],
"removed": [],
"modified": [
"src/webhooks/github.ts"
]
},
"commits": [
{
"id": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"message": "Add checkout webhook retry guard",
"distinct": true,
"added": [
"src/webhooks/retry-guard.ts"
],
"removed": [],
"modified": [
"src/webhooks/github.ts"
]
}
]
}GitHub pull_request payload example
A pull_request payload is usually routed by action, PR number, target branch, source branch, head SHA, repository, and sender. Keep PR automation separate from push automation because the payload shape and business intent are different.
{
"action": "opened",
"number": 42,
"repository": {
"id": 987654321,
"full_name": "acme/payments-api"
},
"pull_request": {
"id": 1789443201,
"number": 42,
"html_url": "https://github.com/acme/payments-api/pull/42",
"title": "Add webhook delivery dashboard",
"state": "open",
"draft": false,
"merged": false,
"user": {
"login": "dev-user"
},
"head": {
"ref": "feature/webhook-dashboard",
"sha": "2c3d4e5f6a7b8c9d0e1f23456789abcdef012345",
"repo": {
"full_name": "acme/payments-api"
}
},
"base": {
"ref": "main",
"sha": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"repo": {
"full_name": "acme/payments-api"
}
}
},
"sender": {
"login": "dev-user"
}
}GitHub issues payload example
The issues payload is useful for ticket routing, incident labels, triage automation, and notifications. Route by action, repository, label names, assignee state, or issue number.
{
"action": "opened",
"issue": {
"id": 20481024,
"number": 1347,
"title": "Webhook delivery failed during deploy",
"state": "open",
"html_url": "https://github.com/acme/payments-api/issues/1347",
"labels": [
{
"name": "incident"
}
],
"user": {
"login": "dev-user"
}
},
"repository": {
"id": 987654321,
"full_name": "acme/payments-api"
},
"sender": {
"login": "dev-user"
}
}FastHook payload setup
FastHook can verify GitHub payloads at the source by comparing X-Hub-Signature-256 against an HMAC-SHA256 digest of the exact raw body. The source HMAC config below is real FastHook behavior: use the GitHub webhook secret, x-hub-signature-256 as the signature header, and sha256= as the prefix. Do not configure a timestamp header for GitHub signatures.
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 payload source",
"description": "Repository events verified with X-Hub-Signature-256",
"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"]
}
}'Send a signed payload smoke test
This is a local test for FastHook source HMAC behavior. It signs the exact JSON string it sends, the same shape GitHub uses for X-Hub-Signature-256.
import crypto from "node:crypto";
const sourceUrl = "https://hook-xxxxxx.fasthook.io/";
const secret = process.env.GITHUB_WEBHOOK_SECRET;
const payload = JSON.stringify({
ref: "refs/heads/main",
after: "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
repository: { full_name: "acme/payments-api" },
head_commit: { message: "Add checkout webhook retry guard" },
sender: { login: "dev-user" }
});
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());Filter by headers and payload fields
Header filters should decide the event family first. Body filters should then match repository, branch, PR target, action, or issue state. When a filter needs alternatives, keep ordinary field checks and $or inside an explicit $and array so the backend matcher evaluates every condition.
{
"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" } } }
]
}
]
}
}Inspect captured payloads in FastHook
Use include=data when you need the stored GitHub payload, headers, method, path, and query string. Accepted requests show payloads that passed source verification. Rejected requests are the place to debug source auth, disabled source, or method problems before an event exists.
List accepted GitHub requests with payload data
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=payments-api" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Open events created from one request
curl "https://api.fasthook.io/v1/requests/req_8bd1e3f2a4c94201/events?limit=20" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"Stored FastHook request example
{
"id": "req_8bd1e3f2a4c94201",
"status": "accepted",
"verified": true,
"rejection_cause": null,
"events_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"
},
"head_commit": {
"message": "Add checkout webhook retry guard"
}
},
"parsed_query": {},
"query": ""
}
}Receiver idempotency example
Use X-GitHub-Delivery for traceability, but also store a business key. A redelivered provider request, a FastHook retry, or a manual replay should not trigger the same deploy, preview environment, or issue mutation twice.
async function handleGitHubWebhook(request) {
const headers = Object.fromEntries(request.headers.entries());
const body = await request.json();
const eventType = headers["x-github-event"];
const deliveryId = headers["x-github-delivery"];
const businessKey = eventType === "push"
? [body.repository?.id, body.ref, body.after].join(":")
: eventType === "pull_request"
? [body.repository?.id, body.pull_request?.id, body.action, body.pull_request?.head?.sha].join(":")
: [body.repository?.id, eventType, deliveryId].join(":");
const inserted = await insertIdempotencyKeyOnce({
provider: "github",
deliveryId,
businessKey
});
if (!inserted) return new Response("duplicate", { status: 200 });
await enqueueRepositoryAutomation({ eventType, deliveryId, body });
return Response.json({ ok: true });
}Public GitHub hook example
The public example endpoint documents the same request shape used in this guide: GitHub Push and PR Events accepts push, pull_request, issues events and stores representative headers and JSON payload examples.
https://hook-xxxxxx.fasthook.io/{
"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",
"timestamp": "2026-05-28T09:42:00Z"
}
}curl -X POST "https://hook-xxxxxx.fasthook.io/" \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-H "X-GitHub-Delivery: 72d3162e-cc78-11ec-9d64-0242ac120002" \
-H "X-Hub-Signature-256: sha256=***redacted***" \
-d '{"ref":"refs/heads/main","after":"5f6b7a8c9d0e1f23456789abcdef0123456789ab","repository":{"full_name":"acme/payments-api"},"head_commit":{"message":"Add checkout webhook retry guard"}}'Production payload checklist
- Subscribe only to GitHub event types your receiver actually handles.
- Keep
Content-Typeasapplication/jsonso payload filters can match body fields. - Verify
X-Hub-Signature-256with the raw body before trusting payload fields. - Route by
X-GitHub-Eventbefore reading event-specific JSON fields. - Use repository and branch fields to prevent one endpoint from triggering the wrong workflow.
- Account for push edge cases: branch deletion, tag push, force push, and
head_commitnull. - Store idempotency keys before starting builds, deploys, preview environments, or issue mutations.
- Use FastHook request data and request events before replaying failed destination work.
GitHub payload FAQ
What is in a GitHub webhook payload?
A GitHub webhook payload contains event-specific JSON plus common objects such as repository and sender. Push payloads include ref, before, after, commits, and head_commit. Pull request payloads include action, number, pull_request, repository, and sender.
Which GitHub webhook header identifies the payload type?
X-GitHub-Event identifies the event family, such as push, pull_request, issues, release, deployment, or workflow_run. Use that header before applying event-specific payload filters.
Can FastHook store GitHub payloads for debugging?
Yes. FastHook stores accepted and rejected request records with method, path, query, headers, and body. Query requests with include=data to inspect the captured GitHub payload and delivery headers.
What fields should GitHub webhook receivers use for idempotency?
Store X-GitHub-Delivery for provider tracing, then combine it with a business key such as repository.id plus ref and after for push events, or pull_request.id plus action and head.sha for pull request events.
GitHub documentation references
GitHub documents delivery headers, payload format, and the 25 MB payload cap in its webhook events and payloads reference. The same reference covers the push and pull_request payload fields. Signature behavior is documented in validating webhook deliveries.