Webhook Guides
GitHub Push Event Payload
The GitHub push event payload is the webhook body behind branch deployments, tag releases, changelog jobs, repository mirrors, preview cleanup, and internal automation. It tells your receiver which Git ref changed, which commit range GitHub saw, whether the ref was created or deleted, and which repository sent the event.
FastHook is useful for push payloads because a single GitHub source URL can verify the raw GitHub signature, store the request, route only the branches or tags you want, preserve provider delivery headers, and keep request, event, attempt, retry, and replay evidence together.
GitHub push payload contract
Treat every push delivery as two connected objects: delivery headers and the JSON payload. Use X-GitHub-Event to confirm the event is push, use X-GitHub-Delivery for provider tracing, verify X-Hub-Signature-256 against the exact raw body, then route on payload fields such as ref, repository.full_name,deleted, forced, and after.
Push event headers
GitHub sends push webhooks as HTTP POST requests. These headers are the first fields to inspect in FastHook when a deployment or release job did not run.
| Header | Example | Use |
|---|---|---|
X-GitHub-Event | push | Choose the push parser before reading push-specific JSON fields. |
X-GitHub-Delivery | 72d3162e-cc78-11ec-9d64-0242ac120002 | Trace one provider delivery through request, event, attempt, retry, and replay evidence. |
X-Hub-Signature-256 | sha256=<hex-hmac> | Verify the exact raw body with the GitHub webhook secret. |
X-GitHub-Hook-ID | 292430182 | Identify the GitHub webhook configuration that sent the delivery. |
Content-Type | application/json | Keep JSON payload delivery enabled so FastHook can filter body fields. |
POST /github HTTP/1.1
Host: hook-xxxxxx.fasthook.io
User-Agent: GitHub-Hookshot/1234567
Content-Type: application/json
X-GitHub-Event: push
X-GitHub-Delivery: 72d3162e-cc78-11ec-9d64-0242ac120002
X-Hub-Signature-256: sha256=<hex-hmac-sha256>Example GitHub push payload
This is a practical push payload shape for a normal commit push to main. The exact payload can include more repository, organization, installation, and enterprise fields depending on how the webhook is configured.
{
"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"
},
"committer": {
"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,
"timestamp": "2026-05-28T09:42:00Z",
"added": [
"src/webhooks/retry-guard.ts"
],
"removed": [],
"modified": [
"src/webhooks/github.ts"
]
}
]
}Important push payload fields
Most production push handlers only need a small stable set of fields. Log these fields, route on them, and avoid treating optional commit text as a security boundary.
| Field | Example | How to use it |
|---|---|---|
ref | refs/heads/main | Full Git ref. Use refs/heads/ for branches and refs/tags/ for tags. |
before | 3b5f2c6d... | Commit SHA before the push. Useful for compare links and change windows. |
after | 5f6b7a8c... | Commit SHA after the push. A natural business id for branch and tag automation. |
created | false | True when the ref was created, such as a new branch or tag. |
deleted | false | True when the ref was deleted. Deletion payloads can have head_commit as null. |
forced | false | True when the push rewrote history. Treat deployment and cache logic carefully. |
repository.full_name | acme/payments-api | Separate repositories when one endpoint receives many projects. |
head_commit.message | Add retry guard | Useful for logs and optional skip rules, not authorization. |
commits | [...] | Commit list included in the delivery. Fetch extra details from GitHub for very large pushes. |
Branch deletes, tags, and force pushes
Push payloads are not always deployment signals. A deleted branch, a tag release, or a force push should usually take a different path than a normal branch update.
| Case | Payload signal | Receiver behavior |
|---|---|---|
| Normal branch push | ref starts with refs/heads/ | Build, deploy, test, or sync branch state. |
| Tag push | ref starts with refs/tags/ | Release, publish, or notify package workflows. |
| Ref deletion | deleted is true | Run cleanup work only; do not assume head_commit exists. |
| Force push | forced is true | Invalidate caches and use after as the new branch tip. |
| Very large push | commits can be capped | Use the GitHub API when complete commit detail matters. |
Branch deletion payload
For delete events, code defensively: head_commit can be null and commits can be empty. Cleanup work should be separate from deploy work.
{
"ref": "refs/heads/old-preview",
"before": "8a7b6c5d4e3f2a109876543210fedcba98765432",
"after": "0000000000000000000000000000000000000000",
"created": false,
"deleted": true,
"forced": false,
"repository": {
"id": 987654321,
"full_name": "acme/payments-api",
"default_branch": "main"
},
"pusher": {
"name": "dev-user",
"email": "dev@example.com"
},
"sender": {
"login": "dev-user"
},
"head_commit": null,
"commits": []
}Tag push payload
Tag pushes use the same push event header, but the ref starts with refs/tags/. Route release automation separately from branch deployment automation.
{
"ref": "refs/tags/v1.2.3",
"before": "0000000000000000000000000000000000000000",
"after": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"created": true,
"deleted": false,
"forced": false,
"repository": {
"id": 987654321,
"full_name": "acme/payments-api"
},
"head_commit": {
"id": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"message": "Release v1.2.3"
},
"commits": []
}Verify GitHub push signatures in FastHook
GitHub signs the exact raw request body with HMAC-SHA256 and sends the result in X-Hub-Signature-256 with a sha256= prefix. FastHook source HMAC auth supports this shape directly: configure signature_header as x-hub-signature-256, set prefix to sha256=, and do not configure a timestamp header for GitHub.
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 push source",
"description": "GitHub push 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"],
"custom_response": {
"content_type": "json",
"body": "{\"ok\":true}"
}
}
}'Send a signed push smoke test
This Node example is only a smoke test for your FastHook source configuration. It signs the exact JSON string it sends, matching the GitHub X-Hub-Signature-256 format.
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",
before: "3b5f2c6d1a1f2b8e0d4a9c7f5e6d3c2b1a0f9e8d",
after: "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
created: false,
deleted: false,
forced: false,
repository: {
id: 987654321,
full_name: "acme/payments-api"
},
sender: { login: "dev-user" },
head_commit: {
id: "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
message: "Add checkout webhook retry guard"
},
commits: []
});
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 GitHub push events in FastHook
Connection filters run after FastHook accepts the source request. Filter first by x-github-event, then by repository, branch, tag, delete flag, or force-push flag. Keep complex alternatives inside an explicit $and array so every condition is evaluated by the backend filter matcher.
Deliver only main branch pushes
{
"type": "filter",
"headers": {
"x-github-event": "push"
},
"body": {
"ref": "refs/heads/main",
"deleted": false,
"repository": {
"full_name": "acme/payments-api"
}
}
}Deliver main branch and release tag pushes
{
"type": "filter",
"headers": {
"x-github-event": "push"
},
"body": {
"$and": [
{
"repository": {
"full_name": "acme/payments-api"
}
},
{
"$or": [
{
"ref": "refs/heads/main"
},
{
"ref": {
"$starts_with": "refs/tags/v"
}
}
]
},
{
"deleted": false
}
]
}
}Inspect captured push payloads
Use include=data when you need the stored GitHub headers and JSON body. Accepted requests prove the source URL received and verified the push. Rejected requests are where you debug source auth, disabled source, or method problems before any destination event exists.
curl -G "https://api.fasthook.io/v1/requests" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b" \
--data-urlencode "source_id=src_q6z62b6py5o79b" \
--data-urlencode "status=accepted" \
--data-urlencode "verified=true" \
--data-urlencode "from=now-24h" \
--data-urlencode "to=now" \
--data-urlencode "limit=20" \
--data-urlencode "include=data" \
--data-urlencode "q=refs/heads/main"curl "https://api.fasthook.io/v1/requests/req_7Jm4Q8/events?include=attempts" \
-H "Authorization: Bearer fhp_xxx" \
-H "x-team-id: tm_3b5335b627084a838b"{
"id": "req_7Jm4Q8",
"source_id": "src_q6z62b6py5o79b",
"status": "accepted",
"verified": true,
"method": "POST",
"path": "/",
"rejection_cause": null,
"data": {
"headers": {
"x-github-event": "push",
"x-github-delivery": "72d3162e-cc78-11ec-9d64-0242ac120002",
"x-hub-signature-256": "sha256=<redacted>"
},
"body": {
"ref": "refs/heads/main",
"after": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
"deleted": false,
"forced": false,
"repository": {
"id": 987654321,
"full_name": "acme/payments-api"
}
},
"parsed_query": {},
"query": "",
"path": "/",
"method": "POST"
}
}Receiver idempotency for push payloads
GitHub redelivery, FastHook retry, and manual replay can make the same push reach your receiver more than once. Store an idempotency row before triggering side effects. Use X-GitHub-Delivery for tracing and a business key such as repository.id + ref + after for the actual duplicate guard.
export async function handleGitHubPush(request) {
const deliveryId = request.headers.get("x-github-delivery");
const event = request.headers.get("x-github-event");
const payload = await request.json();
if (event !== "push") {
return new Response("ignored", { status: 202 });
}
const repoId = payload.repository?.id;
const ref = payload.ref;
const after = payload.after;
const businessKey = `github:push:${repoId}:${ref}:${after}`;
const inserted = await idempotencyStore.insertOnce({
key: businessKey,
providerDeliveryId: deliveryId
});
if (!inserted) {
return Response.json({ duplicate: true });
}
if (payload.deleted) {
await cleanupRefPreview({ repoId, ref });
return Response.json({ queued: "cleanup" });
}
await queueDeployment({
repoId,
ref,
sha: after,
forced: Boolean(payload.forced),
message: payload.head_commit?.message ?? ""
});
return Response.json({ queued: "deployment" });
}Production checklist
- Subscribe to the GitHub
pushevent only when your receiver handles branch or tag changes. - Use
application/jsoncontent type so FastHook can filter payload body fields. - Configure a GitHub webhook secret and verify
X-Hub-Signature-256at ingress. - Filter by
x-github-event,repository.full_name,ref, and delete state. - Handle
deleted,forced, tag refs, andhead_commitnull before deploying. - Use
X-GitHub-Deliveryand a business id before starting builds or release work. - Use FastHook request records with
include=databefore replaying failed destination work. - Fetch extra commit details from GitHub when a large push exceeds the payload commit detail you need.
GitHub push payload FAQ
What is a GitHub push event payload?
A GitHub push event payload is the JSON webhook body GitHub sends when a branch or tag ref changes. It includes ref, before, after, created, deleted, forced, repository, pusher, sender, commits, and head_commit fields.
Which GitHub push payload field identifies the branch?
The ref field identifies the full Git ref. Branch pushes use refs/heads/<branch>, while tag pushes use refs/tags/<tag>.
Can FastHook verify GitHub push webhook signatures?
Yes. FastHook source HMAC auth verifies an HMAC-SHA256 hex digest over the exact raw body. Configure signature_header as x-hub-signature-256 and prefix as sha256= to match GitHub X-Hub-Signature-256.
What should a push webhook receiver use for idempotency?
Store X-GitHub-Delivery for provider tracing, then store a business key such as repository.id plus ref plus after before triggering builds, deploys, release jobs, or cleanup work.
GitHub documentation references
GitHub documents delivery headers and the 25 MB webhook payload cap in its webhook events and payloads reference. The same reference documents the push event payload, including the 2048 commit array limit and push edge cases for many branch or tag refs. GitHub signature behavior is documented in validating webhook deliveries.