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
Understand the three key types
Understand the three key types
Stripe has three API key types, each with different capabilities and exposure rules.
1// PUBLISHABLE KEY (pk_test_ or pk_live_)2// Safe for frontend. Can only:3// - Create tokens / PaymentMethods4// - Confirm PaymentIntents (client_secret required)5// - Initialize Stripe.js / Elements6const stripe = Stripe('pk_test_yourPublishableKey'); // Frontend OK78// SECRET KEY (sk_test_ or sk_live_)9// Server-only. Full account access:10// - Create charges, refunds, customers11// - Read all data, manage account12const stripe = require('stripe')('sk_test_...'); // Server ONLY1314// RESTRICTED KEY (rk_test_ or rk_live_)15// Server-only. Custom permissions:16// - Only what you explicitly grant17const stripe = require('stripe')('rk_test_...'); // Server ONLYExpected result: You understand which key type to use where: pk_ for frontend, sk_ or rk_ for server only.
Create a restricted API key
Create a restricted API key
Go to Dashboard → Developers → API keys → Create restricted key. Name it descriptively and grant only the permissions needed.
1// Example: Restricted key for a payment processing service2// Permissions granted:3// - PaymentIntents: Write4// - Charges: Read5// - Customers: Read6// - Everything else: None78// In your payment service:9const stripePayments = require('stripe')(process.env.STRIPE_RESTRICTED_PAYMENTS_KEY);1011// This works — PaymentIntents Write is granted12async function createPayment() {13 return await stripePayments.paymentIntents.create({14 amount: 5000,15 currency: 'usd',16 payment_method_types: ['card'],17 });18}1920// This would fail — Refunds permission not granted21// await stripePayments.refunds.create({ charge: 'ch_123' }); // Error!Expected result: A restricted key is created that can only perform the operations you explicitly allowed.
Set up proper client-server key separation
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.
1// === FRONTEND (browser) ===2// Only pk_ key here3const stripe = Stripe('pk_test_yourPublishableKey');45// Collect card details securely6const { paymentMethod } = await stripe.createPaymentMethod({7 type: 'card',8 card: cardElement,9});1011// Send token to server — NOT the card details12const 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});2021// === SERVER (Node.js) ===22// Secret or restricted key here23const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);2425app.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.
Detect accidental key exposure
Detect accidental key exposure
Add a startup check to ensure secret keys are not accidentally bundled into frontend code.
1// Add this check to your frontend build or startup2// For React/Next.js apps:34// In your frontend config, verify no sk_ keys leak through5if (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}1415// In Next.js, only NEXT_PUBLIC_ prefixed env vars are bundled16// 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
1require('dotenv').config();23// Server-side Stripe configuration with key validation4function initStripe() {5 const secretKey = process.env.STRIPE_SECRET_KEY;6 const publishableKey = process.env.STRIPE_PUBLISHABLE_KEY;78 if (!secretKey) {9 throw new Error('STRIPE_SECRET_KEY environment variable is not set');10 }1112 if (!publishableKey) {13 throw new Error('STRIPE_PUBLISHABLE_KEY environment variable is not set');14 }1516 // Validate key prefixes17 if (!secretKey.startsWith('sk_') && !secretKey.startsWith('rk_')) {18 throw new Error('STRIPE_SECRET_KEY must start with sk_ or rk_');19 }2021 if (!publishableKey.startsWith('pk_')) {22 throw new Error('STRIPE_PUBLISHABLE_KEY must start with pk_');23 }2425 // Warn about live keys in non-production26 if (process.env.NODE_ENV !== 'production' && secretKey.includes('_live_')) {27 console.warn('WARNING: Using live Stripe keys outside production!');28 }2930 const stripe = require('stripe')(secretKey, {31 apiVersion: '2024-12-18.acacia',32 });3334 return { stripe, publishableKey };35}3637const { stripe, publishableKey } = initStripe();3839// Express app40const express = require('express');41const app = express();42app.use(express.json());4344// Endpoint to send publishable key to frontend45app.get('/api/config', (req, res) => {46 res.json({ publishableKey });47});4849// Payment endpoint using secret key50app.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});6263app.listen(3000, () => console.log('Server running on port 3000'));6465module.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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation