Your Stripe integration is done. Checkout flow works. Test cards go through. You flip to live mode, push to production, and wait for your first real payment. But stripe live mode not working.
It never arrives.
No error on your end. The customer sees “Payment failed” or worse, gets charged but your webhook never fires. You check Stripe Dashboard: the payment is sitting there as incomplete or requires_action.
This post was written for developers searching “stripe test mode works live mode fails” and losing real revenue because of it.
This is one of the most common Stripe integration failures, and it almost always comes down to the same handful of misconfigurations that test mode is designed to hide from you.
Test mode is forgiving by design. It skips 3D Secure challenges, ignores webhook signature validation, and doesn’t enforce SCA requirements. Live mode does all of those, silently. By the time you notice, you’ve already lost real transactions.
Stripe test mode is not a simulation of live mode. It’s a simplified sandbox that deliberately skips the hardest parts of payment processing: 3D Secure, bank declines, and rate limits.
Here’s exactly what breaks and how to fix it.
Why stripe live mode not working while test mode passes
Stripe test mode and live mode use completely different API keys, webhook signing secrets, and authentication requirements. A checkout flow that passes every test card can fail 100% of real transactions if these differences aren’t accounted for.

What is Stripe Test Mode?
Stripe test mode is a sandbox environment that lets you build and test payment flows without processing real money. It uses separate API keys (pk_test_ / sk_test_), accepts only synthetic test card numbers (like 4242 4242 4242 4242), and deliberately simplifies behaviors like 3D Secure, bank declines, and rate limiting so you can iterate quickly.
What is Stripe Live Mode?
Stripe live mode is the production environment where real money moves. It uses live API keys (pk_live_ / sk_live_), processes actual credit card charges through banking networks, and enforces every security protocol including SCA/3D Secure challenges, real bank decline logic, and strict webhook signature verification.
Here’s what test mode skips that live mode enforces:
- Test mode skips 3D Secure / SCA challenges entirely
- Test mode uses different API keys (obvious, but the #1 copy-paste error)
- Webhook signing secrets are different per mode
- Test mode doesn’t enforce idempotency strictly
- Rate limiting is relaxed in test mode
- Test cards never trigger bank-side declines
- Products, prices, and customers created in test mode don’t exist in live mode
Test Mode vs Live Mode Behavior Differences to understand why Stripe live mode not working
| Feature | Test Mode | Live Mode |
|---|---|---|
| 3D Secure / SCA | Skipped unless you use a specific 3DS test card | Enforced by banks in EU, UK, India and expanding markets |
| Webhook Signature | Validated but failures are more forgiving | Strict enforcement, wrong secret = silent 400 errors |
| Card Declines & Bank Behavior | Only triggered by specific test card numbers (e.g. 4000 0000 0000 0002) | Real bank rules: country restrictions, spending limits, fraud scoring, risk flags |
| Rate Limiting & Concurrency | Relaxed; you can hammer the API freely | Strict per-key rate limits; bursts can trigger 429 responses |
| Products / Prices / Customers | Exist only in test mode dashboard | Completely separate: you must recreate them in live |
| Idempotency Enforcement | Loosely enforced | Strict: reusing keys with different params throws IdempotencyError |
This table alone explains why “stripe checkout works in test but fails in production” is one of the most common developer complaints.
The Live Mode Failure Stack
Every live mode failure you’ll encounter falls into one of these categories. Fix them in order. Most issues resolve by step 2.
1. API Key & Environment Variable Mismatch
The simplest but most common error.
pk_test_vspk_live_— wrong key in your frontendsk_test_vssk_live_— wrong key in your backend- Environment variable not set in production (Vercel, Railway, Render, etc.)
.env.localvs.env.productionconfusion
How to check:
// Check which key you're using
console.log(process.env.STRIPE_SECRET_KEY?.substring(0, 8));
// Should show "sk_live_" in production
Real error message examples:
No such token: tok_xxx→ test token used in live modeInvalid API Key provided→ env variable empty or wrong mode
2. 3D Secure / SCA Authentication Failures
Test mode’s biggest lie: it skips 3D Secure entirely.
What is SCA / 3D Secure in Stripe?
SCA (Strong Customer Authentication) is a European regulation requiring two-factor authentication for online payments. Stripe implements this through 3D Secure. The popup or redirect that asks a cardholder to verify their identity with their bank. In test mode, this step is skipped. In live mode, it’s mandatory for most EU, UK, and Indian transactions.
- SCA is mandatory in EU, UK, India and expanding to more markets
- In test mode,
pm_card_visaalways succeeds, no 3DS challenge is triggered - In live mode, the cardholder’s bank may require 3DS →
requires_actionstatus → payment stays incomplete
Failure pattern:
payment_intent.status = "requires_action"
// Frontend that doesn't handle this → customer sees "Payment failed"
// Your money never reaches Stripe
Fix:
- When using
stripe.confirmCardPayment(), ensurehandleActions: true - Listen for the
payment_intent.requires_actionwebhook event - Implement
stripe.handleNextAction()on the frontend
To simulate 3DS even in test mode:
// Test card that requires 3DS authentication:
4000 0027 6000 3184
// Use this in test mode — you'll see the 3DS challenge flow
3. Webhook Signature Mismatch
When you switch from test mode to live mode, the webhook signing secret changes. If you don’t update it, your webhooks fail silently.
- Every Stripe endpoint has its own signing secret (
whsec_...) - Test mode secret ≠ Live mode secret
- Calling
stripe.webhooks.constructEvent()with the wrong secret → 400 error - Error message:
Webhook signature verification failed
How to check:
# Test live webhook with Stripe CLI:
stripe listen --forward-to localhost:3000/api/webhook
# Dashboard → Webhooks → check event logs
Additional checks:
- Webhook endpoint URL must point to production
https://required (no localhost, no http)- Check which events the webhook listens for:
checkout.session.completed,payment_intent.succeeded,invoice.paid
This is exactly why “stripe webhook works locally but not in production” is such a common search. The signing secret changes and nobody realizes it.
Test vs Live Product/Price IDs: Why They Can’t Be Found anf stripe live mode not working
This one hits especially hard if you’re building on Bubble, Make, or any SaaS platform where you configure Stripe product IDs through a UI.
The core issue: Products and prices you create in test mode do not exist in live mode. They are completely separate databases. When your app sends a test-mode price_xxx to the live API, Stripe responds with No such price or resource_missing.
Common patterns that cause this:
- Test and live dashboards are completely separate. A price ID like
price_1Abc123created in test mode has no equivalent in live. You must manually create the matching product and price in the live dashboard (or via the live API). - Hard-coded test price IDs shipped to production. You copy
price_1Abc123from your test dashboard into your codebase, it works locally, you deploy, and live customers getNo such price. - No environment-based ID mapping. Your code uses the same price ID string regardless of environment.
- Bubble/Make/Zapier configs pointing to test objects. Visual builders often store product IDs in settings; easy to forget updating them for live mode.
Fix: Create an environment-based price map:
// config/stripe.ts
const PRICE_IDS = {
starter: {
monthly: process.env.NODE_ENV === 'production'
? 'price_live_starter_monthly' // from live dashboard
: 'price_test_starter_monthly', // from test dashboard
yearly: process.env.NODE_ENV === 'production'
? 'price_live_starter_yearly'
: 'price_test_starter_yearly',
},
pro: {
monthly: process.env.NODE_ENV === 'production'
? 'price_live_pro_monthly'
: 'price_test_pro_monthly',
yearly: process.env.NODE_ENV === 'production'
? 'price_live_pro_yearly'
: 'price_test_pro_yearly',
},
};
export default PRICE_IDS;
This pattern also prevents the embarrassing scenario where you create a customer-facing checkout link with a test price ID. The page loads but the “Pay” button throws resource_missing.
Stripe Live Mode Is Working But Payments Still Fail for Some Users (actually means stripe live mode not working)
Your live mode integration is technically correct, you’ve verified keys, webhooks fire, and your own test payment went through. But some customers still see “Payment failed.” This section addresses why.
The answer: it’s not always your code. Sometimes it’s the bank, sometimes it’s 3D Secure edge cases, and sometimes it’s user behavior.
Bank-Side Declines
When a real payment hits live mode, it passes through the customer’s bank before Stripe processes it. Banks can decline for reasons that never show up in test mode. The reasons stripe live mode not working:
- Country restrictions: Some banks block international transactions by default. A customer in Brazil paying your US-based Stripe account may get auto-declined.
- Spending limits: Daily or per-transaction limits on the cardholder’s account.
- Risk/fraud scoring: The bank’s internal fraud engine flags the transaction as suspicious, especially for first-time payments to an unknown merchant.
- 3DS policy mismatch: Some banks require 3D Secure for every transaction, even for amounts that Stripe’s SCA engine considers exempt.
You’ll see these in your Stripe Dashboard under “Decline code.” Common codes:
card_declined→ generic bank refusaldo_not_honor→ bank refuses without a specific reasoninsufficient_funds→ self-explanatorytry_again_later→ temporary bank-side issueauthentication_required→ bank requires 3DS but your flow didn’t trigger it
3D Secure Edge Cases That Break Your Flow
Even if your 3DS implementation works for most users, some edge cases can make stripe live mode not working:
- Bank requires 3DS but your integration doesn’t handle the redirect. Some banks mandate 3DS even for recurring payments or small amounts. If your flow doesn’t call
handleNextAction(), the payment sits asrequires_actionforever. - User closes the 3DS popup/tab. The PaymentIntent stays
incomplete. Without a recovery flow (an email, a retry prompt), that revenue is gone. - iframe-based 3DS blocked by browser settings. Some privacy-focused browsers or extensions block third-party iframes, which is how some banks render their 3DS challenge.
This is why “stripe 3d secure not working for some customers” shows up so often. It’s not a single bug, it’s a combinaiton of bank policies and browser environments.
Network, Timeout, and Abandoned Payment Scenarios
- Customer’s internet drops mid-3DS challenge → PaymentIntent stays
requires_action - Customer closes the tab during checkout → PaymentIntent is
incomplete - Your server times out while waiting for Stripe’s response → you don’t know if the charge went through
Recovery strategies:
- Implement
payment_intent.payment_failedwebhook handler to log and alert - Use
payment_intent.requires_actionwebhook to trigger a follow-up email - For abandoned checkouts, use Stripe’s recovery emails or build your own
Decision Tree: Is This My Bug or the Bank’s?
When a payment fails in live mode, run through this:
- Check Dashboard → Payment → Status. Is it
failed,incomplete, orrequires_action?requires_action→ Your frontend isn’t handling 3DS. That’s your code.failedwith a decline code → Go to step 2.incomplete→ Customer abandoned checkout or network dropped. Go to step 3.
- Read the decline code. Dashboard → Payment → Failure message.
authentication_required→ Your code didn’t trigger 3DS when the bank wanted it. Fix your flow.card_declined,do_not_honor,insufficient_funds→ Bank-side refusal. Not your bug. Advise the customer to contact their bank.incorrect_cvc,expired_card→ Customer input error. Improve your checkout form UX.
- Check webhook logs. Dashboard → Webhooks → Recent events.
- Events delivered with
200? → Your webhook is fine, the issue is upstream. - Events showing
400or5xx? → Your webhook handler is broken. - No events fired? → Your webhook endpoint isn’t registered for this event type, or the URL is wrong.
- Events delivered with
- Check Stripe status page. Rare, but Stripe outages happen. status.stripe.com
How to Safely Test in Live Mode with a Real Card
This is one of the most frequently asked questions: “How do I test live mode without risking real customer money?”
The short answer: use your own card, charge a minimal amount, and refund immediately.
The Difference Between Test Mode Test Cards and Live Mode Real Cards
Test mode test cards (like 4242 4242 4242 4242) are synthetic. They don’t exist in the real banking network. They only work in test mode. If you enter a test card number in live mode, Stripe will reject it immediately. Live mode requires a real card, processed through real banking infrastructure.
Safe Live Testing Steps
- Use your own debit or credit card. You are the customer and the merchant, no risk to anyone else.
- Set the amount low. $1 to $5 USD/EUR is enough to validate the full flow: checkout → payment confirmation → webhook → your database update.
- Run the complete flow. Don’t just check if the charge succeeds. Verify:
- Payment appears in Stripe Dashboard as
succeeded - Your webhook handler receives
payment_intent.succeededorcheckout.session.completed - Your app/database reflects the purchase (subscription active, access granted, etc.)
- Payment appears in Stripe Dashboard as
- Refund immediately. Stripe Dashboard → Payment → Refund. Full refunds are free on Stripe. No fees charged. The refund typically reflects on your card within 5-10 business days.
- Test edge cases. If possible, try from a different device, different browser, or VPN to a different country to see if 3DS triggers differently.
Important: Refunding doesn’t reverse the Stripe processing fee on the original charge. For a $1 test, the fee is negligible (~$0.33), but keep it in mind.
PaymentIntent ‘incomplete’ / ‘requires_action’ Quick Troubleshooting Guide
PaymentIntent status issues are the #1 source of confused posts on Reddit and Stack Overflow. Here’s a quick reference for the most common statuses and what to do about them:
| PaymentIntent Status | What It Means | What To Do |
|---|---|---|
requires_payment_method | Customer hasn’t entered card details yet, or the previous attempt was declined | Show the payment form again. If declined, display the error message and let them retry |
requires_confirmation | Payment method attached but not confirmed | Call stripe.confirmCardPayment() on frontend or paymentIntent.confirm() on backend |
requires_action | 3D Secure challenge pending — customer needs to authenticate | Call stripe.handleNextAction() on frontend. Show a loading/redirect UI |
processing | Payment is being processed — wait | Don’t change anything. Listen for payment_intent.succeeded or payment_intent.payment_failed webhooks |
requires_capture | Payment authorized but not yet captured (manual capture mode) | Call paymentIntent.capture() on backend within 7 days |
succeeded | Payment complete | Deliver the product/service. This is your happy path |
canceled | Payment was canceled | No action needed. Optionally log for analytics |
Common mistakes that cause requires_action to stay stuck:
- Frontend redirects away before
handleNextAction()resolves - Using an iframe that blocks the 3DS popup
- Not handling the
payment_intent.requires_actionwebhook for server-driven flows - Assuming all payments resolve synchronously. They don’t in SCA markets
Step-by-Step Live Mode Checklist
Production’a geçmeden önce bu listeyi sırayla çalıştır. Her adımın başında neden önemli olduğu açıklanmış:
- API Key check – Prevents
No such tokenandInvalid API Keyerrors. Frontend:pk_live_, backend:sk_live_. Don’t assume; print and verify.console.log('Stripe key prefix:', process.env.STRIPE_SECRET_KEY?.substring(0, 8)); // Must show "sk_live_" in production - Environment variable audit – Prevents silent failures from empty env vars. Check your production host (Vercel/Railway/Render) → Settings → Environment Variables. If you added variables after the last deploy, you must redeploy for them to take effect.
- Webhook signing secret update – Prevents
Webhook signature verification failederrors. Stripe Dashboard → Webhooks → your live endpoint → Signing secret. Copy it to your production env asSTRIPE_WEBHOOK_SECRET. - Webhook endpoint URL – Prevents webhooks from firing into the void. Must start with
https://, must point to your production domain (not localhost, not http). - Product/Price ID check – Prevents
No such priceorresource_missingerrors. Ensure all price IDs in your code match the live dashboard, not test. Use an environment-based config map. - 3D Secure flow test – Prevents
requires_actionpayments from getting stuck. Use test card4000 0027 6000 3184in test mode to verify your frontend handles 3DS correctly. Confirm the popup appears and the payment completes. - Idempotency key strategy – Prevents
IdempotencyErrorand duplicate charges. Every unique payment attempt should use a freshuuid(). Never reuse keys across different parameters.import { v4 as uuidv4 } from 'uuid'; const paymentIntent = await stripe.paymentIntents.create( { amount: 1000, currency: 'usd', payment_method: pmId }, { idempotencyKey: uuidv4() } ); - Stripe Dashboard → Logs – Catch errors before your customers report them. Live mode → Developers → Logs. Any 4xx responses are red flags.
- Real card test payment – The final end-to-end validation. Use your own card, charge $1, verify the complete flow (dashboard + webhook + your app), then refund.
The Errors Nobody Talks About
You’ve fixed the big 3 (keys, 3DS, webhooks) but something still breaks.
Idempotency Key Reuse:
- Sending the same idempotency key with different parameters →
IdempotencyError - Not regenerating the key in retry logic → payment locked to first attempt’s params
- Fix: generate a new
uuid()for every unique payment attempt
API Version Mismatch:
- Test mode on an older API version, live mode on a newer one (or vice versa)
- Stripe Dashboard → Developers → API Version: check both modes
- Webhook event formats change between versions. This causes silent breakage
Restricted API Keys:
- If you’re using scoped keys: verify the live key has the right permissions
- Missing
charges:writeorpayment_intents:write→ 403
Currency Mismatch:
- Testing with
usdbut live customers in different currencies - Minimum charge amounts vary by currency ($0.50 USD, £0.30 GBP, €0.50 EUR)
- Some currencies don’t support certain payment methods
From a Real Integration: What This Looks Like in Practice
Case 1: Slow Webhook Handler Causing Duplicate Prevention to Block Legitimate Payments
Problem: Payments showing as “succeeded” in Stripe Dashboard, but the app wasn’t granting access. Tests: full green. Live: random failures.
Investigation: The webhook handler was doing too much work synchronously. Database writes, email sending, third-party API calls. Response time was 15-30 seconds. Stripe’s default timeout is much shorter, so it retried the event. The app’s duplicate-charge prevention logic saw the second delivery of the same event, flagged it as a duplicate, and silently dropped it; including the original processing.
Fix:
- Made the webhook handler return
200immediately after validating the signature - Moved all processing to a background job queue
- Changed duplicate detection to check
event.ididempotently (process once, ignore subsequent deliveries, don’t block)
Result: Zero dropped payments. Webhook response time under 500ms.
Case 2: Missing card_payments Capability on Connected Accounts
Problem: Marketplace-style integration with Connected accounts. Direct charges worked in test mode. In live mode, some connected accounts saw 3DS challenges appear but the charge creation would fail with account_invalid.
Investigation: Connected accounts were created but hadn’t completed capability verification for card_payments. Test mode doesn’t enforce capability requirements. Live mode does strictly.
Fix:
- Added a capabilities check before routing payments:
account.capabilities.card_payments === 'active' - Built a dashboard notification for connected accounts with pending capabilities
- Added a fallback flow: if capabilities aren’t active, show “Payment setup incomplete” instead of silently failing
Result: Identified 3 connected accounts with missing capabilities that were silently dropping ~$2,800/month in potential charges.
FAQ: Common Stripe Live Mode Issues
1. Can I use test API keys with live mode?
No. Test keys only access test mode objects. Live mode objects (real payment intents, customers, subscriptions) only work with live keys. Mixing them produces No such token or Invalid request errors.
2. Why does my payment show “incomplete” in the Stripe Dashboard?
Usually because a 3D Secure / SCA challenge wasn’t completed. The payment intent is stuck in requires_action state. Your frontend needs to call stripe.handleNextAction() or stripe.confirmCardPayment() to handle this challenge.
3. My webhooks work locally but not in production. Why?
Three common causes: (1) Webhook signing secret not updated from test to live mode. (2) Endpoint URL still points to localhost or uses HTTP. (3) Your production server isn’t accepting POST requests from Stripe’s IP range (firewall/WAF issue).
4. How do I test 3D Secure without going fully live?
Use Stripe’s 3DS-triggering test cards: 4000 0027 6000 3184 (authentication required, will succeed) and 4000 0082 6000 3178 (authentication fails). These cards trigger the 3DS popup even in test mode.
5. What’s the difference between PaymentIntents and Charges API?
PaymentIntents API is Stripe’s current standard and natively supports SCA/3DS. Charges API is legacy with limited 3DS support. All new integrations should use PaymentIntents.
6. Why don’t test cards work in live mode?
Test card numbers like 4242 4242 4242 4242 and tokens like pm_card_visa are synthetic. They only exist in Stripe’s test environment. Live mode connects to the real card network through Visa, Mastercard, etc. Entering a test card number in live mode will result in an immediate decline.
7. Do payments made in test mode process like real transactions?
No. Test mode payments never touch the banking network. No real money moves, no real 3DS challenges occur (unless you use specific 3DS test cards), and no real bank decline logic applies. If you enter a real card in test mode, it will be rejected.
8. Do cards and cardholders I create in test mode carry over to live?
No. If you’re using Stripe Issuing, test-mode cards, cardholders, and transactions are completely separate from live mode. When you switch to live, everything you created in test is inaccessible. You must recreate all objects in the live environment.
Stripe live mode not working: quick debug checklist
When a live mode payment fails and you don’t know where to start, work through this list in order. Most issues resolve by item 3.
- Dashboard: payment status → incomplete / requires_action / failed?
→ incomplete = customer abandoned or network drop. Set up recovery webhook.
→ requires_action = 3DS pending. Your frontend isn’t calling handleNextAction().
→ failed = read the decline code below. - Decline code: authentication_required or card_declined?
→ authentication_required = your code. Fix the 3DS flow.
→ card_declined / do_not_honor = bank-side refusal. Not your bug.
→ No such token / cannot use test ID = you’re using test keys/IDs in live mode. - API keys: pk_live_ and sk_live_ everywhere in production?
→ Log: process.env.STRIPE_SECRET_KEY?.substring(0, 8) must show “sk_live_”
→ Check environment variables are set on host (Vercel / Railway / Render)
→ Redeploy after adding missing env vars. - Webhook signing secret: test vs live updated?
→ Dashboard → Webhooks → your live endpoint → Signing secret → copy to STRIPE_WEBHOOK_SECRET
→ Wrong secret = silent 400 errors, no webhook processing. - Product / Price IDs: exist in live dashboard?
→ Products and prices created in test don’t exist in live. Recreate them.
→ Use environment-based ID map in config/stripe.ts — never hard-code test IDs. - 3DS flow: handleNextAction() called on frontend?
→ Confirm stripe.confirmCardPayment() runs with handleActions: true
→ Check for iframe-blocking browser extensions / privacy modes - Connected account capabilities: card_payments active?
→ account.capabilities.card_payments === ‘active’ before routing charges
→ Inactive capabilities = 3DS appears but charge fails with account_invalid - Stripe Status: any active incidents?
→ status.stripe.com — rare, but rules out infrastructure failures
If all boxes are checked and payments still fail: the issue is in your payment flow architecture — race conditions, aggressive duplicate prevention, or subscription billing that doesn’t account for SCA re-authentication.
Still Failing After All This?
The Live Mode Failure Stack covers the most common causes. But not every cause. If your API keys are correct, 3D Secure is handled, webhooks are verified, and you’ve ruled out idempotency and versioning issues. The problem is usually in your payment flow architecture.
Race conditions between frontend confirmation and backend webhook processing. Duplicate charge prevention logic that’s too aggressive. Subscription billing cycles that don’t account for SCA re-authentication.
I build and debug Stripe payment integrations for solo founders and small businesses who’d rather not lose real revenue to silent failures. If your payments work in test but break in live, get in touch. Tell me what you’ve already checked and I’ll tell you where to look next.