Skip to content
BlackFig

v0 Stripe payments: from the sandbox to taking real money

AntPublished Updated 4 min read

Checked against v0's documentation on 10 November 2026.

Quick answer

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.

On this page
  1. Symptom
  2. Why it happens
  3. The fix
  4. How to prevent it
  5. When to get help

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

  1. 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_ and pk_test_, and live keys start sk_live_ and pk_live_.

  2. 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!);
}
  1. 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's payment_status before 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 });
}
  1. 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.

  1. 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.local as STRIPE_WEBHOOK_SECRET. Then stripe trigger payment_intent.succeeded, or complete a checkout with a test card, and watch your handler.

  2. 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 from event.data.object, and select checkout.session.completed. Copy its whsec_ secret into the Production environment as STRIPE_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 200 quickly 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.

The launch checklist for founders, as a PDF

About 40 checks across legal, payments, domain and email, analytics, backups and support.

You get the PDF now and two short follow-ups over the next week. Reply “stop” and the rest never arrive.

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