Programming in Practice22/09/2026Redação de Tecnologia e Desenvolvimento6 min readUpdated on 26/09/2026

GitHub webhooks: how to validate HMAC, prevent replay, and handle duplicate events

A practical guide to authenticating GitHub webhooks with HMAC-SHA256, using X-GitHub-Delivery against replay, deduplicating events, and processing redeliveries idempotently.

Ilustração em fundo escuro mostra fluxo de webhooks do GitHub com verificação de assinatura, proteção contra replay, fila, cache e descarte de evento duplicado.

Validating a GitHub webhook means solving three separate problems: confirming that the delivery came from GitHub and was not modified, preventing the same delivery from producing repeated effects, and responding quickly enough that slow processing does not become a delivery failure. A practical design combines HMAC over the original request body, deduplication with X-GitHub-Delivery, persistent idempotent state, and a queue that removes heavy work from the HTTP response path.

What each mechanism solves

MechanismProblem it solvesImplementation point
HMAC-SHA256Delivery authenticity and integrityRecompute the signature with the secret and compare it with X-Hub-Signature-256
X-GitHub-DeliveryDelivery identity and replay protectionPersist the GUID and reject or absorb repeats according to state
IdempotencyPrevent duplicated business effectsUse a unique key, transaction, or operation that can be safely repeated
QueueAvoid endpoint timeoutsValidate, record, enqueue, and return 2XX quickly

1. Validate the HMAC before interpreting the JSON

When a webhook has a secret configured, GitHub sends X-Hub-Signature-256. The signature starts with sha256= and is computed with HMAC-SHA256, using the secret as the key and the payload contents as the message. GitHub's documentation recommends validating the signature before continuing with processing.

The detail that most often causes failures is the body used in the calculation. The signature depends on the received contents. If a framework parses JSON and later serializes it again, whitespace, property order, or encoding can change. Capture the original request body and calculate the HMAC before any transformation. GitHub's own Ruby example reads request.body before parsing, and the TypeScript example reads the request text before verification.

Verifiable Node.js example

const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");

const a = Buffer.from(expected, "utf8"); const b = Buffer.from(signatureHeader || "", "utf8"); const valid = a.length === b.length && crypto.timingSafeEqual(a, b);

The length check before timingSafeEqual is required because the Node.js API expects buffers of equal length. If the header is missing, uses another prefix, or does not match the computed HMAC, treat the delivery as unauthenticated and do not let it reach business logic.

GitHub also publishes an official test vector: with the secret It's a Secret to Everybody and the payload Hello, World!, the expected X-Hub-Signature-256 value starts with sha256=757107ea.... This is useful for verifying your implementation before connecting the endpoint to a real webhook.

2. Use X-GitHub-Delivery for replay protection and deduplication

The X-GitHub-Delivery header is a GUID that identifies the delivery. GitHub recommends using it to protect the endpoint against replay attacks. One detail matters greatly: when a delivery is intentionally redelivered, the same X-GitHub-Delivery value is preserved. The identifier therefore helps both with unwanted duplicates and with recognizing a legitimate retry of the same delivery.

Model state, not just a list of blocked IDs

A receipts table can use delivery_id as a unique key and store fields such as status, received_at, attempt_count, and last_error. A robust flow can use these rules:

  • If the GUID has never been seen, create a record in received state and enqueue the work.
  • If the GUID is already completed, return 2XX without repeating the business effect.
  • If the GUID is processing, do not create a second concurrent worker for the same delivery.
  • If the GUID is failed, a redelivery can start a controlled new attempt without creating two simultaneous operations.

This design is safer than placing the GUID into a permanent blacklist at first contact. A delivery can be received successfully and then fail later in the queue. Because redelivery keeps the same GUID, blocking it forever would also block legitimate recovery.

3. Make the business operation idempotent

Deduplicating the GUID reduces repeats, but it does not replace idempotency. Failure windows exist where a worker may complete an operation and crash before marking the receipt as completed. On the next attempt, the same job can appear again.

The strongest protection lives close to the business effect. Examples include a unique (delivery_id, operation) key, writing the event and the state change in the same transaction, or using a natural domain key that prevents the same mutation twice. For outbound integrations, keep an idempotency key associated with the requested effect whenever the destination API supports one.

4. Validate, record, enqueue, and respond

GitHub recommends returning a 2XX response within 10 seconds. If the endpoint takes longer, GitHub terminates the connection and considers the delivery a failure. That is why external API calls, report generation, or other expensive work should not normally happen synchronously before the response.

  1. Read the original body and required headers.
  2. Validate X-Hub-Signature-256.
  3. Read X-GitHub-Delivery and X-GitHub-Event.
  4. Record the delivery atomically while enforcing the GUID unique key.
  5. Enqueue only the work allowed by the current state.
  6. Return 2XX.
  7. In the worker, execute idempotent business logic and update the state to completed or failed.

The X-GitHub-Event header identifies the event type. GitHub also recommends checking the event type and, when present, the payload action before processing. This keeps a generic endpoint from applying the wrong handler to an unexpected event.

5. Intentional redelivery must be part of the design

GitHub does not automatically redeliver failed deliveries. An operator can request redelivery through the interface or API, and GitHub's documentation allows recent deliveries from the past three days to be redelivered. Because redelivery keeps the same GUID, the consumer must use persisted state to decide whether work remains or the delivery has already completed.

A safe pattern is: completed returns 2XX without repeating the effect; failed allows a controlled new attempt; processing requires a lock, lease, or another concurrency guard; stale records stuck in processing can be recovered using a clearly defined operational timeout.

Reference flow

StageCheckResult
IngressHTTPS, expected method, and size limitsRequest accepted for authentication
SignatureHMAC-SHA256 over the original body and timing-safe comparisonOrigin and integrity validated
IdentityX-GitHub-DeliveryGUID associated with persistent state
RoutingX-GitHub-Event and actionCorrect handler selected
QueueIdempotent insertFast 2XX response
WorkerTransaction, unique key, or idempotency keyBusiness effect occurs at most once from a logical perspective

Common mistakes

  • Computing the HMAC over reserialized JSON instead of the received body.
  • Comparing the signature with == instead of an appropriate timing-safe routine.
  • Trusting only source IP and skipping the secret. An IP allow list can be an additional layer, not a replacement for HMAC.
  • Using only local memory for deduplication in a multi-instance application.
  • Marking a GUID as processed before the business effect really completes.
  • Returning 2XX before durably recording the work that still needs processing.
  • Treating every repetition of the same GUID as an attack and thereby blocking a legitimate redelivery of a prior failed attempt.

Implementation checklist

  • High-entropy random secret stored outside source code.
  • X-Hub-Signature-256 validated before parsing and business logic.
  • Original request body preserved for the HMAC calculation.
  • Timing-safe signature comparison.
  • X-GitHub-Delivery enforced with a unique constraint and persistent state.
  • Handler selected using X-GitHub-Event and action.
  • Durable queue and 2XX response within the operational limit.
  • Idempotent worker operation.
  • Clear policy for processing, failed, and completed states.
  • Redelivery procedure and failure observability.

Sources

Topics in this articlefilasGitHubHMACidempotênciaNode.jssegurançawebhooks