Web Crypto · HMAC-SHA256 · zero network calls

Webhook payload signature generator

Reproduce an HMAC-SHA256 digest, compare a received signature, and see exactly which bytes your payment webhook verifier must sign.

Signing inputs

HMAC-SHA256 · UTF-8 · hexadecimal output

Signing convention

Whitespace, key order, line endings, and every byte affect the signature. Do not format parsed JSON before verification.

This tool runs locally, but use a test secret. Production webhook secrets belong in server-side secret storage and should not be pasted into third-party pages.

Generate and verify

Calculated by `crypto.subtle`

String signed

1785254400.{"event":"payment.success","order_id":"ord_demo_2026","client_txn_id":"STORE-1001","status":"success","amount":499,"currency":"INR","timestamp":1785254400}

166 UTF-8 bytes

Paste a signature to compare it with the generated digest.

The comparison loop avoids early exit for well-formed hexadecimal values, but production servers should use the constant-time comparison function supplied by their runtime.

Server implementation

Verify before parsing or changing state

For VyaparGateway events, calculate the expected hexadecimal digest over `timestamp.rawBody`. Capture the request body as bytes, enforce a replay window, and compare equal-length decoded digests using the runtime’s constant-time helper.

Node.js / Express

import crypto from "node:crypto";

const signed = Buffer.concat([
  Buffer.from(timestamp + "."),
  rawBody,
]);

const expected = crypto
  .createHmac("sha256", WEBHOOK_SECRET)
  .update(signed)
  .digest();

const received = Buffer.from(signature, "hex");
const valid =
  received.length === expected.length &&
  crypto.timingSafeEqual(received, expected);

Python / FastAPI

import hashlib
import hmac

raw_body = await request.body()
signed = timestamp.encode() + b"." + raw_body

expected = hmac.new(
    WEBHOOK_SECRET.encode(),
    signed,
    hashlib.sha256,
).hexdigest()

valid = hmac.compare_digest(
    expected,
    received_signature.lower(),
)

Five controls

A valid HMAC is necessary, not sufficient.

  1. 01

    Raw bytes

    Authenticate before JSON parsing changes representation.

  2. 02

    Fresh time

    Reject signed messages outside the allowed replay window.

  3. 03

    Exact order

    Compare local and provider order identifiers.

  4. 04

    Exact money

    Match fixed-decimal amount and INR currency.

  5. 05

    Idempotency

    Store a unique event key before fulfilment.

Questions

Webhook signature FAQ

What input does VyaparGateway sign for a webhook?
VyaparGateway signs the decimal Unix timestamp, a literal period, and the exact raw JSON request body using HMAC-SHA256. The resulting digest is sent as a 64-character hexadecimal signature.
Why does formatting JSON change the HMAC signature?
HMAC operates on bytes, not parsed JSON meaning. Spaces, line breaks, character encoding, escape sequences, and key order change the byte sequence and therefore produce a different digest.
Is it safe to paste a production webhook secret into this tool?
The calculation runs locally without an API call, but production secrets should remain in controlled server-side secret storage. Use a test secret or a disposable test vector in browser utilities.
How should a server compare webhook signatures?
Decode both digests to equal-length byte arrays and use the runtime's constant-time comparison function, such as crypto.timingSafeEqual in Node.js or hmac.compare_digest in Python.
Why should a webhook timestamp be checked?
A valid old signed message can be replayed. Reject timestamps outside a short documented tolerance, then enforce event or order idempotency in the database.

Put the verified event into a real order flow.

Review create-order, status, and webhook integration patterns.

Read developer guide