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.

GitHub webhook payload map showing delivery headers, JSON body, FastHook request data, filters, and receiver idempotency.
A GitHub payload is not just the JSON body. Headers choose the event family and trace the provider delivery; body fields drive filters, automation, idempotency, and debugging.

Common GitHub webhook headers

HeaderExamplePayload use
X-GitHub-EventpushChoose the payload parser and route family.
X-GitHub-Delivery72d3162e-cc78-11ec-9d64-0242ac120002Trace one provider delivery through request, event, attempt, retry, and replay records.
X-Hub-Signature-256sha256=<hex-hmac>Verify the exact raw request body when a webhook secret is configured.
X-GitHub-Hook-ID292430182Identify the webhook configuration that sent the payload.
Content-Typeapplication/jsonKeep JSON delivery enabled so FastHook filters can match body fields.

Sample headers from the public GitHub hook

Sample GitHub 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/*"
}

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.

EventFieldUse
all eventsrepository.full_nameSeparate repositories when one endpoint receives many projects.
all eventssender.loginRecord who or what triggered the provider event.
pushrefRoute branch and tag pushes, such as refs/heads/main or refs/tags/v1.2.3.
pushbefore / afterIdentify the commit range and build an idempotency key.
pushhead_commitInspect the final commit; it can be null for some deletion cases.
pull_requestactionRoute opened, reopened, synchronize, closed, labeled, and review-ready workflows.
pull_requestpull_request.base.refTarget branch for preview, checks, and deployment gates.
pull_requestpull_request.head.shaCommit that CI, scans, or preview environments should evaluate.
issuesactionRoute 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.

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"
    },
    "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.

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

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

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

Signed payload 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",
  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.

Payload filter rule
{
  "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" } } }
        ]
      }
    ]
  }
}
GitHub webhook payload debugging flow from provider delivery to FastHook request data, connection filters, receiver idempotency, and replay.
Payload debugging starts with the stored request. Confirm the event header and body, then check filter match, destination attempt, idempotency, and replay behavior.

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

Inspect payload 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=payments-api" \
  -H "Authorization: Bearer fhp_xxx" \
  -H "x-team-id: tm_3b5335b627084a838b"

Open events created from one request

List request events
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

Stored request JSON
{
  "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.

Receiver idempotency
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.

Public GitHub source URL
https://hook-xxxxxx.fasthook.io/
Public hook payload
{
  "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"
  }
}
Public hook curl
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-Type as application/json so payload filters can match body fields.
  • Verify X-Hub-Signature-256 with the raw body before trusting payload fields.
  • Route by X-GitHub-Event before 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_commit null.
  • 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.

Related guides