A payment integration is more than displaying a pay button: you need to handle statuses, errors and security.
The client experience must be smooth: a clear form, real-time feedback, card error handling and a clean redirect. Stripe Elements or Checkout handle the sensitive part (card numbers) without your servers ever touching raw banking data.
The real challenge of a payment integration is server-side. Stripe notifies your backend via webhooks when an event occurs: successful payment, refund, dispute. You need to listen, validate the signature and update the order state idempotently.
An order goes through several states: pending, processing, paid, failed, refunded. Modelling these transitions clearly in your database is essential to avoid ghost orders or double shipments.
Always validate the webhook signature, log every event and make your handlers idempotent. In case of a Stripe retry, the same event must not trigger the same action twice.
app.post("/webhook", async (req, res) => {
const event = req.body;
if (event.type === "checkout.session.completed") {
await updateOrderStatus(event.data.object.id, "paid");
}
res.status(200).json({ received: true });
});