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.

GitHub push payload anatomy showing delivery headers, push JSON body fields, FastHook source verification, routing filters, receiver idempotency, and replay evidence.
A push payload becomes reliable when headers, raw-body verification, body fields, filters, and receiver idempotency are treated as one delivery contract.

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.

HeaderExampleUse
X-GitHub-EventpushChoose the push parser before reading push-specific JSON fields.
X-GitHub-Delivery72d3162e-cc78-11ec-9d64-0242ac120002Trace one provider delivery through request, event, attempt, retry, and replay evidence.
X-Hub-Signature-256sha256=<hex-hmac>Verify the exact raw body with the GitHub webhook secret.
X-GitHub-Hook-ID292430182Identify the GitHub webhook configuration that sent the delivery.
Content-Typeapplication/jsonKeep JSON payload delivery enabled so FastHook can filter body fields.
Push delivery headers
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.

Push payload JSON
{
  "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.

FieldExampleHow to use it
refrefs/heads/mainFull Git ref. Use refs/heads/ for branches and refs/tags/ for tags.
before3b5f2c6d...Commit SHA before the push. Useful for compare links and change windows.
after5f6b7a8c...Commit SHA after the push. A natural business id for branch and tag automation.
createdfalseTrue when the ref was created, such as a new branch or tag.
deletedfalseTrue when the ref was deleted. Deletion payloads can have head_commit as null.
forcedfalseTrue when the push rewrote history. Treat deployment and cache logic carefully.
repository.full_nameacme/payments-apiSeparate repositories when one endpoint receives many projects.
head_commit.messageAdd retry guardUseful 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.

CasePayload signalReceiver behavior
Normal branch pushref starts with refs/heads/Build, deploy, test, or sync branch state.
Tag pushref starts with refs/tags/Release, publish, or notify package workflows.
Ref deletiondeleted is trueRun cleanup work only; do not assume head_commit exists.
Force pushforced is trueInvalidate caches and use after as the new branch tip.
Very large pushcommits can be cappedUse 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.

Branch delete payload
{
  "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.

Tag push payload
{
  "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": []
}
GitHub push routing matrix showing branch pushes, tag pushes, branch deletes, force pushes, and large push handling through FastHook filters and receiver decisions.
Route push payloads by intent. Branch pushes, tag pushes, deletes, force pushes, and very large pushes need different receiver behavior even though they share the same GitHub event header.

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.

Create GitHub push source
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.

Signed push test
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

Main branch filter
{
  "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

Branch or tag filter
{
  "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.

Inspect push requests
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"
List request events
curl "https://api.fasthook.io/v1/requests/req_7Jm4Q8/events?include=attempts" \
  -H "Authorization: Bearer fhp_xxx" \
  -H "x-team-id: tm_3b5335b627084a838b"
Stored request JSON
{
  "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.

Push receiver idempotency
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 push event only when your receiver handles branch or tag changes.
  • Use application/json content type so FastHook can filter payload body fields.
  • Configure a GitHub webhook secret and verify X-Hub-Signature-256 at ingress.
  • Filter by x-github-event, repository.full_name, ref, and delete state.
  • Handle deleted, forced, tag refs, and head_commit null before deploying.
  • Use X-GitHub-Delivery and a business id before starting builds or release work.
  • Use FastHook request records with include=data before 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.

Related guides