Securealy
Guide · Payments · Stripe

Is my app running Stripe in test mode in production?

Look at the publishable key in your page source. pk_test_ means every checkout the live domain opens runs in sandbox mode: test cards succeed, real cards are refused, no money moves, and nothing in your logs says so. pk_live_ means production. The server key and the webhook must match the same mode.

How do I check in thirty seconds?

Fetch the home page and search for the key prefix. Stripe keys name their mode: pk_test_ and sk_test_ are sandbox keys, pk_live_ and sk_live_ are live, rk_ are restricted. One curl against the live domain shows which publishable key the page ships; the check does the same on every grade.

curl -s https://YOUR_DOMAIN | grep -o 'pk_[a-z]*_' | sort -u
curl -s https://YOUR_DOMAIN/_next/static/chunks/*.js 2>/dev/null | grep -o 'pk_[a-z]*_' | sort -u   # or your bundle URL

Stripe's key documentation lists the prefixes: sandbox keys start with pk_test_ for publishable keys, rk_test_ for restricted keys, and sk_test_ for secret keys; live keys start with pk_live_, rk_live_ and sk_live_. Stripe now calls test mode a sandbox throughout its documentation; the prefixes have not changed. stripe-test-key-in-prod reads the key from the home page and its first-party bundles: pk_live_ passes, pk_test_ on a production domain fails, and pk_test_ on a preview or platform subdomain only warns, because that is where test keys belong.

A publishable key is public by design, so finding one is not a finding; only its mode is. If the grep also turns up sk_, stop reading this page and go to the rotation guide: a secret key in the bundle can refund, charge and read every customer, and secrets-in-bundle caps the grade at D for it.

What breaks when production runs on a test key?

Everything about money, quietly. Real cards are declined, the customer gives up, and no error reaches your logs because sandbox mode worked as designed. Objects live in one mode only, so a customer created in the sandbox does not exist in live mode and a live webhook never sees them.

The documentation is exact about the wall between modes: each mode has its own set of API keys, and objects in one mode aren't accessible to the other. Products, prices, customers, subscriptions and webhook endpoints all exist twice or not at all. A page with a test publishable key talking to a server with a live secret key fails every checkout session; the reverse succeeds with test cards and declines real ones.

The failure arrives on launch day. A sports data product in the Fix Kit's own history took its first paying signup and landed the user on a page that said the page was not working; the keys had been set with echo and carried a trailing newline, so the Authorization header was corrupt and the error read "connection error, retried 2 times". Mode mismatches fail the same way: nothing in your logs, one customer who left. The kit's rule since then is that every env read trims, and every key is set with printf or pasted into the dashboard.

Is my webhook pointed at the right domain and mode?

Check it separately, because payments succeed without it and your app never finds out. A webhook endpoint is registered per mode and per URL, and its whsec_ signing secret differs between modes. An endpoint created against the preview URL, or in the sandbox, delivers nothing to the live domain.

Stripe's webhook documentation shows the signing secret beginning with whsec_ on each endpoint's settings page, and notes that if you use the same endpoint for both test and live API keys, the secret is different for each one. So going live means a second registration: https://YOUR_DOMAIN/api/webhook in live mode, subscribed to the events your handler reads (checkout.session.completed, the customer.subscription events, invoice.paid, invoice.payment_failed, charge.refunded), and its live whsec_ value in the Production environment only.

The silent failure is a delivery that fails signature verification, which shows as a 400 in the Stripe dashboard and nowhere in yours. The customer paid, Stripe is the ledger of record, your own table is what the app enforces, and the webhook is the only thing that writes one into the other. stripe-webhook-wiring runs on your machine with your own secret key, never printed, and reports four things: an endpoint exists for the production domain, it is enabled, it is in the same mode as the key your site runs on, and it subscribes to the checkout and payment events. Pass is all four.

How do I switch to live mode safely?

In order: live mode in the dashboard, payouts to a bank account, the live secret key in Production only with test keys kept on Preview, the catalogue mirrored in live mode, the live webhook registered with its own secret, a deploy, one grep for pk_live_, and one real purchase with your own card refunded from the dashboard.

  1. Switch the Stripe dashboard to live mode. Under Settings, Payouts, confirm the destination is a bank account, not a Financial Account, or revenue sits inside Stripe where nobody looks. Configure the customer portal again; it is per mode.
  2. Developers, API keys: the live secret key goes into STRIPE_SECRET_KEY for Production only. Keep the test key on Preview so previews cannot charge anyone. Paste it in the dashboard, or printf %s 'sk_live_...' | vercel env add STRIPE_SECRET_KEY production. Set the live publishable key the same way under whatever name the page reads.
  3. Mirror products and prices in live mode with an idempotent setup script run in your own terminal, so the key never passes through an AI, and paste each printed price_ id into Production.
  4. Register the live webhook at the live domain and set its whsec_ on Production.
  5. Redeploy, wait for Ready, then verify with single requests: the grep above returns pk_live_ only, and npx securealy check-stripe reports the endpoint.
  6. Buy the smallest plan with your own card and refund it. Stripe keeps the card fee on that one charge, about 30 cents plus 2.9%, which is the price of knowing.

Every step a human does deliberately, because it creates real charges. The written version with the webhook handler template is Fix Kit Module 04; the check page carries the free summary.

Grade it

Read the key your live domain ships

The grade reports the Stripe key mode on your domain, warns on a preview and fails on production. Add your own secret key locally and it checks the webhook too.

Checks this guide refers to: stripe-test-key-in-prod, stripe-webhook-wiring, secrets-in-bundle, vercel-env-hygiene, legal-pages.