6 min read

How to Set Up Payment Intents in Stripe

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.

javascript
npm install stripe
Install the Stripe Node SDK

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

javascript
const stripe = require('stripe')('sk_test_...');
// or for ES modules:
// import Stripe from 'stripe';
// const stripe = new Stripe('sk_test_...');
Initialize Stripe with your secret key

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.

javascript
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 });
});
Create a Payment Intent on your server
Watch out: The 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.

javascript
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>
  );
}
Initialize Stripe.js and 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.

javascript
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();
Fetch the client secret

Confirm the Payment Intent with the client secret

Use confirmCardPayment() to complete the payment. Pass the client secret and payment details from the CardElement.

javascript
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');
}
Confirm the payment and check the status
Tip: Use 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.

javascript
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 });
});
Listen for payment_intent.succeeded events

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.

Watch out: Webhooks are asynchronous and can be delayed. Payments can also be in 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_secret in 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.

Track these metrics automatically

Product Analyst connects to your stack and surfaces the insights that matter.

Try Product Analyst — Free