Webhook Security: HMAC Signature Verification for Payment APIs
Every payment-API integration relies on webhooks for the terminal status of
a transaction. If you skip signature verification, an attacker who discovers
your webhook URL can forge “payment succeeded” events and take over your
business logic. This guide covers the HMAC-SHA256 pattern used by
NxtBanking, Razorpay, Stripe, Cashfree and most serious payment
APIs, and the three attack classes you have to defend against.
1. What HMAC actually gives you
HMAC-SHA256 is a keyed hash. Sender and receiver share a secretS. The sender computes hex(HMAC_SHA256(S, raw_body))
and ships it in an X-Signature header. The receiver computes the
same thing and compares. If they match:
- The body has not been tampered with (integrity).
- The sender knew the shared secret (authenticity).
What it does not give you:
(a) confidentiality — the body is still plain JSON;
(b) replay protection — an attacker who captured a valid request can resend it.
We fix (b) below.
2. Three attack classes you must handle
A. Forgery. Attacker hits your endpoint with arbitrary JSON.
Defeated by HMAC.
B. Replay. Attacker captured a valid “SUCCESS” webhook and
replays it to credit a customer twice. Defeated by aX-Timestamp header + rejecting anything older than 5 minutes +
idempotency on event_id.
C. Timing side-channel. Naive string comparison
(===) leaks byte-by-byte timing. Defeated by constant-time compare
(hash_equals in PHP, crypto.timingSafeEqual in Node,hmac.compare_digest in Python).
3. The canonical verification function (PHP)
function verify_webhook(string $raw_body, string $sig_header,
string $ts_header, string $secret, int $max_skew = 300): bool {
// 1. Timestamp freshness
if (abs(time() - (int)$ts_header) > $max_skew) return false;
// 2. HMAC over "timestamp.body" prevents signature replay across endpoints
$signing_payload = $ts_header . '.' . $raw_body;
$expected = hash_hmac('sha256', $signing_payload, $secret);
// 3. Constant-time compare
return hash_equals($expected, $sig_header);
}4. Node.js version
const crypto = require('crypto');
function verifyWebhook(rawBody, sigHeader, tsHeader, secret, maxSkewSec = 300) {
if (Math.abs(Date.now()/1000 - Number(tsHeader)) > maxSkewSec) return false;
const payload = `${tsHeader}.${rawBody}`;
const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex');
const a = Buffer.from(sigHeader, 'utf8');
const b = Buffer.from(expected, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}5. Python version
import hmac, hashlib, time
def verify_webhook(raw_body: bytes, sig_header: str, ts_header: str,
secret: str, max_skew: int = 300) -> bool:
if abs(int(time.time()) - int(ts_header)) > max_skew:
return False
payload = f"{ts_header}.".encode() + raw_body
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig_header)6. The five mistakes we still see in code reviews
- Parsing JSON before verifying. Frameworks like Express, Fastify and Django reformat the body during JSON parsing — your computed HMAC then differs from the sender’s. Always capture the raw body first (in Express, use
express.raw({type:'application/json'})). - Using
===instead of constant-time compare. Opens a practical timing attack in Node/Python; exploitable over a LAN. - Trusting the timestamp sent in the body. The timestamp must be on a signed header, not inside the JSON payload the attacker can rewrite.
- No idempotency store. Even with signatures, a retried legitimate event will fire twice. Persist
event_idwith 7-day retention. - Secret logged in plaintext. If the secret ends up in your log aggregator, an insider can forge webhooks. Redact via a central logger.
7. Rotating the secret
Plan for rotation on day one — don’t wait for a breach. The cleanest pattern
is two active secrets for a short overlap window:
- Provider generates
secret_v2; emits webhooks signed with both v1 and v2 headers (X-Signature-v1,X-Signature-v2). - You update your verifier to accept either.
- After 24 h, provider stops signing with v1.
- You disable v1 acceptance.
8. Beyond HMAC — when do you need mTLS?
For extremely high-value flows (RBI-regulated settlement files, NACH debit
confirmations) a mutual TLS channel between provider and merchant
is standard. HMAC still runs on top. mTLS authenticates the TCP endpoint; HMAC
authenticates individual events. Most fintechs do not need mTLS for retail
payment webhooks — HMAC + replay protection is the well-established bar.
Where to go next
- UPI API Integration Guide — where webhook verification plugs in.
- Payout RBI Compliance Checklist — why webhook authenticity is an audit-trail requirement.
- Integrate BBPS API for Bill Payments — another webhook-heavy flow.
About This Topic
This page is part of NxtBanking's documentation and product information for Indian fintech teams. NxtBanking provides Payout API, BBPS, AEPS, UPI Collection, KYC, DMT, Recharge, and Travel APIs — all available under one contract with a unified dashboard, sandbox environment, and dedicated technical support. Explore the API marketplace, commercial pillar pages, and developer guides linked from the main navigation. For a compliance-oriented walkthrough or architecture review, book a demo and our team will map your flows to the right rails.
Quick Answers
What makes a good fintech API integration?
A well-built fintech API integration covers: proper OAuth 2.0 authentication, idempotency keys on every write request, webhook HMAC signature verification, exponential-backoff retry logic for transient errors, and a status-query fallback for ambiguous outcomes. NxtBanking's sandbox environment lets you test all these scenarios before production.
How do I handle failed transactions in a payment API?
Categorise failures: (1) Hard failures (invalid account, KYC mismatch) — do not retry; notify user. (2) Transient failures (timeout, 5xx) — retry with idempotency key and exponential back-off. (3) Ambiguous (no response) — call the transaction status endpoint before retrying to avoid duplicate processing.
Is NxtBanking RBI-compliant for payment APIs?
Yes. NxtBanking operates through RBI-licensed partner banks for all payment services (IMPS, NEFT, RTGS, UPI) and is NPCI-certified for BBPS, AEPS, and UPI flows. All APIs follow RBI's Master Directions on payment aggregators, KYC, and PMLA obligations. We maintain audit logs, data localisation, and consent frameworks compliant with the DPDP Act 2023.
How does NxtBanking handle API downtime and failover?
NxtBanking uses a connected-banking architecture that links a single API credential to multiple RBI-licensed partner banks. When one bank's rails experience degradation or maintenance, the API automatically routes to the next available bank — with no code change required on the client side. This multi-bank failover is what delivers 99%+ transaction success rates and 99.9% API uptime SLA for enterprise clients.
What does it cost to integrate NxtBanking APIs?
NxtBanking offers pay-as-you-go pricing with no setup fees and no minimum commitment for most APIs. Typical pricing: IMPS/UPI payout ₹3–₹8 per transaction, NEFT ₹1–₹3, BBPS bill payment ₹0.50–₹3, AEPS cash withdrawal ₹2–₹5. Enterprise clients on committed volumes negotiate flat-rate pricing. Sandbox access is free and unlimited. Contact sales for a custom quote based on your expected transaction volume.
Key Terms
- API
- Application Programming Interface — a structured software interface that lets applications communicate with each other over the internet using defined endpoints, authentication, and data formats.
NxtBanking is India's AI-powered fintech API platform trusted by hundreds of fintechs, BC networks, NBFCs, and enterprise companies. Our unified API marketplace covers payout (IMPS, NEFT, RTGS, UPI), BBPS bill payment with 20,000+ billers, AEPS biometric banking, KYC and identity verification (Aadhaar, PAN, Bank, Driving Licence, Voter ID, RC), UPI collection and QR codes, domestic money transfer (DMT), mobile and DTH recharge, Micro-ATM, and travel APIs — all under one master agreement, one set of credentials, and one consolidated monthly invoice.
Every NxtBanking API is backed by a 99.9% uptime SLA, real-time webhook delivery, a full-featured sandbox environment with simulated error scenarios, comprehensive API documentation with Postman collections and code samples in multiple languages, and dedicated technical onboarding support. Production go-live for most APIs is achievable within 7–15 business days after KYC and compliance review. For enterprise clients requiring custom SLAs, dedicated infrastructure, or white-label platform builds, NxtBanking offers tailored commercial terms with no minimum volume commitment at the pilot stage.
