developer

How to Verify UPI Webhook Signatures: Preventing Replay Attacks and Payload Forgery

A security engineer's guide to verifying UPI payment webhook signatures. Learn HMAC-SHA256 math, timestamp tolerance windows, and constant-time comparisons.

VT VyaparGateway Team Payments & Compliance 2 min read
How to Verify UPI Webhook Signatures: Preventing Replay Attacks and Payload Forgery guide
verify payment webhook signature hmac sha256 payment security prevent webhook replay attack api security VyaparGateway

When an e-commerce platform accepts online payments, its webhook receiver is a publicly exposed HTTPS endpoint. If that endpoint is not fortified with rigorous cryptographic checks, bad actors can forge fake payment confirmation payloads, artificially crediting wallets and stealing physical merchandise.

Here is the authoritative security engineering guide to validating HMAC-SHA256 webhook signatures, enforcing timestamp tolerance windows, and preventing replay attacks.


The Anatomy of a Payment Webhook Exploit

Direct Answer: An insecure webhook listener that validates payments solely on the JSON body (status: "SUCCESS") without verifying cryptographic headers can be exploited by anyone capable of sending a basic cURL request. Attackers simply send fake payment events directly to your callback URL, tricking your system into fulfilling orders without paying a single rupee.

To guarantee zero fraud, your server must enforce Message Integrity (the payload was not altered) and Origin Authenticity (the request came from VyaparGateway).


The HMAC-SHA256 Cryptographic Contract

VyaparGateway signs every outbound webhook using the HMAC-SHA256 standard.

The Header Structure:

POST /api/webhooks/payment HTTP/1.1
Host: yourstore.com
Content-Type: application/json
X-Webhook-Signature: t=1728038400,v1=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
  • t: The UNIX timestamp (in seconds) when the gateway dispatched the event.
  • v1: The computed HMAC-SHA256 hexadecimal signature of the signed payload.

The Signed Payload Construction:

The signature is computed over the string:

signed_payload = timestamp + "." + raw_body_bytes

Defeating Replay Attacks with Timestamp Tolerances

Even if an attacker cannot forge your secret key, they might intercept a genuine webhook and replay it 2 days later.

To eliminate this vulnerability:

  1. Extract t from the header.
  2. Compare it against your current server timestamp:
    current_time = now()
    if (current_time - t) > 300 seconds:
        REJECT ("Timestamp outside 5-minute tolerance window")
  3. This 300-second window accounts for network latency and NTP server drift while rendering captured packets useless to attackers.

Complete Multi-Language Verification Snippets

1. Node.js / TypeScript (Express)

import crypto from 'crypto';
import { Request, Response } from 'express';

export function verifyWebhook(req: Request, res: Response, secret: string): boolean {
  const signatureHeader = req.headers['x-webhook-signature'] as string;
  if (!signatureHeader) return false;

  const parts = Object.fromEntries(
    signatureHeader.split(',').map((item) => item.split('='))
  );

  const timestamp = parseInt(parts.t, 10);
  const receivedSig = parts.v1;

  // 1. Check timestamp tolerance (5 minutes)
  const currentTime = Math.floor(Date.now() / 1000);
  if (Math.abs(currentTime - timestamp) > 300) {
    return false; // Stale or replayed webhook
  }

  // 2. Reconstruct signed payload using RAW BODY buffer
  const rawBody = (req as any).rawBody.toString('utf-8');
  const signedPayload = `${timestamp}.${rawBody}`;

  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  // 3. Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(receivedSig, 'hex'),
    Buffer.from(expectedSig, 'hex')
  );
}

2. Python (FastAPI / Standard Library)

import hmac
import hashlib
import time

def verify_webhook_signature(raw_body: bytes, header_sig: str, secret: str) -> bool:
    try:
        parts = dict(x.split("=") for x in header_sig.split(","))
        timestamp = int(parts["t"])
        received_sig = parts["v1"]
    except Exception:
        return False

    # Check 5-minute replay tolerance
    if abs(int(time.time()) - timestamp) > 300:
        return False

    # Create signed message
    signed_payload = f"{timestamp}.".encode("utf-8") + raw_body

    expected_sig = hmac.new(
        key=secret.encode("utf-8"),
        msg=signed_payload,
        digestmod=hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(received_sig, expected_sig)

Security Audit Checklist for Payment Listeners

  • Disable Body Parsers for HMAC Path: In Express, use express.raw({ type: 'application/json' }) for your webhook route to retain untouched body bytes.
  • Store Secrets in Environment Variables: Never hardcode your webhook secret in Git repositories.
  • Log Signature Failures with IP Addresses: Alert your SecOps team when an IP repeatedly triggers 401 signature mismatches.
  • Return 200 OK Quickly: Acknowledge receipt within 2 seconds, dispatching database updates and third-party API calls to asynchronous background queues.

Test and debug your HMAC signatures in seconds using our interactive Webhook Payload Signature Generator.

Direct answers

Frequently asked questions

What is an HMAC-SHA256 signature in payment webhooks?
An HMAC-SHA256 signature is a cryptographic checksum generated by combining the webhook payload's raw body with a secret key known only to the gateway and the merchant, ensuring message authenticity and data integrity.
What is a webhook replay attack and how is it prevented?
A replay attack occurs when an eavesdropper intercepts a valid payment webhook and re-sends it to the merchant server. It is prevented by embedding a timestamp in the signature header and enforcing a strict 5-minute validity window.
Why must timing-safe string comparison be used to verify webhook signatures?
Standard string equality operators (== or ===) terminate early upon finding the first mismatched character, creating subtle latency differences that attackers exploit via timing attacks. Constant-time comparison ensures identical evaluation duration regardless of string contents.

Build your payment flow

Explore the API and browser-only merchant tools.

Create UPI checkout orders, verify signed events, or test the free calculators and generators without exposing credentials.