Two gateways, one job: take the money, then prove it's real. Every step of an Express integration, side by side, so you can wire up whichever one — or both — your backend needs.
The shape of the integration is identical — create a payment, collect it, verify it — but almost nothing underneath matches.
PaymentIntent — tracks a payment through its whole lifecycle.Order — created first, then paid against via Checkout.1099 = $10.99.1099 = ₹10.99.stripe.webhooks.constructEvent().crypto module.striperazorpayBoth SDKs follow the same shape: install the package, then construct a client with your secret key. That key stays server-side — it never belongs in frontend code.
require('dotenv').config(); const express = require('express'); const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY); const app = express(); app.use(express.json());
require('dotenv').config(); const express = require('express'); const Razorpay = require('razorpay'); const app = express(); app.use(express.json()); const razorpay = new Razorpay({ key_id: process.env.RAZORPAY_KEY_ID, key_secret: process.env.RAZORPAY_KEY_SECRET, });
The client asks your server for something to pay against. Stripe hands back a client_secret; Razorpay hands back an order_id. Neither gateway charges anyone yet.
app.post('/create-payment', async (req, res) => { const { amount, currency = 'usd' } = req.body; try { const paymentIntent = await stripe.paymentIntents.create({ amount: Math.round(amount * 100), // smallest unit currency, automatic_payment_methods: { enabled: true }, }); res.json({ clientSecret: paymentIntent.client_secret }); } catch (err) { res.status(500).json({ error: err.message }); } });
app.post('/create-payment', async (req, res) => { const { amount, currency = 'INR' } = req.body; try { const order = await razorpay.orders.create({ amount: Math.round(amount * 100), // paise currency, receipt: `receipt_${Date.now()}`, }); res.json({ orderId: order.id, amount: order.amount, keyId: process.env.RAZORPAY_KEY_ID, }); } catch (err) { res.status(500).json({ error: err.message }); } });
This step isn't Express at all — it's the browser talking to the gateway's own UI. Stripe embeds inline; Razorpay pops up a modal.
<script src="https://js.stripe.com/v3/"></script> <script> const stripe = Stripe('pk_test_...'); async function pay() { const { clientSecret } = await fetch('/create-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 10.99 }), }).then(r => r.json()); const elements = stripe.elements({ clientSecret }); elements.create('payment').mount('#payment-element'); // on form submit: await stripe.confirmPayment({ elements, confirmParams: { return_url: 'https://yoursite.com/done' }, }); } </script>
<script src="https://checkout.razorpay.com/v1/checkout.js"></script> <script> async function pay() { const { orderId, amount, keyId } = await fetch('/create-payment', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: 10.99 }), }).then(r => r.json()); const rzp = new Razorpay({ key: keyId, amount, order_id: orderId, handler: function (response) { // response.razorpay_payment_id, .razorpay_order_id, .razorpay_signature verifyPayment(response); }, }); rzp.open(); } </script>
The browser telling you "it worked" isn't proof — anyone can fake that request. Only your server, checking a signature only the gateway could have produced, can actually confirm it.
app.post( '/webhook', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['stripe-signature']; let event; try { event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return res.status(400).send(`Webhook Error: ${err.message}`); } if (event.type === 'payment_intent.succeeded') { // mark the order paid in your own DB } res.json({ received: true }); } );
const crypto = require('crypto'); app.post('/verify-payment', (req, res) => { const { razorpay_order_id, razorpay_payment_id, razorpay_signature, } = req.body; const body = razorpay_order_id + '|' + razorpay_payment_id; const expected = crypto .createHmac('sha256', process.env.RAZORPAY_KEY_SECRET) .update(body) .digest('hex'); if (expected === razorpay_signature) { // mark the order paid in your own DB res.json({ verified: true }); } else { res.status(400).json({ verified: false }); } });
express.json() parses the body first, you're hashing a re-serialized copy that won't match — mount express.raw() on the webhook route specifically, before any JSON-parsing middleware runs.
This runs Razorpay's exact algorithm — HMAC-SHA256 over order_id|payment_id — live in your browser, so you can watch what "tampered" actually looks like.
This computation normally happens only on your server — the secret key never reaches the browser. It's running client-side here purely so the mechanism is visible; don't do this in production.