Synthetic Industry

Troubleshooting guide · updated 2026-10-11

Webhook signature check fails on genuine events? Verify the exact bytes the provider signed

A signature is a hash of exact bytes. Parsing the JSON and re-serialising it, a proxy that rewrites the body or the wrong secret each change the hash. Find which, safely.

What a webhook signature actually proves

A webhook provider cannot send a password with every event, so it signs the event instead. It takes your shared secret and the body of the request, computes a keyed hash (commonly HMAC with SHA-256) and puts the result in a header. Your endpoint recomputes the hash from the body it received and compares. A match proves two things: whoever sent the request knew the secret, and the body was not altered on the way. GitHub documents exactly this scheme: an HMAC SHA-256 digest over the payload, hex-encoded and prefixed with sha256=, in the X-Hub-Signature-256 header.

The word that matters is exact. The hash covers bytes, not meaning. Two JSON documents that mean the same thing but differ by one space, a different key order or a different way of writing a non-ASCII character produce completely different hashes.

  • Check which header, algorithm and encoding your provider documents; do not assume them from another provider.
  • Check whether the provider signs only the body or also a timestamp or an identifier.

Why parsing the body first breaks it

The usual cause of rejected genuine events is that the code parses the JSON, then serialises the object again to hash it. The re-serialised text is almost never byte-identical to what was sent. Frameworks with a body parser in front of the handler produce the same effect before your code even runs. In the Next.js App Router, a Route Handler can read the raw text with request.text(), and the documentation (version 16.4.0) shows this pattern for webhooks and notes that the Pages Router body-parser configuration is not needed there. In frameworks that parse first, you need the framework's way to keep the raw bytes.

Read the body once as raw text or bytes, verify, then parse that same text.

  • Verify before parsing, using the untouched text.
  • Treat the payload as UTF-8, as GitHub advises.
  • Do not trim, normalise line endings or pretty-print before hashing.

Other causes that look the same

A proxy, firewall or content network that rewrites the body (compressing, minifying or changing encoding) breaks the hash; GitHub states that proxies and load balancers must not alter the payload or headers. The secret can be wrong in the environment that is failing: many providers give a different secret to each endpoint, and a secret from a local forwarding tool is not the secret of the live endpoint. A missing header means no secret was configured at the provider. A comparison with == instead of a constant-time function does not usually cause a false rejection, but it is a weakness GitHub tells you to avoid.

  • Live fails and local works: compare which secret each environment reads.
  • Header missing: the provider has no secret set for that endpoint.
  • Hash differs on long or non-ASCII bodies only: suspect encoding.

A safe first investigation

Test your verification function in isolation with published values before blaming the provider. GitHub documents a test: the secret "It's a Secret to Everybody" and the payload "Hello, World!" must give the signature 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17. If your function returns that value, the hashing is right and the live problem is the bytes or the secret. Then log, without the secret, the length of the body you hashed and compare it with the length the provider shows in its delivery record. Never log the secret or full event bodies that contain customer data.

  • Function wrong on the published test: fix the function.
  • Function right, length different: something changed the body.
  • Function right, length same: suspect the secret or the header.

What fixes it, what does not and how the paid job is accepted

The repair makes the endpoint hash the exact received bytes with the right secret and header and compare in constant time, and adds tests for a correct signature and for altered body, altered signature, wrong secret and missing header. It never turns verification off: an endpoint without verification accepts forged events from anyone who finds its address. Our webhook repair (posted test price from £295, untested, paid only after sign-off) is accepted when those four tampered cases are rejected and the provider's documented sample is accepted. It excludes new event handlers, replaying history and changes to your provider account. Send the provider, the event type, the status your endpoint returns and the framework in your first enquiry, not the secret or real event bodies.

  • The worked example shows a synthetic payload and what happens when it is re-serialised.

Sources and limits

  • GitHub: Validating webhook deliveries Checked 2026-10-11.
    • GitHub signs the payload with HMAC SHA-256 and sends a hex digest prefixed sha256= in the X-Hub-Signature-256 header; proxies and load balancers must not alter the payload or headers; treat the payload as UTF-8; compare with a constant-time function, not ==; a documented test uses the secret "It's a Secret to Everybody", the payload "Hello, World!" and the signature 757107ea0eb2509fc211221cce984b8a37570b6d7586c22c46f4379c8b043e17.
  • Next.js: route.js file convention (documentation version 16.4.0) Checked 2026-10-11.
    • A Route Handler can receive webhooks and read the body with request.text(); unlike Pages Router API routes, no bodyParser configuration is needed.