Express.js · Node.js

StripevsRazorpay

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.

Stripe Razorpay
Quick compare

Same job, different receipts

The shape of the integration is identical — create a payment, collect it, verify it — but almost nothing underneath matches.

Attribute
Stripe
Razorpay
Region & methods
StripeGlobal reach — cards, wallets, local methods that vary by country.
RazorpayBuilt for India — UPI, netbanking, wallets, and cards, one checkout.
Payment object
StripePaymentIntent — tracks a payment through its whole lifecycle.
RazorpayOrder — created first, then paid against via Checkout.
Amount unit
StripeSmallest currency unit — cents for USD, e.g. 1099 = $10.99.
RazorpayPaise — always ×100, e.g. 1099 = ₹10.99.
Client checkout
StripeElements — embeds inline on your own page.
RazorpayCheckout.js — opens as a hosted modal overlay.
Verify payment
StripeBuilt into the SDK — stripe.webhooks.constructEvent().
RazorpayManual HMAC-SHA256 via Node's own crypto module.
npm package
Stripestripe
Razorpayrazorpay
1

Install & initialize

Both 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.

stripe · server.js
npm install express stripe dotenv
require('dotenv').config();
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();
app.use(express.json());
razorpay · server.js
npm install express razorpay dotenv
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,
});
2

Create a payment

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.

stripe · POST /create-payment
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 });
  }
});
razorpay · POST /create-payment
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 });
  }
});
3

Collect payment, client-side

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.

stripe · checkout.html
<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>
razorpay · checkout.html
<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>
4

Verify the payment

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.

stripe · POST /webhook
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 });
  }
);
razorpay · POST /verify-payment
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 });
  }
});
⚠️ Both need the raw body. A signature check hashes the exact bytes the gateway sent. If 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.
Try it

Why the signature check actually matters

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.

computing…
computing…
checking…

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.