# How to test webhook signature verification in agent code

> Webhook signature verification fails when code parses the body before checking the HMAC. Learn the raw-body, replay, and secret rotation tests to require on agent-written handlers, and how Factory security review checks them on every pull request.

URL: https://factory.com/articles/webhook-verification-coding-agents
Published: 2026-09-27
Updated: 2026-10-23

To verify a webhook, compute the provider's HMAC over the exact raw request bytes, compare it with the signature header in constant time, and reject stale timestamps and repeated delivery IDs before any side effect runs. When a coding agent edits a webhook handler, require negative tests for each of those steps and a security review of the pull request before merge.

Webhook verification is easy to weaken during an otherwise routine handler change. A coding agent may add an event type, move request parsing, or update middleware while preserving the happy path. The endpoint still accepts legitimate deliveries, but it may verify transformed bytes, compare signatures unsafely, or admit stale requests.

Provider contracts define the evidence. GitHub's [webhook validation guidance](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries) requires computing an HMAC over the received payload and recommends constant-time comparison. Stripe's [signature documentation](https://docs.stripe.com/webhooks/signature) requires the unmodified request body and includes a signed timestamp for replay resistance.

## Review webhook verification at the byte boundary

Trace the request from the network adapter to the verification call. Signature verification must receive the exact bytes sent by the provider. JSON parsing, character conversion, whitespace normalization, and middleware that consumes the body can all change that input.

Keep parsing after authentication. Select the secret from trusted configuration rather than request data. Confirm that the implementation rejects an unsupported algorithm, malformed header, missing signature, and invalid encoding without falling through to event handling.

A coding agent should preserve the provider's canonical construction rather than invent a shared abstraction across incompatible schemes. GitHub signs the payload with a configured secret. Stripe signs a timestamp and payload according to its documented header format. Similar names do not make those protocols interchangeable.

Framework upgrades deserve a focused adapter test. Capture a synthetic byte sequence at the HTTP boundary, sign those bytes, and confirm the verifier receives them unchanged. This catches body-parser behavior that a direct unit test of the cryptographic verification function cannot observe.

## Test webhook verification failures

Positive fixtures prove only that one valid sample works. Add negative cases that mutate the body after signing, change one signature byte, omit required fields, and use the wrong secret. For timestamped schemes, test both sides of the accepted tolerance and control the clock so the suite stays deterministic.

Replay behavior also depends on application state. A valid delivery may arrive more than once. Store the provider's stable delivery or event identifier when the operation has side effects, and prove that processing the duplicate does not repeat a payment, notification, or state transition.

Secret rotation needs an explicit overlap policy. Test the old and new secret during the allowed window, then prove the old value fails after retirement. Never put real endpoint secrets or captured customer payloads in fixtures.

The negative cases above fit in one checklist. Each row should exist as an automated test before an agent-written handler change merges.

| Test case | How to build it | Expected result |
| --- | --- | --- |
| Body changed after signing | Sign the fixture, then add one byte | Reject before event handling |
| Signature byte changed | Flip one character in the header | Reject |
| Wrong secret | Sign with an unrelated test secret | Reject |
| Missing or malformed header | Drop the header or break its encoding | Reject without falling through |
| Stale timestamp | Sign outside the tolerance with a controlled clock | Reject (timestamped schemes) |
| Duplicate delivery | Send the same delivery ID twice | Process once, no repeated side effect |
| Rotation overlap | Sign with the old and the new secret | Both pass in the window, old fails after |

## Give reviewers security evidence

The pull request should identify the provider documentation, raw-body path, comparison function, timestamp policy, duplicate-handling rule, and tests run. Reviewers can then distinguish protocol-preserving refactoring from a change to the trust boundary.

Stop the change when the provider contract, raw-body behavior, or production secret rotation policy is unknown. A passing valid-delivery test is not enough evidence for an internet-facing endpoint.

## How Factory reviews webhook handler changes

Factory is a platform for running a software factory, where agents called Droids take work from a signal to a reviewed pull request. For webhook handlers, the Factory features that matter most sit at the pull request, where a change to the trust boundary can be caught before it ships.

**Security review on every pull request.** Factory's [security review](https://docs.factory.com/software-factory/security-review) traces changed data flows across authentication, validation, and network boundaries. It runs in two passes. The first pass produces candidate vulnerabilities, and the second re-checks each one for reachability, exploitability, and existing controls before anything is reported. Findings appear inline with a severity from P0 (critical) to P3 (low) and a suggested fix.

Pull request</span><br/><span style='color:#8A8380 !important'>webhook handler change</span>"]
    B["<span style='color:#FAFAFA !important;font-weight:600'>1. Find candidates</span><br/><span style='color:#8A8380 !important'>trace request bytes<br/>across trust boundaries</span>"]
    C["<span style='color:#FAFAFA !important;font-weight:600'>2. Validate</span><br/><span style='color:#8A8380 !important'>reachability, exploitability,<br/>existing controls</span>"]
    D["<span style='color:#FAFAFA !important;font-weight:600'>Inline finding</span><br/><span style='color:#8A8380 !important'>P0 to P3 severity<br/>and suggested fix</span>"]
    E["<span style='color:#FAFAFA !important;font-weight:600'>Repository owner</span><br/><span style='color:#8A8380 !important'>reviews and merges</span>"]
    A --> B --> C --> D --> E
`}
/>

Adding `automatic_security_review: true` to the Droid review workflow runs this pass on every non-draft pull request. Commenting `@droid security` on a pull request runs it on demand.

**Repository rules for the reviewer.** A `security-review-guidelines` skill in `.factory/skills/` can state that every handler must verify the raw body before parsing, use constant-time comparison, and record delivery IDs for side effects. The guidelines take priority over the shared STRIDE and OWASP methodology when the two conflict.

**Runtime evidence.** Static review cannot prove how a framework passes raw bytes at runtime. Factory's [Automated QA](https://docs.factory.com/software-factory/automated-qa) generates a project QA skill with `/install-qa`. The generated `/qa` skill then reads each pull request diff and runs only the affected flows, so a signed test delivery can hit the running API and leave evidence on the pull request.

To set up the same checks on your own repository, run these commands in the Droid CLI or desktop app.

<div className="not-prose my-6 flex flex-col gap-3 [&_button]:!w-auto">
  <div className="grid grid-cols-1 gap-y-5 md:grid-cols-[max-content_1fr] md:gap-x-4">
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">On-demand audit.</strong> Reviews the entire repository or just the current diff.</span>
    
    <span className="text-base-300 text-sm leading-relaxed"><strong className="text-light-base-primary font-medium">One-time setup.</strong> Adds automated code and security review to your pull requests.</span>
  </div>
</div>

## Frequently asked questions

### What is webhook signature verification?

Webhook signature verification checks that a delivery came from the provider and was not changed in transit. The receiver computes an HMAC over the raw request body with a shared secret and compares it with the signature header, as GitHub and Stripe document.

### Why must webhook verification use the raw request body?

The signature covers the exact bytes the provider sent. JSON parsing, whitespace normalization, and character conversion change those bytes, so verification has to run on the unmodified body before parsing.

### How do you prevent webhook replay attacks?

For timestamped schemes such as Stripe, reject deliveries outside the signed timestamp tolerance. For operations with side effects, store the provider's delivery or event ID so a repeated delivery does not repeat a payment, notification, or state change.

### How should webhook secret rotation be tested?

Test that the old and new secret both work during the allowed overlap window, then prove the old secret fails after retirement. Never use real endpoint secrets or captured customer payloads in fixtures.

### How does Factory review webhook handler changes?

Factory security review runs on pull requests in two passes, first finding candidate issues and then validating each one, and reports findings with P0 to P3 severity. A security-review-guidelines skill can add repository rules such as verifying the raw body before parsing.

## Further reading

- [SSRF testing for coding agent network changes](https://factory.com/articles/ssrf-testing-coding-agent-changes)
- [Reviewing agent-generated pull requests at scale](https://factory.com/articles/reviewing-agent-generated-pull-requests)
- [Retry safety in agent-generated code](https://factory.com/articles/retry-safety-agent-generated-code)
- [Security review docs](https://docs.factory.com/software-factory/security-review)
