Skip to main content
RapidDev - Software Development Agency
stripe-guide

How to create a Payment Intent in Stripe

Create a PaymentIntent by calling stripe.paymentIntents.create() on your server with an amount (in cents) and currency. Pass the returned client_secret to your frontend where Stripe.js confirms the payment securely. This gives you full control over the payment UI unlike hosted Checkout.

What you'll learn

  • How to create a PaymentIntent on the server with amount and currency
  • How to pass the client_secret to the frontend securely
  • The PaymentIntent lifecycle and status transitions
  • How to handle errors and incomplete payments
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate5 min read20 minutesStripe API v2024-12+, Node.js 18+, any frontend frameworkMarch 2026RapidDev Engineering Team
TL;DR

Create a PaymentIntent by calling stripe.paymentIntents.create() on your server with an amount (in cents) and currency. Pass the returned client_secret to your frontend where Stripe.js confirms the payment securely. This gives you full control over the payment UI unlike hosted Checkout.

What Is a PaymentIntent and When Should You Use It?

A PaymentIntent represents a single payment attempt in Stripe. Unlike Checkout Sessions (which redirect to a Stripe-hosted page), PaymentIntents let you build a fully custom payment form using Stripe Elements. You create the PaymentIntent on your server, pass the client_secret to your frontend, and Stripe.js handles the rest — including 3D Secure and SCA compliance. Use PaymentIntents when you need full UI control over the checkout experience.

Prerequisites

  • A Stripe account with API keys from Dashboard → Developers
  • Node.js 18+ and the stripe npm package installed
  • An Express server (or equivalent HTTP framework)
  • Stripe.js loaded on your frontend (pk_test_ publishable key)

Step-by-step guide

1

Install dependencies

Install the Stripe SDK for your server. You'll also need a way to serve your frontend and parse JSON request bodies.

typescript
1npm install stripe express

Expected result: Both packages installed successfully.

2

Create the PaymentIntent on your server

Call stripe.paymentIntents.create() with the amount in cents, the currency, and optionally automatic_payment_methods to let Stripe choose the best payment methods for the customer's location.

typescript
1const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
2
3app.post('/create-payment-intent', async (req, res) => {
4 try {
5 const { amount } = req.body; // amount in cents
6
7 const paymentIntent = await stripe.paymentIntents.create({
8 amount: amount,
9 currency: 'usd',
10 automatic_payment_methods: { enabled: true },
11 metadata: { order_id: 'order_123' },
12 });
13
14 // Send ONLY the client_secret to the frontend
15 res.json({ clientSecret: paymentIntent.client_secret });
16 } catch (err) {
17 res.status(500).json({ error: err.message });
18 }
19});

Expected result: The endpoint returns a JSON object with a clientSecret string like pi_xxx_secret_yyy.

3

Load Stripe.js on the frontend

Load Stripe.js using the publishable key (pk_test_). This is the only Stripe key safe to use on the frontend.

typescript
1<script src="https://js.stripe.com/v3/"></script>
2<script>
3 const stripe = Stripe('pk_test_YOUR_PUBLISHABLE_KEY');
4</script>

Expected result: The Stripe object is available in your frontend code.

4

Fetch the client_secret and mount Elements

Call your server to create the PaymentIntent, then use the client_secret to initialize Stripe Elements. The PaymentElement renders an all-in-one payment form.

typescript
1const response = await fetch('/create-payment-intent', {
2 method: 'POST',
3 headers: { 'Content-Type': 'application/json' },
4 body: JSON.stringify({ amount: 2000 }), // $20.00
5});
6const { clientSecret } = await response.json();
7
8const elements = stripe.elements({ clientSecret });
9const paymentElement = elements.create('payment');
10paymentElement.mount('#payment-element');

Expected result: A payment form renders in the #payment-element div, showing card fields and any other enabled payment methods.

5

Test the PaymentIntent

Use Stripe's test card 4242 4242 4242 4242 with any future expiry and any CVC. In your Stripe Dashboard (test mode), you'll see the PaymentIntent transition from requires_payment_method → succeeded.

typescript
1// Test card: 4242 4242 4242 4242
2// Expiry: 12/34
3// CVC: 123

Expected result: The payment succeeds and the PaymentIntent status changes to 'succeeded' in the Stripe Dashboard.

Complete working example

server.js
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3
4const app = express();
5app.use(express.static('public'));
6app.use(express.json());
7
8// Create a PaymentIntent
9app.post('/create-payment-intent', async (req, res) => {
10 try {
11 const { amount, currency = 'usd' } = req.body;
12
13 if (!amount || amount < 50) {
14 return res.status(400).json({ error: 'Amount must be at least 50 cents' });
15 }
16
17 const paymentIntent = await stripe.paymentIntents.create({
18 amount: Math.round(amount), // Ensure integer cents
19 currency,
20 automatic_payment_methods: { enabled: true },
21 metadata: {
22 integration: 'custom_payment_form',
23 },
24 });
25
26 res.json({ clientSecret: paymentIntent.client_secret });
27 } catch (err) {
28 console.error('PaymentIntent error:', err.message);
29 res.status(500).json({ error: err.message });
30 }
31});
32
33// Check PaymentIntent status
34app.get('/payment-status/:id', async (req, res) => {
35 try {
36 const paymentIntent = await stripe.paymentIntents.retrieve(req.params.id);
37 res.json({
38 status: paymentIntent.status,
39 amount: paymentIntent.amount,
40 currency: paymentIntent.currency,
41 });
42 } catch (err) {
43 res.status(500).json({ error: err.message });
44 }
45});
46
47const PORT = process.env.PORT || 3000;
48app.listen(PORT, () => console.log(`Server on port ${PORT}`));

Common mistakes when creating a Payment Intent in Stripe

Why it's a problem: Sending the full PaymentIntent object to the frontend

How to avoid: Only send the client_secret. The full object contains sensitive data. The client_secret is designed to be safe for frontend use.

Why it's a problem: Creating the PaymentIntent on the frontend

How to avoid: PaymentIntents require your secret key (sk_). Always create them on your server and send only the client_secret to the client.

Why it's a problem: Using floating-point amounts instead of integer cents

How to avoid: Stripe expects amounts as integers in the smallest currency unit. Use 2000 for $20.00, not 20 or 20.00.

Why it's a problem: Forgetting to handle the requires_action status

How to avoid: Some payments require 3D Secure. Stripe.js handles this automatically when you use confirmPayment(), but you must handle the redirect flow.

Best practices

  • Always create PaymentIntents server-side using your secret key (sk_test_ or sk_live_)
  • Send only the client_secret to the frontend — never the full PaymentIntent object
  • Use automatic_payment_methods: { enabled: true } to let Stripe enable the best methods
  • Store the PaymentIntent ID in your database to track payment status
  • Use metadata to attach your internal order ID or user ID to the PaymentIntent
  • Validate the amount on your server — never trust client-submitted amounts without checking
  • Set up a webhook for payment_intent.succeeded rather than polling for status

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write a Node.js Express server that creates a Stripe PaymentIntent for a given amount in cents. Return only the client_secret. Then show the frontend code to mount a PaymentElement using that client_secret and confirm the payment.

Stripe Prompt

Add a Stripe payment flow to my app. Create a server endpoint POST /create-payment-intent that accepts an amount, creates a Stripe PaymentIntent, and returns the client_secret. On the frontend, mount a Stripe PaymentElement and handle payment confirmation.

Frequently asked questions

What's the difference between a PaymentIntent and a Checkout Session?

A Checkout Session redirects to a Stripe-hosted payment page. A PaymentIntent lets you build a custom payment form on your own site using Stripe Elements. Use Checkout for speed, PaymentIntents for full UI control.

What is the minimum amount for a PaymentIntent?

The minimum is $0.50 USD (50 cents) or equivalent in other currencies. Stripe will reject amounts below this.

Can I update a PaymentIntent after creating it?

Yes. Call stripe.paymentIntents.update(id, { amount: newAmount }) on your server before the customer confirms payment. You cannot update a PaymentIntent that has already succeeded.

How do I handle 3D Secure authentication?

Stripe.js handles 3D Secure automatically when you use stripe.confirmPayment(). It opens the bank's authentication modal and resolves the promise when the customer completes it.

Is it safe to expose the client_secret?

Yes. The client_secret can only confirm the specific PaymentIntent it belongs to. It cannot create new charges, access your account, or read other data. It's designed for frontend use.

Can RapidDev help with a custom PaymentIntent integration?

Yes. RapidDev can help you build custom payment flows including PaymentIntents, saved cards, subscriptions, and multi-party payments. This is especially useful for complex checkout experiences.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.