Symptoms
Clicking "Buy" or "Subscribe" shows an error or does nothing, and your server logs show one of these errors:
No such price: 'price_1Abc...'; a similar object exists in live mode, but a test mode key was used to make this request.No such price: 'price_1Abc...'; a similar object exists in test mode, but a live mode key was used to make this request.No such price: 'price_1Abc...'with no hint after it.No such price: 'prod_...'
The error code is resource_missing. Stripe's description of that code: "The ID provided isn't valid. Either the resource doesn't exist, or an ID for a different resource has been provided" (Stripe: Error codes).
It often starts right after you switch from test keys to live keys, or after you move to a different Stripe account.
Why it happens
Stripe keeps live mode and each sandbox separate. Stripe's docs: "Each mode has its own set of API keys, and objects in one mode aren't accessible to the other. For example, a sandbox product object can't be part of a live mode payment" (Stripe: API keys). A price you created while testing has an ID that doesn't exist in live mode. If your app switches to sk_live_... but still sends the old price_... ID, Stripe returns "No such price".
| What you see | Usual cause | Fix |
|---|---|---|
| "...a similar object exists in test mode, but a live mode key was used" | Live key, test price ID | Use the live price ID |
| "...a similar object exists in live mode, but a test mode key was used" | Test key, live price ID | Use the test price ID, or switch to live keys |
No such price: 'prod_...' | A product ID was sent as the price | Use one of the product's price_... IDs |
No such price: 'price_...' with no hint | A typo, an extra space or line break, a price from a different sandbox, or a price from a different Stripe account | Copy the ID again from the right account and check for hidden characters |
The price specified is inactive. This field only accepts active prices. | The price was archived. This is a different error with a similar fix | Unarchive the price, or use an active price |
Step-by-step fix
1. Find out which mode your key is in
Look at the secret key your server uses. It is in your host's env vars or your app's secrets, not in the browser code.
sk_live_...orrk_live_...: live modesk_test_...orrk_test_...: a sandbox (test mode)
The price ID must come from the same mode and the same account. A price ID doesn't show which mode it belongs to, so you have to check in the Dashboard.
2. Copy the right price ID
- In the Stripe Dashboard, switch to the same mode as your key: live mode, or the sandbox the key belongs to.
- Go to More > Product catalog and open the product.
- Under the product's prices, open the price you charge and copy its ID. It starts with
price_. Don't copy the product ID, which starts withprod_.
No live price yet? If you created your products in a sandbox, open the product and click Copy to live mode in the upper right corner. Stripe copies the product and its prices to live mode, and the live copies have new IDs. Stripe's docs: "Each copy creates a separate live product, and updates you make to the sandbox product later aren't reflected in existing live copies" (Stripe: Manage products and prices).
3. Put it in your server's env var
Store the price ID next to the secret key, so both come from the same mode:
STRIPE_SECRET_KEY=sk_live_...
STRIPE_PRICE_ID=price_...Where you set this depends on your setup: Supabase Edge Function secrets (Lovable and Bolt apps that use Supabase), your Vercel or Netlify project's environment variables, or Replit's deployment secrets. On Vercel and Netlify, redeploy after the change, because a running deployment keeps the values it was deployed with. Supabase says Edge Functions read a new secret immediately, without a redeploy (Supabase: Function secrets).
If the price ID is written directly in the code (AI tools often do this), search the project for price_ and replace each one. Each plan and each billing interval (monthly, yearly) has its own price ID.
4. Remove hidden characters
A price ID pasted with a trailing space or line break gives "No such price" even though it looks right. Trim it in code:
const priceId = (process.env.STRIPE_PRICE_ID ?? '').trim()5. Using Stripe Connect? Pass the connected account
If the price was created on a connected account, not on your platform account, Stripe looks for it on your platform account unless you name the connected account. Send the Stripe-Account header with the connected account's ID (Stripe: Making API calls for connected accounts). In stripe-node that is the stripeAccount option:
await stripe.checkout.sessions.create({ /* ... */ }, { stripeAccount: 'acct_...' })Optional: look prices up by lookup key instead of copying IDs
Give the price the same lookup_key (for example pro_monthly) in the sandbox and in live mode, and look up the ID when the code runs. The same code then works in both modes (Stripe: Lookup keys):
async function getPriceId(stripe: Stripe, lookupKey: string) {
const { data } = await stripe.prices.list({ lookup_keys: [lookupKey], active: true, limit: 1 })
if (!data.length) throw new Error(`No active price with lookup key "${lookupKey}" in this Stripe mode`)
return data[0].id
}Check the key and the price with a script
Run this with the same env vars your server uses. It prints the key's mode and whether Stripe can find the price with that key. It only reads the price and doesn't change anything in your Stripe account.
// check-stripe-price.mjs (npm i stripe, then: node check-stripe-price.mjs)
import Stripe from 'stripe'
const key = (process.env.STRIPE_SECRET_KEY ?? '').trim()
const priceId = (process.env.STRIPE_PRICE_ID ?? '').trim()
const keyMode = /^(sk|rk)_live_/.test(key) ? 'live' : /^(sk|rk)_test_/.test(key) ? 'test/sandbox' : 'unknown'
console.log(`Key mode: ${keyMode}`)
if (!key) {
console.log('STRIPE_SECRET_KEY is empty in this environment.')
process.exit(1)
}
if (process.env.STRIPE_PRICE_ID !== priceId) console.log('Warning: the price ID has spaces or a line break around it.')
if (!priceId.startsWith('price_')) {
console.log(`"${priceId}" is not a price ID. Checkout needs an ID that starts with price_ (not prod_).`)
process.exit(1)
}
const stripe = new Stripe(key)
try {
const price = await stripe.prices.retrieve(priceId)
console.log(`Found ${price.id}: livemode=${price.livemode}, active=${price.active}, product=${price.product}`)
if (!price.active) console.log('This price is archived. Unarchive it or use an active price ID.')
} catch (err) {
console.log(`Stripe says (${err.code}): ${err.message}`)
}If it prints "Found ..." with active=true and the livemode value you expect, the price is fine. Checkout should work once the server uses the same key and price ID.
Fix prompt for your AI builder
Paste this into Lovable, Bolt, Cursor, Replit, v0 or whichever tool you build with:
My Stripe checkout fails with "No such price". Fix only how the code gets the Stripe price ID:
1. Find every place the code uses a Stripe price ID (search for "price_" and "prod_"). Replace hard-coded IDs with server-side env vars, for example STRIPE_PRICE_ID, or one per plan such as STRIPE_PRICE_PRO_MONTHLY. Never put the secret key or the price lookup in browser code.
2. Make sure the value passed to line_items[].price is a price ID that starts with "price_", not a product ID that starts with "prod_".
3. Trim each env var value with .trim() before using it.
4. When creating the Checkout Session fails, log the Stripe error code and message on the server, and show the user a clear "Payment is unavailable right now" message instead of failing silently.
5. Don't change prices, amounts or Stripe keys. Tell me which env vars I need to set, and I will paste the price IDs from the same Stripe mode as my secret key.How to confirm it's fixed
- Open your live site in a private window and click the buy or subscribe button.
- You should reach the Stripe Checkout page with the right product. You don't need to pay; reaching the page is enough.
- In the Stripe Dashboard, open Workbench > Logs. The
POST /v1/checkout/sessionsrequest should have status 200, not an error status such as 400.
How to find out sooner next time
This error can return when someone changes the keys, adds a plan, or an AI edit rewrites the checkout code. After any key or price change, click the buy button once yourself.
Heedza checks on a schedule that your checkout page opens and shows its payment form or pay button, and sends you a screenshot and a fix prompt if it doesn't. It never clicks Pay, so it can't see an error that only happens after a click. If your Stripe Checkout page is only created when someone clicks a button, Heedza may only reach your pricing page. See checkout monitoring.
Let Heedza check this for you. Heedza’s payment page check opens your live payment page on a schedule and tells you if the Pay button disappears. It never pays. It never logs in or fills in forms, so it can’t see errors that only show after that.
Made with Lovable, Bolt, Replit, Cursor or v0? Free for 1 site: start free or see prices.