Stripe Live Mode Not Working: 7 Root Causes and Fixes


  • Share on Pinterest

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.


Table of Contents

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.

stripe live mode not working

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

FeatureTest ModeLive Mode
3D Secure / SCASkipped unless you use a specific 3DS test cardEnforced by banks in EU, UK, India and expanding markets
Webhook SignatureValidated but failures are more forgivingStrict enforcement, wrong secret = silent 400 errors
Card Declines & Bank BehaviorOnly 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 & ConcurrencyRelaxed; you can hammer the API freelyStrict per-key rate limits; bursts can trigger 429 responses
Products / Prices / CustomersExist only in test mode dashboardCompletely separate: you must recreate them in live
Idempotency EnforcementLoosely enforcedStrict: 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_ vs pk_live_ — wrong key in your frontend
  • sk_test_ vs sk_live_ — wrong key in your backend
  • Environment variable not set in production (Vercel, Railway, Render, etc.)
  • .env.local vs .env.production confusion

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 mode
  • Invalid 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_visa always succeeds, no 3DS challenge is triggered
  • In live mode, the cardholder’s bank may require 3DS → requires_action status → 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(), ensure handleActions: true
  • Listen for the payment_intent.requires_action webhook 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.completedpayment_intent.succeededinvoice.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:

  1. Test and live dashboards are completely separate. A price ID like price_1Abc123 created 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).
  2. Hard-coded test price IDs shipped to production. You copy price_1Abc123 from your test dashboard into your codebase, it works locally, you deploy, and live customers get No such price.
  3. No environment-based ID mapping. Your code uses the same price ID string regardless of environment.
  4. 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 refusal
  • do_not_honor → bank refuses without a specific reason
  • insufficient_funds → self-explanatory
  • try_again_later → temporary bank-side issue
  • authentication_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 as requires_action forever.
  • 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_failed webhook handler to log and alert
  • Use payment_intent.requires_action webhook 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:

  1. Check Dashboard → Payment → Status. Is it failedincomplete, or requires_action?
    • requires_action → Your frontend isn’t handling 3DS. That’s your code.
    • failed with a decline code → Go to step 2.
    • incomplete → Customer abandoned checkout or network dropped. Go to step 3.
  2. 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_declineddo_not_honorinsufficient_funds → Bank-side refusal. Not your bug. Advise the customer to contact their bank.
    • incorrect_cvcexpired_card → Customer input error. Improve your checkout form UX.
  3. Check webhook logs. Dashboard → Webhooks → Recent events.
    • Events delivered with 200? → Your webhook is fine, the issue is upstream.
    • Events showing 400 or 5xx? → Your webhook handler is broken.
    • No events fired? → Your webhook endpoint isn’t registered for this event type, or the URL is wrong.
  4. 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

  1. Use your own debit or credit card. You are the customer and the merchant, no risk to anyone else.
  2. Set the amount low. $1 to $5 USD/EUR is enough to validate the full flow: checkout → payment confirmation → webhook → your database update.
  3. 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.succeeded or checkout.session.completed
    • Your app/database reflects the purchase (subscription active, access granted, etc.)
  4. 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.
  5. 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 StatusWhat It MeansWhat To Do
requires_payment_methodCustomer hasn’t entered card details yet, or the previous attempt was declinedShow the payment form again. If declined, display the error message and let them retry
requires_confirmationPayment method attached but not confirmedCall stripe.confirmCardPayment() on frontend or paymentIntent.confirm() on backend
requires_action3D Secure challenge pending — customer needs to authenticateCall stripe.handleNextAction() on frontend. Show a loading/redirect UI
processingPayment is being processed — waitDon’t change anything. Listen for payment_intent.succeeded or payment_intent.payment_failed webhooks
requires_capturePayment authorized but not yet captured (manual capture mode)Call paymentIntent.capture() on backend within 7 days
succeededPayment completeDeliver the product/service. This is your happy path
canceledPayment was canceledNo 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_action webhook 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ış:

  1. API Key check – Prevents No such token and Invalid API Key errors. 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
  2. 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.
  3. Webhook signing secret update – Prevents Webhook signature verification failed errors. Stripe Dashboard → Webhooks → your live endpoint → Signing secret. Copy it to your production env as STRIPE_WEBHOOK_SECRET.
  4. Webhook endpoint URL – Prevents webhooks from firing into the void. Must start with https://, must point to your production domain (not localhost, not http).
  5. Product/Price ID check – Prevents No such price or resource_missing errors. Ensure all price IDs in your code match the live dashboard, not test. Use an environment-based config map.
  6. 3D Secure flow test – Prevents requires_action payments from getting stuck. Use test card 4000 0027 6000 3184 in test mode to verify your frontend handles 3DS correctly. Confirm the popup appears and the payment completes.
  7. Idempotency key strategy – Prevents IdempotencyError and duplicate charges. Every unique payment attempt should use a fresh uuid(). 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() } );
  8. Stripe Dashboard → Logs – Catch errors before your customers report them. Live mode → Developers → Logs. Any 4xx responses are red flags.
  9. 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:write or payment_intents:write → 403

Currency Mismatch:

  • Testing with usd but 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:

  1. Made the webhook handler return 200 immediately after validating the signature
  2. Moved all processing to a background job queue
  3. Changed duplicate detection to check event.id idempotently (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:

  1. Added a capabilities check before routing payments: account.capabilities.card_payments === 'active'
  2. Built a dashboard notification for connected accounts with pending capabilities
  3. 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.