Careeroza
Real World Example
Payment Webhook Like Razorpay
After a customer pays, Razorpay (or Stripe, PhonePe) calls your server with a webhook. You must verify the signature, because anyone could POST fake "payment successful" events.
// app/api/webhooks/razorpay/route.ts
import crypto from 'node:crypto';
import { markOrderPaid } from '@/lib/orders';
export async function POST(req: Request) {
const body = await req.text(); // raw body is needed for the signature
const signature = req.headers.get('x-razorpay-signature') ?? '';
const expected = crypto.createHmac('sha256', process.env.RAZORPAY_WEBHOOK_SECRET!).update(body).digest('hex');
if (signature.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return Response.json({ error: 'Invalid signature' }, { status: 400 });
}
const event = JSON.parse(body) as { event: string; payload: { payment: { entity: { notes: { orderId: string } } } } };
if (event.event === 'payment.captured') {
await markOrderPaid(event.payload.payment.entity.notes.orderId);
}
return Response.json({ received: true });
}Signature Check, Runnable
Example
The same HMAC check with node:crypto:
import crypto from 'node:crypto';
const secret = 'whsec_test_123';
const body = JSON.stringify({ event: 'payment.captured', orderId: 'ORD-1001', amount: 179900 });
function sign(payload: string): string {
return crypto.createHmac('sha256', secret).update(payload).digest('hex');
}
function verify(payload: string, signature: string): boolean {
const expected = sign(payload);
return signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
const sig = sign(body);
console.log('signature :', sig.slice(0, 16) + '...');
console.log('genuine :', verify(body, sig));
console.log('tampered :', verify(body.replace('179900', '100'), sig));
console.log('bad header:', verify(body, 'abc'));Output
signature : d4237d8af23937b5... genuine : true tampered : false bad header: false