Replit Stripe payments: from sandbox to live without breaking checkout
Replit gives you two ways to add Stripe: the Replit Stripe connector, which starts in a sandbox and goes live through the Replit Integrated Payments app, or your own keys in Secrets. Use one method per environment, test with Stripe's test cards, and only mark an order paid when a verified payment event arrives.
Symptom
Checkout works in the preview with a test card. Then one of these:
- A customer pays, Stripe shows the payment, but the order or booking in your app still says unpaid.
- Webhook events fail signature checks in production.
- Agent keeps asking you to paste keys or add a webhook, even though Stripe is already connected.
- You finished business verification in Stripe, but Replit still shows the step as open.
Why it happens
Two methods that don't mix
Replit supports two setups, and its docs are firm that you use one per environment:
| Replit Stripe connector | Your own Stripe keys | |
|---|---|---|
| Plans | Core and Pro, not Starter | You manage it yourself |
| Where it lives | Integrations, then Stripe | Secrets |
| Testing | A Stripe sandbox Agent sets up | Your test keys and the Stripe CLI |
| Going live | Install the Replit Integrated Payments app from Publish | Live keys in Production app secrets |
| Payment events | Agent wires up the integration | You register and verify your own webhook endpoint |
| Products and prices | Sandbox changes sync to your app's database | Create them again in live mode: Stripe objects don't cross between sandbox and live |
Combining them in one environment causes connection conflicts, even when Agent suggests adding a webhook. You can use different methods in each environment, for example the connector's sandbox while building and your own live keys in production.
The sandbox is a separate account
Sandbox payments never touch real money. Replit's docs warn the sandbox integration is not production-ready. Publishing checks for a live connection and stops if there isn't one.
With the connector, product and price changes you make in the sandbox dashboard sync into your app's database. You can see them in the Database tool under My Data by switching the schema selector to Stripe. That is handy for checking what Agent built against, and a good reminder that the payment state your app shows is a copy. The source of truth is Stripe.
One more boundary: if you are building a mobile app and need in-app subscriptions, Replit points you to RevenueCat rather than Stripe.
The success page is not proof of payment
If your app marks an order paid when the customer lands on the success page, anyone who closes the tab early leaves a paid order marked unpaid. Anyone who finds the success URL gets the reverse. Replit's own troubleshooting covers the first case: connect the payment success event to the order status in the database.
The fix
-
Pick one method per environment. Open Integrations, then Stripe, and open Secrets. If the connector is active and you also have
STRIPE_SECRET_KEYin the same environment, remove one. To remove the connector, go to Integrations, then Stripe, then Manage, and disconnect the sandbox and live accounts. -
Test properly in the sandbox. Open the preview in a new tab and pay with
4242 4242 4242 4242, any future expiry and any CVC. Then pay with the decline card4000 0000 0000 0002and confirm nothing gets marked paid. Finally, pay and close the tab before the success page loads. The order should still turn paid. -
Going live with the connector. Select Publish, then Install Stripe app. When Stripe asks for an account, choose your live account, not a sandbox. If the flow picks the sandbox version of the app, choose the Live Mode version yourself. Keep the Stripe window open until it finishes. If your account isn't activated, Stripe runs business verification first. Back in Replit, select Got It to clear the step, then publish.
-
Going live with your own keys. Under Publishing, Adjust settings, Production app secrets, set
STRIPE_SECRET_KEYto a live key startingsk_live_orrk_live_(the name must match exactly) andSTRIPE_PUBLISHABLE_KEYto your live publishable key. Register a separate live webhook endpoint in Stripe at your published URL plus the webhook path. Choose the Snapshot payload and thecheckout.session.completedevent, because the handler below reads the full session from the event. SetSTRIPE_WEBHOOK_SECRETto that endpoint's signing secret, not the onestripe listenprinted, then republish. On a first publish, get the URL first, then finish the endpoint and republish before taking payments. -
Mark orders paid from the webhook. On the own-keys path, the handler must read the raw body, so register it before
express.json():
// server/stripeWebhook.ts
import express from "express";
import Stripe from "stripe";
import { eq } from "drizzle-orm";
import { db } from "./db";
import { orders } from "@shared/schema";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export const stripeWebhook = express.Router();
stripeWebhook.post(
"/api/stripe/webhook",
express.raw({ type: "application/json" }),
async (req, res) => {
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"] as string,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error("Stripe signature check failed:", (err as Error).message);
return res.sendStatus(400);
}
if (event.type === "checkout.session.completed") {
const session = event.data.object;
const orderId = session.metadata?.orderId; // set when you create the session
if (orderId && session.payment_status === "paid") {
await db
.update(orders)
.set({ status: "paid", stripeSessionId: session.id })
.where(eq(orders.id, orderId));
}
}
res.json({ received: true });
}
);// server/index.ts: order matters
app.use(stripeWebhook); // raw body for signature checks
app.use(express.json()); // everything elseThe update is safe to run twice, which matters because Stripe retries deliveries. On the connector path, ask Agent to show you the exact code that changes an order's status after payment, then run the close-the-tab test from step 2 against it.
- Make one real payment. Replit's own guide suggests a low-cost real purchase with your own card before you share the app. Refund it afterwards.
How to prevent it
- Write the payment method into
replit.md, for example "Stripe uses the Replit connector in development and production. Never add STRIPE_SECRET_KEY." Agent reads that file in every session. - On the own-keys path, create products and prices in the live account yourself. Stripe's docs say sandbox objects can't be used in live mode.
- Keep the three test payments from step 2 as a release check, and run them after every change near checkout.
When to get help
Payments are where a small bug costs real money and real trust. The prototype to product guide covers the rest of going live, and adding Stripe to an AI-built app goes deeper on subscriptions, refunds and failed payments. Payment problems often arrive with sign-in problems, so see Replit auth not working too.
Run the free vibe-code health check first. If the app needs more than a payment fix, the Rescue Sprint covers payments, auth and the rest in one go.
- Replit docs: Stripe Payments Integration, checked 10 November 2026
- Replit docs: Accept payments with Stripe, checked 10 November 2026
- Replit docs: Payments and Stripe help, checked 10 November 2026
- Replit docs: Secrets, checked 10 November 2026
- Stripe docs: Receive Stripe events in your webhook endpoint, checked 10 November 2026
- Stripe docs: API keys (sandbox and live mode), checked 10 November 2026
The launch checklist for founders, as a PDF
About 40 checks across legal, payments, domain and email, analytics, backups and support.
Questions founders ask
Can I use the Replit Stripe connector on the free plan?
No. Replit's docs say the Stripe connector is available on paid plans (Core and Pro), and Agent prompts you to upgrade when it proposes it. Your own keys in Secrets are the alternative.
Do I need to paste live Stripe keys into Replit?
Not with the connector. Installing the Replit Integrated Payments app from the Publish pane connects your live account. With your own keys, you set the live values yourself under Production app secrets.
Why does publishing say I need to connect a live Stripe account?
The sandbox only takes test payments. Either install the Stripe app from the Publish pane, or, if production uses your own keys, store a live key starting sk_live_ or rk_live_ under the exact name STRIPE_SECRET_KEY.

Ant
Product designer who builds MVPs, based in Nottingham. 10+ years designing digital products, and the only person at BlackFig. More about me