Stop 22 · Build Backend Portfolio ProjectsProjects · 8/14

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.

The purple blob shopkeeper smiles as a speaker box chimes while a customer holds up a phone.

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

Checkout page
Your API
Stripe

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

ts
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

~45 min

Quick check

Why should you store the IDs of webhook events you've processed?

Go deeper

  1. 1Implement a checkout flow using Stripe's test mode API.
  2. 2Handle Stripe webhooks correctly, including signature verification.
  3. 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.

Download
InsideEdge