v0 Stripe payments: from the sandbox to taking real money
v0 adds Stripe through the Vercel Marketplace, which creates a Stripe sandbox and adds the API keys to your Vercel project. To take real payments you connect a live Stripe account, redeploy, and add a verified webhook so orders are only fulfilled when Stripe confirms the payment.
Symptom
If Stripe has tried to reach your webhook, the answer is usually in Stripe Workbench. Open Webhooks, select the endpoint and check the Event deliveries tab. Stripe's docs list each delivery as Delivered, Pending or Failed, with the HTTP status. A 4xx there means your endpoint refused the request; a 3xx means a redirect, which Stripe counts as a failure.
Why it happens
The integration starts in a sandbox
Stripe in v0 comes from the Vercel Marketplace. v0's docs list Stripe among the Marketplace integrations you install from Project menu then Settings then Integrations. Vercel's Stripe docs say installing it creates a Stripe sandbox or connects your existing account, and provisions the API keys as environment variables.
A sandbox is for testing. The first Publish in v0 adds a Commerce mode step when there are unclaimed Stripe test sandboxes, so you can decide whether to claim them. Claiming is not the same as going live. Until a live account is connected, the app has sandbox keys, and Stripe's docs say a sandbox cannot accept real payment methods.
Live keys need a new deployment
To go live, Vercel's guide has you open the Stripe installation page, choose Import Existing Stripe Account from the Install menu, then redeploy so the app picks up the live keys. Vercel states that variable changes only apply to new deployments. Until you publish again, the running app still has the old keys.
Nothing listens for the payment
Redirecting to a success page is not proof of payment. Stripe sends the result to a webhook, and Stripe's docs require the handler to verify the Stripe-Signature header against the raw request body using a whsec_ signing secret. Vercel's integration docs name two keys, STRIPE_SECRET_KEY and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, and say nothing about a webhook secret. If your generated code has no webhook route, or one that parses JSON before verifying, fulfilment either never runs or runs for anyone who posts to it.
Previews are protected
v0 creates a preview deployment for each working branch. Vercel's Standard Protection covers every deployment except production domains. Stripe's docs say a 4xx can mean the destination URL has access restrictions (401, 403). A webhook pointed at a protected preview fails every time.
The fix
-
Check which Stripe account the project uses. In v0, open Project menu then Settings then Environment Variables and confirm the Stripe keys exist for both Development (the preview reads only Development) and Production. Stripe's docs say sandbox keys start
sk_test_andpk_test_, and live keys startsk_live_andpk_live_. -
Create sessions on the server, with prices you control. Vercel's security notes say checkout sessions, payment intents, webhooks and refunds belong on the server. Never take an amount from the browser:
// app/checkout/actions.ts
"use server";
import Stripe from "stripe";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const ALLOWED_PRICES = new Set([process.env.STRIPE_PRICE_PRO!]);
export async function startCheckout(formData: FormData) {
const priceId = String(formData.get("priceId"));
if (!ALLOWED_PRICES.has(priceId)) throw new Error("Unknown price");
const origin = (await headers()).get("origin") ?? "http://localhost:3000";
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${origin}/thanks?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${origin}/pricing`,
});
redirect(session.url!);
}- Add a verified webhook route. In the App Router, read the body with
req.text()so it reaches Stripe's library untouched. Stripe's fulfilment guide also says to check the session'spayment_statusbefore fulfilling, because some payment methods complete checkout before the money arrives:
// app/api/stripe/webhook/route.ts
import Stripe from "stripe";
import { markOrderPaid } from "@/lib/orders";
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function POST(req: Request) {
const body = await req.text(); // raw body: do not JSON.parse before verifying
const signature = req.headers.get("stripe-signature");
if (!signature) return new Response("Missing signature", { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Invalid signature", { status: 400 });
}
if (event.type === "checkout.session.completed") {
const session = event.data.object;
if (session.payment_status !== "unpaid") {
await markOrderPaid({ sessionId: session.id, eventId: event.id });
}
}
return new Response(null, { status: 200 });
}- Make fulfilment safe to repeat. Stripe's docs say events can arrive more than once and in any order, and recommend logging processed event IDs. A unique constraint does that for you:
create table processed_stripe_events (
event_id text primary key,
processed_at timestamptz not null default now()
);Insert the event ID inside the same transaction that marks the order paid. If the insert conflicts, the event was already handled, so return 200 and stop.
-
Test locally with the Stripe CLI. Run
stripe listen --forward-to localhost:3000/api/stripe/webhook. It prints a signing secret; put that in.env.localasSTRIPE_WEBHOOK_SECRET. Thenstripe trigger payment_intent.succeeded, or complete a checkout with a test card, and watch your handler. -
Go live in order. Connect your live account (on the Vercel Stripe installation page, open the Install menu and choose Import Existing Stripe Account). Then create an event destination in Workbench pointing at
https://yourdomain.com/api/stripe/webhook. Choose the Snapshot payload, not Thin, because the handler above reads the full session fromevent.data.object, and selectcheckout.session.completed. Copy itswhsec_secret into the Production environment asSTRIPE_WEBHOOK_SECRET, then select Publish Changes so a fresh deployment reads everything. Stripe's docs note that test and live use different signing secrets for the same endpoint.
How to prevent it
- Keep payment logic in two files: the Server Action that creates sessions and the webhook route. Tell v0 so when you prompt; its docs recommend specific, detailed instructions.
- Return
200quickly and move slow work after it. Stripe's docs say to respond before complex logic that could time out. - Never point a webhook at a preview URL. Use the CLI for local work and production for live.
- Re-run one real purchase after every publish that touches checkout, then refund it.
My broader guide to adding Stripe to an AI-built app covers subscriptions and customer portals. Payments go live with the rest of the app, so read deploying a v0 app to production alongside this, and the wider prototype to product guide.
When to get help
Payments are where a small mistake costs real money, either by charging without fulfilling or fulfilling without charging. If you want a second pair of eyes before launch, run the free vibe code health check. If the app needs more than payments fixed, the Rescue Sprint covers auth, data and errors too.
- v0 docs: Deployments (Commerce mode in the publish flow), checked 10 November 2026
- v0 docs: Agentic features (Marketplace integrations, Stripe), checked 10 November 2026
- v0 docs: Vercel Integration (preview reads Development variables), checked 10 November 2026
- Vercel docs: Stripe Integration, checked 10 November 2026
- Vercel changelog: Stripe is now generally available on the Marketplace and v0, checked 10 November 2026
- Vercel docs: Environment variables, checked 10 November 2026
- Vercel docs: Deployment Protection, checked 10 November 2026
- Stripe docs: API keys, checked 10 November 2026
- Stripe docs: Receive Stripe events in your webhook endpoint, checked 10 November 2026
- Stripe docs: Fulfil orders, 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
Does v0 create a real Stripe account?
The Vercel Stripe integration starts with a Stripe sandbox, which only takes test payments. The v0 publish flow shows a Commerce mode step for unclaimed Stripe test sandboxes, and you connect a live account when you are ready.
Which Stripe keys does the integration add?
Vercel's docs name STRIPE_SECRET_KEY for server code and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY for the browser. They do not mention a webhook signing secret, so add that yourself.
Why does my Stripe webhook fail on a preview URL?
Preview deployments are usually behind Vercel Deployment Protection, and Stripe treats a 401 or 403 as a failed delivery. Point the live webhook at your production domain.

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