Skip to content
← escentmachine field notes

Why Formatting JSON Can Break Webhook Signature Verification

On this page
AI-generated illustration; not taken on location · Conceptual comparison of compact and spaced JSON, with the added space highlighted in the byte sequence.
AI-generated illustration; not taken on location · Formatting can preserve a JSON value while changing the bytes supplied to signature verification.AI-generated illustration

JSON formatting can break webhook signature verification when the signature authenticates the original request-body bytes. Adding a space, rewriting a character escape, or serializing a parsed object can change that input even when the decoded JSON value stays the same. Capture the original body, verify it according to the provider’s complete signing rules, and then parse or format it for processing.

The distinction matters when debugging an integration that receives sensible-looking JSON but rejects its signature. Comparing parsed values answers whether the data agrees. Comparing the preserved signing input answers whether verification received the expected representation. The examples below make that difference visible without assuming that every webhook provider uses the same signing format.

The same JSON value can have different signed bytes

Consider these two JSON representations:

{"a":1}
{"a": 1}

Both represent an object whose a value is 1. The second representation includes a space after the colon. For these UTF-8 examples, the first body contains seven bytes and the second contains eight.

Comparison Result
Parsed JSON values Equal
Original body bytes Different
Suitable substitutes for raw-body verification Do not assume so

When a signing procedure includes the original body, that space is part of the representation being authenticated. A verifier cannot replace the received body with a visually similar version merely because both versions parse to the same value.

This gives debugging a useful starting point: matching objects do not establish matching signing inputs. If verification fails after formatting, compare the preserved body with the bytes actually passed to verification. A successful parse does not resolve that comparison, and making the JSON compact again does not establish that its original representation has been restored.

Formatting and escaping can change the signing input

Formatting involves several choices that should be inspected separately. Otherwise, a broad label such as “JSON cleanup” can hide the exact change responsible for a mismatch.

Transformation What to inspect
Whitespace Were spaces or indentation added or removed?
Property order Did serialization write properties in a different order?
Line endings Were line breaks added, removed, or rewritten?
Character escaping Did literal characters become escape sequences, or the reverse?
Character encoding Was the body converted into a different encoding?

Escaped and literal characters

Compare {"text":"é"} with {"text":"\u00e9"}. Both decode to the same string value. Their UTF-8 body representations differ: one writes the character directly, while the other writes its JSON escape sequence.

A formatter or serializer that changes between these representations changes the potential signing input. The decoded text can still look identical in an application interface, which makes a byte comparison more useful than a visual comparison alone.

Readability and verification serve different purposes

Indentation can make a payload easier to inspect. Changing property order can also make a displayed object easier to compare. Those operations belong on an inspection copy when verification depends on the original body.

Character encoding deserves its own check. If a processing step converts the body to another encoding, the original signature can become invalid. Follow the provider’s encoding requirements instead of assuming that any representation of the same text is interchangeable.

Parsing a copy does not replace the original body

Retaining the original bytes and parsing a separate copy is different from reconstructing the request body from a parsed object. In the first approach, the verification input remains available. In the second, verification receives a newly serialized representation that may differ from the delivery.

The distinction is about which value reaches the verifier, not whether parsing has happened anywhere in the application.

Approach Input supplied to verification
Preserve the body and parse a separate copy The preserved original body
Parse, then serialize the resulting object A reconstructed body whose representation may differ

A parsed object does not preserve every detail of the text that produced it. For example, it does not establish whether a space followed a colon or whether a character appeared literally or through an escape sequence. Serializing that object therefore cannot be assumed to recover the original body.

When reviewing an implementation, trace the variable passed into verification. Does it still contain the captured body bytes, or was it replaced with the output of a serializer? Keeping the original body under a distinct variable name can make this boundary easier to review.

Capture the body, verify it, then transform it

Organize the receiving path so that verification has access to the required input before formatting or reconstruction can replace it.

  1. Capture the original request body. Do this before middleware transforms the representation needed by the provider’s signing procedure.
  2. Preserve the required signature headers and metadata. The body may be only one component of the signed input.
  3. Verify using the provider’s library or documented procedure. Supply the preserved body and every other required component in the specified form.
  4. Stop event processing if verification fails. Do not let a readable payload or successful JSON parse stand in for authentication.
  5. Parse and transform after successful verification. Use the verified delivery for application processing, and create a separate formatted copy when helpful for inspection.

The application’s capture point is worth checking explicitly. A request handler receiving an already parsed object does not demonstrate that the original body remains available. Inspect the framework’s body-capture mechanism and the middleware order rather than assuming that serializing the object will recover the incoming representation.

Keep business actions behind successful verification. This makes the receive-to-process sequence clear: preserve the input, authenticate the delivery, then interpret its contents.

Follow the provider's complete signing format

Preserving the body is necessary for a raw-body signing procedure, but it may not be sufficient. Webhook signing formats can include metadata as well as the request body. Verification must follow the complete format used by the integration.

A body-only procedure and a timestamp-plus-body procedure require different inputs. In the latter case, having the correct body does not compensate for a missing timestamp, an incorrect ordering of components, or an incorrect way of joining them. Do not infer those rules from an unrelated example.

Requirement Question to resolve
Signed components Which body bytes, headers, or metadata are included?
Ordering and encoding In what order and representation are those components combined?
Signature header How must the received header be interpreted?
Algorithm Which algorithm does this integration require?
Digest encoding How is the signature represented for comparison?
Secret interpretation How must the configured secret be supplied to verification?
Replay checks What authenticated freshness information or delivery identifier is available?

A valid signature does not prevent repeated processing

A captured delivery can retain a valid signature when replayed. Signature verification alone therefore does not prevent the same delivery from being processed again.

Where the protocol authenticates a timestamp, apply its documented freshness checks. If the integration provides an event identifier, determine how it should support duplicate-processing controls. Establish these rules from the provider’s procedure and the application’s processing requirements; do not invent a universal acceptance window or assume every protocol includes the same fields.

Demonstrate the mismatch with a small Python fixture

This executable fixture isolates the effect of parsing and serialization. It uses a synthetic body-only HMAC calculation; it is not a complete verifier for any particular provider.

import hmac, json

a = b'{"a":1}'
b = json.dumps(json.loads(a)).encode()
mac = lambda x: hmac.digest(b'demo-only', x, 'sha256')

print(json.loads(a) == json.loads(b), a == b, mac(a) == mac(b))

Expected output:

True False False

Read the three results in order:

  • True: both bodies parse to the same JSON value.
  • False: the original body and the serialized body contain different bytes.
  • False: the HMAC results for these two inputs differ.

Here, serialization produces b'{"a": 1}', adding a space after the colon. The fixture keeps the secret and algorithm fixed, so the changed body representation is the difference being demonstrated.

Use this example to test the distinction between value equality and byte equality before investigating a larger receiving pipeline. The literal demo-only is fixture data. An actual integration must use its required secret interpretation, algorithm, signed components, header handling, and replay checks. Substituting a real secret into this example would not establish that those other requirements had been implemented.

Find the first transformation that caused the mismatch

Use synthetic payloads to trace the body through the receiving pipeline. At each relevant boundary, compare the preserved input with the representation about to enter the next step. Record byte lengths and locate the first differing byte rather than relying only on formatted displays.

In the Python fixture, the original body has seven bytes and the serialized body has eight. Their first difference is at zero-based byte offset 5: the original has 1, while the serialized version has a space. That identifies the inserted whitespace directly. Equal lengths would still require a byte comparison because a representation can change without becoming longer.

Inspect the transformation boundary

Work through the path in order:

  1. Check where the body is captured relative to JSON parsing and other middleware.
  2. Inspect whether a proxy or middleware step transforms the body before capture.
  3. Compare the captured body with the exact bytes supplied to verification.
  4. Check encoding conversions, escaping changes, property ordering, and newline handling at the first point of difference.
  5. If the body remains identical, inspect additional signed components, signature decoding, algorithm selection, and secret selection.

Keep the formatter on a separate inspection copy throughout this process. It can help reveal the payload’s structure, while the preserved body remains the input used for verification.

The final check is concrete: can the synthetic delivery reach verification with its original body intact, and does the verifier receive every other component required by the provider? Once those boundaries are explicit, a mismatch has a smaller, testable set of causes.

Previous note
Next note