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 delivery flow through FastHook source HMAC verification, request record, connection filters, automation destination, attempts, and replay.
GitHub sends repository events to a FastHook source URL. Source HMAC verifies the raw body, accepted requests route through connection filters, and destination attempts keep retry and replay evidence.

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.

HeaderExampleUse
X-GitHub-EventpushRoute by event family before inspecting deep payload fields.
X-GitHub-Delivery72d3162e-cc78-11ec-9d64-0242ac120002Track the provider delivery id for logs and idempotency.
X-Hub-Signature-256sha256=<hex-hmac>Verify the raw request body with the GitHub webhook secret.
X-GitHub-Hook-ID123456789Identify which GitHub webhook configuration sent the request.
User-AgentGitHub-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

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

GitHub webhook settings
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: checked

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

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

Create filtered connection
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

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

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

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

Verify GitHub signature
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.

Verify FastHook signature
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>
GitHub webhook recovery map showing accepted requests, rejected requests, connection filters, destination attempts, idempotency, GitHub redelivery, FastHook retry, and replay.
A reliable GitHub workflow separates provider ingress from downstream automation delivery. Rejected requests explain source problems; failed events and attempts explain destination problems.

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

Inspect accepted 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

Inspect rejected 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

Retrieve request
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

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"

Inspect failed destination events

Inspect failed 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

Retry failed 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

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

Rejected request JSON
{
  "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-256 against the raw body before routing the request.
  • Filter by x-github-event, repository, branch, PR base, workflow name, or deployment environment.
  • Use X-GitHub-Delivery and 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=data to 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.

Related guides