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

How to restrict Stripe API keys on frontend

Only publishable keys (pk_test_ or pk_live_) should appear in frontend code. Secret keys (sk_) must never be exposed client-side. For backend services that need limited access, create restricted API keys in the Stripe Dashboard with only the permissions they need. This guide explains the key types, how to create restricted keys, and how to enforce safe key usage.

What you'll learn

  • The difference between publishable, secret, and restricted Stripe API keys
  • How to create restricted keys with specific permissions
  • How to ensure secret keys never appear in frontend code
  • How to set up API key usage in a typical web application
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner6 min read10 minutesAll platforms, Stripe.js v3+March 2026RapidDev Engineering Team
TL;DR

Only publishable keys (pk_test_ or pk_live_) should appear in frontend code. Secret keys (sk_) must never be exposed client-side. For backend services that need limited access, create restricted API keys in the Stripe Dashboard with only the permissions they need. This guide explains the key types, how to create restricted keys, and how to enforce safe key usage.

Understanding Stripe API Key Types and Frontend Safety

Stripe provides three types of API keys: publishable (for client-side), secret (for server-side), and restricted (for server-side with limited permissions). The most common security mistake is exposing the secret key in frontend JavaScript. Publishable keys can only create tokens and confirm payments — they cannot read customer data, issue refunds, or access your account. Restricted keys let you limit server-side access to only the operations a specific service needs.

Prerequisites

  • A Stripe account
  • Access to Dashboard → Developers → API keys
  • Basic understanding of client-server architecture

Step-by-step guide

1

Understand the three key types

Stripe has three API key types, each with different capabilities and exposure rules.

typescript
1// PUBLISHABLE KEY (pk_test_ or pk_live_)
2// Safe for frontend. Can only:
3// - Create tokens / PaymentMethods
4// - Confirm PaymentIntents (client_secret required)
5// - Initialize Stripe.js / Elements
6const stripe = Stripe('pk_test_yourPublishableKey'); // Frontend OK
7
8// SECRET KEY (sk_test_ or sk_live_)
9// Server-only. Full account access:
10// - Create charges, refunds, customers
11// - Read all data, manage account
12const stripe = require('stripe')('sk_test_...'); // Server ONLY
13
14// RESTRICTED KEY (rk_test_ or rk_live_)
15// Server-only. Custom permissions:
16// - Only what you explicitly grant
17const stripe = require('stripe')('rk_test_...'); // Server ONLY

Expected result: You understand which key type to use where: pk_ for frontend, sk_ or rk_ for server only.

2

Create a restricted API key

Go to Dashboard → Developers → API keys → Create restricted key. Name it descriptively and grant only the permissions needed.

typescript
1// Example: Restricted key for a payment processing service
2// Permissions granted:
3// - PaymentIntents: Write
4// - Charges: Read
5// - Customers: Read
6// - Everything else: None
7
8// In your payment service:
9const stripePayments = require('stripe')(process.env.STRIPE_RESTRICTED_PAYMENTS_KEY);
10
11// This works — PaymentIntents Write is granted
12async function createPayment() {
13 return await stripePayments.paymentIntents.create({
14 amount: 5000,
15 currency: 'usd',
16 payment_method_types: ['card'],
17 });
18}
19
20// This would fail — Refunds permission not granted
21// await stripePayments.refunds.create({ charge: 'ch_123' }); // Error!

Expected result: A restricted key is created that can only perform the operations you explicitly allowed.

3

Set up proper client-server key separation

Build your application so the frontend only uses the publishable key, and all secret/restricted key operations happen on the server.

typescript
1// === FRONTEND (browser) ===
2// Only pk_ key here
3const stripe = Stripe('pk_test_yourPublishableKey');
4
5// Collect card details securely
6const { paymentMethod } = await stripe.createPaymentMethod({
7 type: 'card',
8 card: cardElement,
9});
10
11// Send token to server — NOT the card details
12const response = await fetch('/api/create-payment', {
13 method: 'POST',
14 headers: { 'Content-Type': 'application/json' },
15 body: JSON.stringify({
16 paymentMethodId: paymentMethod.id,
17 amount: 5000,
18 }),
19});
20
21// === SERVER (Node.js) ===
22// Secret or restricted key here
23const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
24
25app.post('/api/create-payment', async (req, res) => {
26 const paymentIntent = await stripe.paymentIntents.create({
27 amount: req.body.amount,
28 currency: 'usd',
29 payment_method: req.body.paymentMethodId,
30 confirm: true,
31 automatic_payment_methods: { enabled: true, allow_redirects: 'never' },
32 });
33 res.json({ status: paymentIntent.status });
34});

Expected result: Frontend uses pk_ key for tokenization, server uses sk_ key for payment processing. No secrets exposed.

4

Detect accidental key exposure

Add a startup check to ensure secret keys are not accidentally bundled into frontend code.

typescript
1// Add this check to your frontend build or startup
2// For React/Next.js apps:
3
4// In your frontend config, verify no sk_ keys leak through
5if (typeof window !== 'undefined') {
6 const allScripts = document.querySelectorAll('script');
7 // Your publishable key should start with pk_
8 const key = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
9 if (key && key.startsWith('sk_')) {
10 console.error('CRITICAL: Secret key exposed in frontend!');
11 throw new Error('Stripe secret key detected in client bundle');
12 }
13}
14
15// In Next.js, only NEXT_PUBLIC_ prefixed env vars are bundled
16// So use:
17// NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
18// STRIPE_SECRET_KEY=sk_test_... (server only, no prefix)

Expected result: A runtime check prevents accidental secret key exposure in the frontend bundle.

Complete working example

stripe-key-config.js
1require('dotenv').config();
2
3// Server-side Stripe configuration with key validation
4function initStripe() {
5 const secretKey = process.env.STRIPE_SECRET_KEY;
6 const publishableKey = process.env.STRIPE_PUBLISHABLE_KEY;
7
8 if (!secretKey) {
9 throw new Error('STRIPE_SECRET_KEY environment variable is not set');
10 }
11
12 if (!publishableKey) {
13 throw new Error('STRIPE_PUBLISHABLE_KEY environment variable is not set');
14 }
15
16 // Validate key prefixes
17 if (!secretKey.startsWith('sk_') && !secretKey.startsWith('rk_')) {
18 throw new Error('STRIPE_SECRET_KEY must start with sk_ or rk_');
19 }
20
21 if (!publishableKey.startsWith('pk_')) {
22 throw new Error('STRIPE_PUBLISHABLE_KEY must start with pk_');
23 }
24
25 // Warn about live keys in non-production
26 if (process.env.NODE_ENV !== 'production' && secretKey.includes('_live_')) {
27 console.warn('WARNING: Using live Stripe keys outside production!');
28 }
29
30 const stripe = require('stripe')(secretKey, {
31 apiVersion: '2024-12-18.acacia',
32 });
33
34 return { stripe, publishableKey };
35}
36
37const { stripe, publishableKey } = initStripe();
38
39// Express app
40const express = require('express');
41const app = express();
42app.use(express.json());
43
44// Endpoint to send publishable key to frontend
45app.get('/api/config', (req, res) => {
46 res.json({ publishableKey });
47});
48
49// Payment endpoint using secret key
50app.post('/api/create-payment-intent', async (req, res) => {
51 try {
52 const pi = await stripe.paymentIntents.create({
53 amount: req.body.amount,
54 currency: 'usd',
55 payment_method_types: ['card'],
56 });
57 res.json({ clientSecret: pi.client_secret });
58 } catch (err) {
59 res.status(400).json({ error: err.message });
60 }
61});
62
63app.listen(3000, () => console.log('Server running on port 3000'));
64
65module.exports = { stripe, publishableKey };

Common mistakes when restricting Stripe API keys on frontend

Why it's a problem: Putting sk_test_ or sk_live_ in frontend JavaScript or HTML

How to avoid: Only pk_ keys go in frontend code. All sk_ operations must happen on your server.

Why it's a problem: Using NEXT_PUBLIC_ prefix for secret keys in Next.js

How to avoid: Only publishable keys should use NEXT_PUBLIC_ prefix. Secret keys without the prefix stay server-side only.

Why it's a problem: Using the full secret key when a restricted key would suffice

How to avoid: Create restricted keys with minimal permissions for each service to limit blast radius if compromised.

Why it's a problem: Not validating key type at startup

How to avoid: Add startup checks that verify sk_ keys are on the server and pk_ keys are on the client.

Best practices

  • Use publishable keys (pk_) exclusively in client-side code — they can only tokenize cards and confirm payments
  • Create restricted keys for backend services that do not need full account access
  • Never prefix secret keys with NEXT_PUBLIC_, VITE_, or REACT_APP_ — these expose values to the browser
  • Add a /api/config endpoint to serve the publishable key to the frontend instead of hardcoding it
  • Run a pre-commit hook that scans for sk_ patterns in frontend files
  • Use separate restricted keys per microservice or function

Still stuck?

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

ChatGPT Prompt

Explain the difference between Stripe publishable, secret, and restricted API keys. How do I make sure my secret key never appears in frontend code? Show me a safe setup for a Node.js + React app.

Stripe Prompt

Set up a secure Stripe key configuration for my app. I need the publishable key served to the frontend via an API endpoint, restricted keys for different backend services, and validation to prevent accidental key exposure.

Frequently asked questions

What can someone do with my publishable key?

Very little. Publishable keys can only create tokens and PaymentMethods and confirm payments that already have a client_secret. They cannot read customer data, create charges, or access your account.

What can someone do with my secret key?

Everything. Create charges, issue refunds, read customer data, modify account settings. Treat it like a password to your bank account.

How many restricted keys can I create?

Stripe allows up to 25 restricted keys per account. Each can have a unique set of permissions.

Do restricted keys work in test mode?

Yes. You can create restricted keys for both test mode (rk_test_) and live mode (rk_live_). Test them thoroughly before going live.

Can RapidDev help audit my Stripe key configuration?

Yes. RapidDev can review your Stripe integration for security issues including key exposure, permission scoping, and webhook signature verification.

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.