Webhook Guides

GitHub Pull Request Webhook

GitHub pull_request webhooks are the repository signals behind preview environments, deployment gates, CI checks, security scans, release validation, review automation, and PR cleanup jobs. They should be routed separately from push events because the payload shape and workflow intent are different.

FastHook gives PR automation a stable source URL, GitHub raw-body HMAC verification, accepted and rejected request records, connection filters, destination attempts, retry controls, and replayable failure evidence. The examples below match the current FastHook source HMAC, filter, and request inspection behavior.

Pull request delivery model

GitHub sends a pull_request webhook when activity happens on a pull request. Use X-GitHub-Event to choose the PR route, use X-GitHub-Delivery for traceability, verify X-Hub-Signature-256 against the exact raw body, then route by action,pull_request.base.ref, pull_request.head.sha, draft state, merge state, and repository identity.

GitHub pull request webhook delivery flow through FastHook source HMAC verification, request records, action filters, preview or merge automation, attempts, retry, and replay.
A PR delivery should keep provider identity, source verification, action filters, receiver idempotency, and replay evidence connected from the first request.

Pull request headers

Headers tell you which provider event arrived, which GitHub delivery id to trace, and whether a GitHub webhook secret signature is present. FastHook stores these headers with the request record.

HeaderExampleUse
X-GitHub-Eventpull_requestChoose the PR parser and route family before reading PR-specific fields.
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 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.
Pull request headers
POST /github HTTP/1.1
Host: hook-xxxxxx.fasthook.io
User-Agent: GitHub-Hookshot/1234567
Content-Type: application/json
X-GitHub-Event: pull_request
X-GitHub-Delivery: 72d3162e-cc78-11ec-9d64-0242ac120002
X-Hub-Signature-256: sha256=<hex-hmac-sha256>

Pull request payload example

This sample shows the practical fields most PR receivers use: action, PR number, target branch, source branch, head SHA, draft state, merge state, repository, sender, and PR URL.

Pull request payload JSON
{
  "action": "opened",
  "number": 42,
  "pull_request": {
    "id": 1789443201,
    "node_id": "PR_kwDOExample42",
    "html_url": "https://github.com/acme/payments-api/pull/42",
    "diff_url": "https://github.com/acme/payments-api/pull/42.diff",
    "patch_url": "https://github.com/acme/payments-api/pull/42.patch",
    "title": "Add webhook delivery dashboard",
    "state": "open",
    "draft": false,
    "merged": false,
    "mergeable": true,
    "user": {
      "login": "dev-user",
      "id": 1234567
    },
    "head": {
      "ref": "feature/webhook-dashboard",
      "sha": "2c3d4e5f6a7b8c9d0e1f23456789abcdef012345",
      "repo": {
        "id": 987654321,
        "full_name": "acme/payments-api"
      }
    },
    "base": {
      "ref": "main",
      "sha": "5f6b7a8c9d0e1f23456789abcdef0123456789ab",
      "repo": {
        "id": 987654321,
        "full_name": "acme/payments-api"
      }
    }
  },
  "repository": {
    "id": 987654321,
    "name": "payments-api",
    "full_name": "acme/payments-api",
    "default_branch": "main"
  },
  "sender": {
    "login": "dev-user",
    "id": 1234567
  }
}

Fields worth filtering and logging

Keep filters focused on stable, workflow-critical fields. Pull request payloads are deep, but most production receivers only need a small set of action, branch, SHA, state, and repository fields.

FieldExampleHow to use it
actionopenedDecide whether to create, update, close, or ignore PR work.
number42Human-facing PR number for URLs, logs, and support workflows.
pull_request.id1789443201Stable provider id for idempotency and database joins.
pull_request.base.refmainTarget branch. Use it for deploy gates, preview policy, and release branch routing.
pull_request.head.reffeature/webhook-dashboardSource branch. Useful for preview names, not a trust boundary.
pull_request.head.sha2c3d4e5f...Commit that checks, scans, or preview environments should evaluate.
pull_request.draftfalseAvoid running expensive automation while a PR is still draft.
pull_request.mergedfalseClosed does not always mean merged; check this field on closed actions.
repository.full_nameacme/payments-apiPrevent one endpoint from routing work across the wrong repository.

PR actions and receiver decisions

The action field is the first business decision. A preview worker, merge workflow, label automation, and review workflow should not all run from the same branch of receiver code.

ActionTypical pathGuardrail
opened / reopenedCreate or resume preview workUse base branch and draft state before queueing.
synchronizeUpdate preview or checksUse pull_request.head.sha as the new commit to evaluate.
ready_for_reviewStart heavier automationGood boundary for draft-to-review workflows.
closed + merged=falseCleanup preview stateDo not trigger merge-only deploy logic.
closed + merged=trueMerge or release workflowRoute separately from abandoned PR cleanup.
labeled / unlabeledOptional policy automationKeep label workflows separate from build workflows.

Synchronize payload

A synchronize action usually means new commits were pushed to the PR branch. Use the new pull_request.head.sha as the commit to test or deploy to a preview environment.

Synchronize payload
{
  "action": "synchronize",
  "number": 42,
  "pull_request": {
    "id": 1789443201,
    "html_url": "https://github.com/acme/payments-api/pull/42",
    "state": "open",
    "draft": false,
    "merged": false,
    "head": {
      "ref": "feature/webhook-dashboard",
      "sha": "6f7a8b9c0d1e2f3456789abcdef0123456789abc"
    },
    "base": {
      "ref": "main",
      "sha": "5f6b7a8c9d0e1f23456789abcdef0123456789ab"
    }
  },
  "repository": {
    "id": 987654321,
    "full_name": "acme/payments-api"
  },
  "sender": {
    "login": "dev-user"
  }
}

Closed and merged payload

A closed action is not always a merge. Check pull_request.merged before starting post-merge deploys, release jobs, or branch cleanup that assumes the PR landed.

Closed merged payload
{
  "action": "closed",
  "number": 42,
  "pull_request": {
    "id": 1789443201,
    "html_url": "https://github.com/acme/payments-api/pull/42",
    "state": "closed",
    "draft": false,
    "merged": true,
    "merge_commit_sha": "9abc0d1e2f3456789abcdef0123456789abcdef0",
    "head": {
      "ref": "feature/webhook-dashboard",
      "sha": "6f7a8b9c0d1e2f3456789abcdef0123456789abc"
    },
    "base": {
      "ref": "main",
      "sha": "9abc0d1e2f3456789abcdef0123456789abcdef0"
    }
  },
  "repository": {
    "id": 987654321,
    "full_name": "acme/payments-api"
  },
  "sender": {
    "login": "maintainer"
  }
}
GitHub pull request action map showing opened, synchronize, ready for review, closed not merged, and closed merged decisions through FastHook filters and receiver behavior.
Treat PR actions as routing inputs. Open, synchronize, ready-for-review, closed, and merged states need explicit receiver decisions.

Verify GitHub PR signatures in FastHook

GitHub uses X-Hub-Signature-256 for HMAC-SHA256 signatures over the exact raw body. FastHook can verify that at the source by using auth_type HMAC, signature_header x-hub-signature-256, and prefix sha256=. Do not configure a timestamp header for GitHub.

Create GitHub PR 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 pull request source",
    "description": "GitHub PR 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 PR smoke test

This is a FastHook source test, not a claim that GitHub sent the request. It signs the exact JSON string being sent with the same sha256= HMAC shape GitHub documents.

Signed PR test
import crypto from "node:crypto";

const sourceUrl = "https://hook-xxxxxx.fasthook.io/";
const secret = process.env.GITHUB_WEBHOOK_SECRET;

const payload = JSON.stringify({
  action: "opened",
  number: 42,
  pull_request: {
    id: 1789443201,
    html_url: "https://github.com/acme/payments-api/pull/42",
    draft: false,
    merged: false,
    head: {
      ref: "feature/webhook-dashboard",
      sha: "2c3d4e5f6a7b8c9d0e1f23456789abcdef012345"
    },
    base: {
      ref: "main"
    }
  },
  repository: {
    id: 987654321,
    full_name: "acme/payments-api"
  },
  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": "pull_request",
    "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 pull_request events in FastHook

Connection filters run after the source accepts the request. Use header filters for the event family and body filters for repository, action, base branch, draft state, merge state, or head SHA. When a filter has alternatives, keep them inside an explicit $and array so the backend matcher evaluates every condition.

Preview environment filter

Preview filter
{
  "type": "filter",
  "headers": {
    "x-github-event": "pull_request"
  },
  "body": {
    "action": {
      "$in": [
        "opened",
        "reopened",
        "synchronize",
        "ready_for_review",
        "closed"
      ]
    },
    "repository": {
      "full_name": "acme/payments-api"
    },
    "pull_request": {
      "base": {
        "ref": "main"
      }
    }
  }
}

Main or release branch filter

Main or release filter
{
  "type": "filter",
  "headers": {
    "x-github-event": "pull_request"
  },
  "body": {
    "$and": [
      {
        "repository": {
          "full_name": "acme/payments-api"
        }
      },
      {
        "action": {
          "$in": [
            "opened",
            "reopened",
            "synchronize",
            "ready_for_review"
          ]
        }
      },
      {
        "$or": [
          {
            "pull_request": {
              "base": {
                "ref": "main"
              }
            }
          },
          {
            "pull_request": {
              "base": {
                "ref": {
                  "$starts_with": "release/"
                }
              }
            }
          }
        ]
      },
      {
        "pull_request": {
          "draft": false
        }
      }
    ]
  }
}

Merged PR filter

Merged PR filter
{
  "type": "filter",
  "headers": {
    "x-github-event": "pull_request"
  },
  "body": {
    "action": "closed",
    "repository": {
      "full_name": "acme/payments-api"
    },
    "pull_request": {
      "merged": true,
      "base": {
        "ref": "main"
      }
    }
  }
}

Inspect captured PR payloads

Use include=data when you need the stored PR JSON body, headers, method, path, and query. Start with the request record, then open request events and destination attempts to see which connection matched and whether the receiver accepted the delivery.

Inspect PR 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=pull_request"
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 PR request
{
  "id": "req_7Jm4Q8",
  "source_id": "src_q6z62b6py5o79b",
  "status": "accepted",
  "verified": true,
  "method": "POST",
  "path": "/",
  "rejection_cause": null,
  "data": {
    "headers": {
      "x-github-event": "pull_request",
      "x-github-delivery": "72d3162e-cc78-11ec-9d64-0242ac120002",
      "x-hub-signature-256": "sha256=<redacted>"
    },
    "body": {
      "action": "synchronize",
      "number": 42,
      "pull_request": {
        "id": 1789443201,
        "draft": false,
        "merged": false,
        "head": {
          "ref": "feature/webhook-dashboard",
          "sha": "6f7a8b9c0d1e2f3456789abcdef0123456789abc"
        },
        "base": {
          "ref": "main"
        }
      },
      "repository": {
        "id": 987654321,
        "full_name": "acme/payments-api"
      }
    },
    "parsed_query": {},
    "query": "",
    "path": "/",
    "method": "POST"
  }
}

Receiver idempotency for PR events

GitHub redelivery, FastHook retry, and manual replay can all deliver the same PR event more than once. Store an idempotency key before creating previews, queueing checks, deploying merged code, or cleaning up resources. Include action in the key so synchronize updates and closed cleanup do not collide.

PR receiver idempotency
export async function handleGitHubPullRequest(request) {
  const deliveryId = request.headers.get("x-github-delivery");
  const event = request.headers.get("x-github-event");
  const payload = await request.json();

  if (event !== "pull_request") {
    return new Response("ignored", { status: 202 });
  }

  const repoId = payload.repository?.id;
  const pr = payload.pull_request;
  const action = payload.action;
  const prId = pr?.id;
  const headSha = pr?.head?.sha ?? "no-head-sha";
  const businessKey = `github:pr:${repoId}:${prId}:${action}:${headSha}`;

  const inserted = await idempotencyStore.insertOnce({
    key: businessKey,
    providerDeliveryId: deliveryId
  });

  if (!inserted) {
    return Response.json({ duplicate: true });
  }

  if (action === "closed") {
    await cleanupPreview({ repoId, prId, number: payload.number });

    if (pr?.merged) {
      await queuePostMergeWorkflow({
        repoId,
        prId,
        baseRef: pr.base?.ref,
        mergeCommitSha: pr.merge_commit_sha
      });
    }

    return Response.json({ queued: pr?.merged ? "merge-workflow" : "cleanup" });
  }

  if (pr?.draft) {
    return Response.json({ ignored: "draft" });
  }

  await upsertPreview({
    repoId,
    prId,
    number: payload.number,
    baseRef: pr?.base?.ref,
    headRef: pr?.head?.ref,
    headSha
  });

  return Response.json({ queued: "preview" });
}

Production checklist

  • Subscribe to pull_request only for PR lifecycle automation.
  • Use separate routes for pull_request_review, review comments, issue comments, and review threads.
  • Verify X-Hub-Signature-256 with the raw body before trusting payload fields.
  • Filter by x-github-event, repository, PR action, base branch, draft state, and merge state.
  • Use pull_request.head.sha for checks and preview updates.
  • Check pull_request.merged before running post-merge workflows.
  • Store X-GitHub-Delivery for tracing and a business id for duplicate protection.
  • Inspect FastHook request data and destination attempts before replaying failed PR automation.

GitHub pull_request FAQ

What is a GitHub pull_request webhook?

A GitHub pull_request webhook is an HTTP POST delivery with X-GitHub-Event set to pull_request and a JSON payload containing action, number, pull_request, repository, and sender fields.

Which pull_request fields should I route on?

Route on action, repository.full_name, pull_request.base.ref, pull_request.head.ref, pull_request.head.sha, pull_request.draft, pull_request.state, and pull_request.merged depending on the workflow.

Are pull request review comments part of the pull_request event?

No. GitHub uses separate events for pull_request_review, pull_request_review_comment, issue_comment, and pull_request_review_thread. Keep those on separate routes when they trigger different work.

Can FastHook verify GitHub pull_request signatures?

Yes. FastHook source HMAC auth can verify GitHub X-Hub-Signature-256 by using signature_header x-hub-signature-256 and prefix sha256= with the same webhook secret configured in GitHub.

GitHub documentation references

GitHub documents delivery headers in the webhook events and payloads reference, the pull_request payload in the same reference, and X-Hub-Signature-256 behavior in validating webhook deliveries.

Related guides