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

How to fix CORS issues when using Stripe API on frontend

Stripe API calls from the browser fail with CORS errors because Stripe's servers do not allow cross-origin requests from frontend code. The fix is to never call the Stripe API directly from the browser. Instead, create a server-side endpoint that makes the Stripe API call and return the result to your frontend. Use Stripe.js and Elements for client-side payment collection.

What you'll learn

  • Why Stripe API calls fail with CORS errors in the browser
  • How to move Stripe API calls to a server-side endpoint
  • How to use Stripe.js and Elements safely on the frontend
  • The correct client-server payment flow architecture
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate6 min read20 minutesStripe API v2024-12+, Node.js 18+, Any frontend frameworkMarch 2026RapidDev Engineering Team
TL;DR

Stripe API calls from the browser fail with CORS errors because Stripe's servers do not allow cross-origin requests from frontend code. The fix is to never call the Stripe API directly from the browser. Instead, create a server-side endpoint that makes the Stripe API call and return the result to your frontend. Use Stripe.js and Elements for client-side payment collection.

Why Stripe Blocks Frontend API Calls

Stripe intentionally does not set CORS headers on its API responses because the secret key (sk_) should never be in frontend code. If Stripe allowed browser-side API calls, your secret key would be exposed to anyone viewing the page source. The correct architecture is: frontend uses Stripe.js with the publishable key (pk_) for payment collection, then sends a request to YOUR server, which calls the Stripe API with the secret key.

Prerequisites

  • A Stripe account with API keys
  • Node.js 18+ with Express for the backend
  • A frontend application (React, Vue, vanilla JS, etc.)
  • Understanding of client-server communication

Step-by-step guide

1

Understand the correct Stripe payment flow

The payment flow should be: (1) Frontend loads Stripe.js with your publishable key (pk_test_), (2) Customer enters card details in Stripe Elements, (3) Frontend sends the payment method ID or amount to YOUR server, (4) Your server creates the PaymentIntent using the secret key (sk_test_), (5) Your server returns the client secret to the frontend for confirmation.

Expected result: You understand that Stripe API calls happen server-side, while Stripe.js handles secure card collection on the frontend.

2

Remove Stripe secret key calls from frontend code

Search your frontend code for any direct calls to api.stripe.com or usage of your secret key (sk_). These must be moved to your backend server. The publishable key (pk_) is safe for the frontend.

typescript
1// WRONG — this will cause CORS errors and expose your secret key
2const response = await fetch('https://api.stripe.com/v1/payment_intents', {
3 method: 'POST',
4 headers: {
5 'Authorization': 'Bearer sk_test_...', // NEVER do this
6 },
7});
8
9// RIGHT — call YOUR server instead
10const response = await fetch('/api/create-payment-intent', {
11 method: 'POST',
12 headers: { 'Content-Type': 'application/json' },
13 body: JSON.stringify({ amount: 2000, currency: 'usd' }),
14});

Expected result: All Stripe API calls are removed from frontend code and replaced with calls to your own backend endpoints.

3

Create a server-side endpoint for PaymentIntent creation

Add an Express endpoint that creates PaymentIntents on behalf of the frontend. The secret key stays on the server, and the frontend only receives the client_secret needed to confirm the payment.

typescript
1const express = require('express');
2const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
3const cors = require('cors');
4
5const app = express();
6app.use(cors({ origin: 'http://localhost:3000' })); // Your frontend origin
7app.use(express.json());
8
9app.post('/api/create-payment-intent', async (req, res) => {
10 const { amount, currency } = req.body;
11
12 const paymentIntent = await stripe.paymentIntents.create({
13 amount, // in cents: 2000 = $20.00
14 currency: currency || 'usd',
15 automatic_payment_methods: { enabled: true },
16 });
17
18 // Only send the client_secret — NEVER send the full PaymentIntent
19 res.json({ clientSecret: paymentIntent.client_secret });
20});

Expected result: Your server endpoint creates a PaymentIntent and returns only the client_secret to the frontend.

4

Set up Stripe.js and Elements on the frontend

Load Stripe.js with your publishable key and use Stripe Elements for secure card collection. The publishable key (pk_) is designed for frontend use and does not expose any sensitive data.

typescript
1// Frontend code (React example)
2import { loadStripe } from '@stripe/stripe-js';
3import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
4
5// pk_ key is safe for the frontend
6const stripePromise = loadStripe('pk_test_your_publishable_key');
7
8function CheckoutForm() {
9 const stripe = useStripe();
10 const elements = useElements();
11
12 const handleSubmit = async (e) => {
13 e.preventDefault();
14
15 // 1. Create PaymentIntent on YOUR server
16 const res = await fetch('/api/create-payment-intent', {
17 method: 'POST',
18 headers: { 'Content-Type': 'application/json' },
19 body: JSON.stringify({ amount: 2000, currency: 'usd' }),
20 });
21 const { clientSecret } = await res.json();
22
23 // 2. Confirm payment with Stripe.js
24 const { error } = await stripe.confirmPayment({
25 elements,
26 clientSecret,
27 confirmParams: { return_url: 'http://localhost:3000/success' },
28 });
29
30 if (error) console.error(error.message);
31 };
32
33 return (
34 <form onSubmit={handleSubmit}>
35 <PaymentElement />
36 <button type="submit">Pay</button>
37 </form>
38 );
39}

Expected result: The frontend collects card details via Stripe Elements and confirms the payment using the client_secret from your server.

Complete working example

server.js
1require('dotenv').config();
2const express = require('express');
3const cors = require('cors');
4const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
5
6const app = express();
7
8// Allow your frontend origin
9app.use(cors({
10 origin: process.env.FRONTEND_URL || 'http://localhost:3000',
11}));
12app.use(express.json());
13
14// Create PaymentIntent — server-side only
15app.post('/api/create-payment-intent', async (req, res) => {
16 try {
17 const { amount, currency } = req.body;
18
19 if (!amount || amount < 50) {
20 return res.status(400).json({ error: 'Amount must be at least 50 cents' });
21 }
22
23 const paymentIntent = await stripe.paymentIntents.create({
24 amount,
25 currency: currency || 'usd',
26 automatic_payment_methods: { enabled: true },
27 });
28
29 res.json({ clientSecret: paymentIntent.client_secret });
30 } catch (err) {
31 console.error('PaymentIntent error:', err.message);
32 res.status(500).json({ error: 'Failed to create payment intent' });
33 }
34});
35
36// List prices — server-side proxy for product data
37app.get('/api/prices', async (req, res) => {
38 try {
39 const prices = await stripe.prices.list({
40 active: true,
41 expand: ['data.product'],
42 limit: 10,
43 });
44 res.json(prices.data);
45 } catch (err) {
46 res.status(500).json({ error: 'Failed to fetch prices' });
47 }
48});
49
50const PORT = process.env.PORT || 4000;
51app.listen(PORT, () => console.log(`API server on port ${PORT}`));

Common mistakes when fixing CORS issues when using Stripe API on frontend

Why it's a problem: Calling api.stripe.com directly from browser JavaScript

How to avoid: Never call the Stripe API from the frontend. Create a server-side endpoint that makes the API call and returns only the data the frontend needs.

Why it's a problem: Putting the Stripe secret key (sk_) in frontend code

How to avoid: The secret key must only exist on your server. Use the publishable key (pk_) for Stripe.js on the frontend. If your secret key was exposed, rotate it immediately in the Dashboard.

Why it's a problem: Trying to add CORS headers to Stripe's API via a proxy

How to avoid: Don't proxy Stripe API calls just to add CORS headers. The correct fix is to call Stripe from your server. Proxying still risks exposing your secret key.

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

How to avoid: Only return the client_secret to the frontend. The full PaymentIntent contains metadata and information that should stay server-side.

Best practices

  • Never call the Stripe API directly from browser code — always use a server-side proxy
  • Use the publishable key (pk_) on the frontend and the secret key (sk_) on the server only
  • Set up CORS on YOUR server to allow your frontend's origin
  • Use Stripe.js and Elements for secure, PCI-compliant card collection
  • Return only the client_secret from your server — never the full PaymentIntent
  • Validate amounts and currencies on the server before creating PaymentIntents
  • Use environment variables for both keys — never hard-code them

Still stuck?

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

ChatGPT Prompt

I'm getting CORS errors when calling the Stripe API from my React frontend. Write a Node.js Express server that creates PaymentIntents server-side and returns the clientSecret. Show the matching React frontend code using @stripe/react-stripe-js and PaymentElement. Use pk_ on the frontend and sk_ on the server only.

Stripe Prompt

Build a Stripe payment integration with a Node.js Express backend and React frontend. The server creates PaymentIntents with the secret key. The frontend uses Stripe.js with the publishable key and PaymentElement for card collection. Include CORS configuration.

Frequently asked questions

Why does Stripe block CORS requests?

Stripe blocks browser-side API calls because the secret key (sk_) should never be in frontend code. Allowing CORS would encourage developers to put secret keys in client-side JavaScript, which is a major security risk.

Can I use a CORS proxy to call the Stripe API from the frontend?

You should not. While technically possible, a CORS proxy still requires your secret key, creating a security risk. The correct approach is to create your own server endpoint that calls the Stripe API.

Is the publishable key (pk_) safe to use in frontend code?

Yes. The publishable key is designed for client-side use. It can only be used to create tokens and confirm payments — it cannot read customer data, create charges, or perform any sensitive operations.

Do I need CORS on my own server?

Yes. If your frontend and backend are on different origins (e.g., localhost:3000 and localhost:4000), you need to configure CORS on your server using the cors npm package to allow your frontend's origin.

What is the correct Stripe payment flow?

Frontend collects card details via Stripe Elements (pk_ key) → frontend sends amount to your server → server creates PaymentIntent (sk_ key) → server returns client_secret → frontend confirms payment with Stripe.js.

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.