Payment Integration Backend (Stripe)
Part of the Backend Developer Roadmap
In short
Money code has no room for "mostly works". Integrating Stripe in test mode teaches webhooks, idempotency and the golden rule: the client never decides a payment succeeded.
A backend integrating a real payment provider (Stripe test mode) — practice for webhooks, idempotency, and handling money-related edge cases correctly.

Think of it like this
The UPI soundbox at the counter
A customer can flash any screen saying "paid". The shopkeeper waits for the soundbox to announce the amount, because that message comes from the bank, not the customer. Stripe webhooks are your soundbox.
Watch it happen
A card payment, the safe way
POST /checkout
1/7The page sends the order ID. The server works out the amount.
The big ideas
What you need to know
Webhooks are the truth
Mark orders paid only from a verified webhook, never because the browser said so.
Verify signatures
Use the raw request body and your webhook secret with stripe.webhooks.constructEvent. Reject anything that fails.
Idempotency everywhere
Send Idempotency-Key when creating payments, and store processed event IDs, because webhooks can arrive twice.
Smallest currency unit
Stripe amounts are integers in the smallest unit, so ₹499 is 49900. Never use floats for money.
See it in code
Walk through the code
1import Stripe from "stripe";2const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);3 4app.post("/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => {5 let event: Stripe.Event;6 try {7 event = stripe.webhooks.constructEvent(req.body, req.headers["stripe-signature"]!, process.env.STRIPE_WEBHOOK_SECRET!);8 } catch {9 return res.status(400).send("Bad signature");10 }11 if (await alreadyProcessed(event.id)) return res.sendStatus(200); // Stripe may retry12 if (event.type === "payment_intent.succeeded") {13 await markOrderPaid(event.data.object.metadata.orderId, event.id);14 }15 res.sendStatus(200);16});1/4This route must get the raw body, not parsed JSON, or the signature check fails.
express.raw is essential: signature checks need the exact bytes Stripe sent. Test locally with the Stripe CLI's stripe listen command.
Save yourself the pain
Common mistakes
Oops
Marking the order paid when the frontend redirects to /success.
Do this instead
Anyone can open /success. Wait for the verified payment_intent.succeeded webhook.
Oops
Putting app.use(express.json()) before the webhook route.
Do this instead
The JSON parser consumes the body. Mount the webhook route with express.raw first, or exclude it.
Your mission
Milestone 1: test-mode checkout
Quick check
Why should you store the IDs of webhook events you've processed?
Go deeper
- 1Implement a checkout flow using Stripe's test mode API.
- 2Handle Stripe webhooks correctly, including signature verification.
- 3Use idempotency keys to ensure a retried payment request never double-charges.
More projects at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.