# How to fix CORS issues when using Stripe API on frontend

- Tool: Stripe
- Difficulty: Intermediate
- Time required: 20 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+, Any frontend framework
- Last updated: March 2026

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

## Before you start

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

```
// WRONG — this will cause CORS errors and expose your secret key
const response = await fetch('https://api.stripe.com/v1/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer sk_test_...',  // NEVER do this
  },
});

// RIGHT — call YOUR server instead
const response = await fetch('/api/create-payment-intent', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ amount: 2000, currency: 'usd' }),
});
```

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

```
const express = require('express');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const cors = require('cors');

const app = express();
app.use(cors({ origin: 'http://localhost:3000' })); // Your frontend origin
app.use(express.json());

app.post('/api/create-payment-intent', async (req, res) => {
  const { amount, currency } = req.body;

  const paymentIntent = await stripe.paymentIntents.create({
    amount,  // in cents: 2000 = $20.00
    currency: currency || 'usd',
    automatic_payment_methods: { enabled: true },
  });

  // Only send the client_secret — NEVER send the full PaymentIntent
  res.json({ clientSecret: paymentIntent.client_secret });
});
```

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

```
// Frontend code (React example)
import { loadStripe } from '@stripe/stripe-js';
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';

// pk_ key is safe for the frontend
const stripePromise = loadStripe('pk_test_your_publishable_key');

function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();

  const handleSubmit = async (e) => {
    e.preventDefault();

    // 1. Create PaymentIntent on YOUR server
    const res = await fetch('/api/create-payment-intent', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ amount: 2000, currency: 'usd' }),
    });
    const { clientSecret } = await res.json();

    // 2. Confirm payment with Stripe.js
    const { error } = await stripe.confirmPayment({
      elements,
      clientSecret,
      confirmParams: { return_url: 'http://localhost:3000/success' },
    });

    if (error) console.error(error.message);
  };

  return (
    <form onSubmit={handleSubmit}>
      <PaymentElement />
      <button type="submit">Pay</button>
    </form>
  );
}
```

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

## Complete code example

File: `server.js`

```javascript
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

const app = express();

// Allow your frontend origin
app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:3000',
}));
app.use(express.json());

// Create PaymentIntent — server-side only
app.post('/api/create-payment-intent', async (req, res) => {
  try {
    const { amount, currency } = req.body;

    if (!amount || amount < 50) {
      return res.status(400).json({ error: 'Amount must be at least 50 cents' });
    }

    const paymentIntent = await stripe.paymentIntents.create({
      amount,
      currency: currency || 'usd',
      automatic_payment_methods: { enabled: true },
    });

    res.json({ clientSecret: paymentIntent.client_secret });
  } catch (err) {
    console.error('PaymentIntent error:', err.message);
    res.status(500).json({ error: 'Failed to create payment intent' });
  }
});

// List prices — server-side proxy for product data
app.get('/api/prices', async (req, res) => {
  try {
    const prices = await stripe.prices.list({
      active: true,
      expand: ['data.product'],
      limit: 10,
    });
    res.json(prices.data);
  } catch (err) {
    res.status(500).json({ error: 'Failed to fetch prices' });
  }
});

const PORT = process.env.PORT || 4000;
app.listen(PORT, () => console.log(`API server on port ${PORT}`));
```

## Common mistakes

- **Calling api.stripe.com directly from browser JavaScript** — undefined Fix: 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.
- **Putting the Stripe secret key (sk_) in frontend code** — undefined Fix: 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.
- **Trying to add CORS headers to Stripe's API via a proxy** — undefined Fix: 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.
- **Returning the full PaymentIntent object to the frontend** — undefined Fix: 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

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

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-fix-cors-issues-when-using-stripe-api-on-frontend
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-fix-cors-issues-when-using-stripe-api-on-frontend
