Stripe's Payment Intent API is the standard way to accept payments today—it handles payment failures, retries, and multi-step flows like 3D Secure without you building complex state machines. If you're still treating payment handling as a simple charge-and-move-on operation, you're missing fraud protection and recovery flows that Payment Intents give you out of the box.
Create a Payment Intent on Your Server
Every payment starts with a Payment Intent. You create it on your server, then use the client secret to confirm the payment from your frontend.
Install the Stripe SDK
Use the official Node.js SDK to interact with Stripe's API. Install it via npm.
npm install stripeInitialize the Stripe client with your secret key
Set up the SDK in your backend code. Your secret key is private—never expose it to the frontend.
const stripe = require('stripe')('sk_test_...');
// or for ES modules:
// import Stripe from 'stripe';
// const stripe = new Stripe('sk_test_...');Create a Payment Intent endpoint
Build an API endpoint that creates a Payment Intent. Accept the amount and currency from your frontend, then call stripe.paymentIntents.create(). Return the client_secret to the frontend—this is what the client uses to confirm the payment.
app.post('/create-payment-intent', async (req, res) => {
const { amount, currency } = req.body;
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: currency,
automatic_payment_methods: { enabled: true }
});
res.json({ clientSecret: paymentIntent.client_secret });
});amount is in the smallest currency unit (cents for USD). So $10.00 = 1000.Confirm the Payment Intent on the Client
Once you have the client secret, use Stripe.js on the frontend to confirm the payment. This step handles all payment method types—cards, digital wallets, bank transfers—without you needing to know the details.
Load Stripe.js and initialize Elements
Include Stripe.js from the CDN or install it as a package. Then create a Stripe instance with your publishable key and initialize Elements, which gives you pre-built form components.
import { loadStripe } from '@stripe/js';
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js';
const stripePromise = loadStripe('pk_test_...');
function PaymentForm() {
return (
<Elements stripe={stripePromise}>
<CheckoutForm />
</Elements>
);
}Fetch the client secret from your endpoint
Call your backend endpoint to create the Payment Intent and get the client_secret. Store it so you can use it in the next step.
const response = await fetch('/create-payment-intent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: 5000, currency: 'usd' })
});
const { clientSecret } = await response.json();Confirm the Payment Intent with the client secret
Use confirmCardPayment() to complete the payment. Pass the client secret and payment details from the CardElement.
const stripe = useStripe();
const elements = useElements();
const result = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: elements.getElement(CardElement),
billing_details: { name: 'Jane Doe' }
}
});
if (result.paymentIntent.status === 'succeeded') {
console.log('Payment successful');
}confirmPayment() instead of confirmCardPayment() if you're using the latest Stripe.js—it handles all payment method types, not just cards.Handle Payment Status and Webhooks
Payments can fail, succeed, or require additional action like 3D Secure verification. Set up webhooks to know when a payment completes so you can fulfill the order.
Set up a webhook endpoint
Create an endpoint in your backend that listens for Stripe events. Use stripe.webhooks.constructEvent() to verify the signature and parse the event. Listen for payment_intent.succeeded.
app.post('/webhook', express.raw({type: 'application/json'}), async (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') {
const paymentIntent = event.data.object;
console.log(`Payment successful: ${paymentIntent.id}`);
// Fulfill the order here
}
res.json({ received: true });
});Register the webhook in the Stripe Dashboard
Go to Developers > Webhooks in the Stripe Dashboard. Add an endpoint URL (e.g., https://yoursite.com/webhook) and select the events you want to listen for. Stripe will send you a signing secret—store it as STRIPE_WEBHOOK_SECRET.
Fulfill the order only after the webhook fires
Don't rely on the client-side confirmation alone. Always confirm fulfillment via the webhook event. Stripe retries failed webhooks, so use the paymentIntent.id as an idempotency key to avoid double-fulfilling.
processing or requires_action state. Check the status before fulfilling.Common Pitfalls
- Creating multiple Payment Intents for the same purchase. A new intent = a new charge attempt. Reuse the intent's client secret if the user needs to retry.
- Ignoring payment status beyond 'succeeded'. Payments can be 'processing' or 'requires_action' (3D Secure). Always check the final status before fulfilling.
- Shipping the
client_secretin URL parameters. It's sensitive data—pass it in response bodies or session storage, not query strings. - Forgetting to verify webhook signatures. Anyone can POST to your endpoint. Always validate with
stripe.webhooks.constructEvent().
Wrapping Up
You now have a live Payment Intent flow: create on the server, confirm on the client, and listen for webhooks. This setup handles everything from simple card payments to 3D Secure fraud checks without you writing complex state logic. If you want to track payment performance, conversion metrics, and churn across Stripe and your other tools, Product Analyst can help.