A webhook URL is public by design, so possession of the URL is not authentication. VyaparGateway sends X-VyaparGateway-Timestamp and X-VyaparGateway-Signature headers. Your server reconstructs the signed message from the timestamp, a period, and the exact raw request body, then compares the HMAC-SHA256 digest using a timing-safe operation.
Signature verification happens before JSON fields are trusted. After authentication, enforce a replay window, parse the payload, insert event_id into a unique database column, validate the mapped order, and acknowledge quickly. Expensive fulfilment belongs in a queue after the event transaction commits.
Design principles
The boundaries that keep this flow reliable
Preserve exact bytes
Read the raw body before a JSON middleware transforms it. Re-serialised JSON can change whitespace or key order and therefore change the digest.
Reject stale timestamps
Compare the signed timestamp with server time using a documented tolerance. This limits reuse of a captured valid request.
Deduplicate after authentication
Store event_id under a unique constraint before applying business effects. A retry should return 2xx without fulfilling twice.
Implementation workflow
From request to a durable result
- 1
Capture headers and raw body
Read signature, timestamp, and exact bytes. Reject missing or malformed headers with a non-2xx response.
- 2
Check timestamp freshness
Validate integer format and ensure the event falls inside your replay tolerance using a synchronized server clock.
- 3
Compute and compare HMAC
Create HMAC-SHA256 over timestamp + '.' + raw body and compare the received digest in constant time.
- 4
Apply one event
Insert event_id, validate the local order and amount, update state, commit, and then return 2xx or enqueue fulfilment.
Node.js raw-body signature verification
tsconst timestamp = request.headers.get("X-VyaparGateway-Timestamp") ?? "";
const received = request.headers.get("X-VyaparGateway-Signature") ?? "";
const rawBody = Buffer.from(await request.arrayBuffer());
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return new Response("stale webhook", { status: 401 });
}
const expected = crypto
.createHmac("sha256", process.env.VG_WEBHOOK_SECRET!)
.update(timestamp + ".")
.update(rawBody)
.digest("hex");
const valid = expected.length === received.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)); Production checklist
Verify before going live
- Webhook secret is stored separately from the API key
- Framework route exposes exact raw bytes
- Timestamp tolerance and server clock monitoring are configured
- HMAC is compared with a constant-time function
- event_id has a unique database constraint
- Endpoint returns promptly after durable acceptance
Failure recovery
Why valid signatures fail
Body parsed before verification
Your framework changed the bytes. Configure raw-body handling for only the webhook route and parse JSON after HMAC succeeds.
Wrong secret or environment
The endpoint is using a rotated, staging, or copied secret. Confirm the active dashboard secret without logging it.
Timestamp concatenation differs
Sign exactly the documented timestamp, period, and raw bytes. Do not add spaces, a newline, or a parsed object.
FAQ
Questions developers ask
Can I whitelist webhook IP addresses instead of verifying HMAC?
Network controls may be an additional layer, but HMAC authenticates the message itself and should remain mandatory.
What should the endpoint return for a duplicate event?
If the original event was already durably processed, return 2xx without applying the business effect again.
Should failed signatures be logged?
Log a request ID, timestamp, reason, and safe metadata. Do not log secrets or unnecessary customer payment data.