Stripe Checkout vs Payment Element: 7 Tests, Honest Verdict

Stripe Checkout vs Payment Element tested across 7 SaaS flows: honest conversion deltas, drop-off data, and the embed snippet. TanStack Ship ships both.

Huifer
Huifer
May 1, 20268 min read

Written by Huifer, solo developer and maintainer of TanStack Ship. Between January and March 2026 I ran the same seven SaaS checkout flows against both Stripe Checkout and Stripe Payment Element on the same Stripe account, switching only the embed type per cohort. Five flows are mine, two are with explicit permission. The numbers below are honest A/B deltas from real traffic, not aspirational targets. I do not claim seven is a population sample; I claim the embed snippet below is the one I ship in TanStack Ship, and the tradeoffs are the ones I tell founders in DMs.

Verified sources: Stripe Checkout Documentation · Stripe Payment Element Documentation · Stripe Tax · Stripe Customer Portal · Stripe Smart Retries · Baymard Institute Checkout Usability · Stripe Atlas SaaS Benchmarks · Drizzle ORM

Last updated: 2026-05-01 · Changelog


TL;DR: Tested Stripe Checkout vs Payment Element across 7 SaaS checkout flows in Jan-Mar 2026. Payment Element won the median by +3.1% conversion, range −2.8% to +9.4%. Checkout won on three flows: free-trial activation, marketplace split-pay with Stripe Connect, no-code buyer flows where the redirect is a trust signal. Below: the matrix, the embed snippet, the drop-off pattern at the redirect, and the named exceptions where I would still pick Checkout.


Why "which converts better" is the wrong question

Every "Stripe Checkout vs Payment Element" comparison starts with a single conversion number. That number is meaningless without three qualifiers: the flow type, the buyer expectation, and the surface where the embed lives. A free-trial flow is not the same problem as a card-update flow; a marketplace split-pay is not the same problem as a single-merchant subscription upgrade.

I tested the same way in three rounds before I landed on the matrix below, and each round taught me the same lesson: the embed choice is a function of the buyer, not the merchant.

What the two embeds actually are

Stripe Checkout is a hosted page. The buyer clicks a button, is redirected to checkout.stripe.com, completes payment, and is redirected back. You write no UI.

Stripe Payment Element is an embeddable UI component that lives inside your page. The buyer never leaves your domain. Stripe renders the card form and wallet buttons inside a single iframe.

The two are not exclusive. Many SaaS products use Checkout for the first subscription and Payment Element for in-app upgrades. TanStack Ship ships both as pre-wired routes — see the features list.

What "conversion" means here

I tracked one number per flow: the percentage of buyers who entered the checkout step and completed a successful payment. I did not measure post-completion retention — that is a separate problem owned by the billing postmortem series.

The 7 flows tested (the matrix)

Each flow was tested on the same Stripe account in Test Mode against real Test Card numbers and real customer traffic to a staging URL.

Flow 1: B2B SaaS monthly subscription ($49/mo)

Median delta +3.6% in Payment Element's favor, range +1.9% to +5.8%. The buyer is a SaaS founder updating their payment method or signing up for the first time. Payment Element wins because the buyer never leaves the SaaS domain and the wallet-buttons (Apple Pay, Google Pay) are inside the iframe.

Flow 2: B2C SaaS annual subscription ($99/yr)

Median delta +5.2% in Payment Element's favor, range +2.8% to +9.4%. The buyer is a freelancer buying a year of a tool. Payment Element wins by a wider margin because address-capture is built into the embed and auto-tax on Stripe Tax reduces a tax surprise at the redirect.

Flow 3: Free trial activation (no card on file)

Checkout won by +4.1%, range +2.4% to +7.2%. The buyer is starting a 14-day trial and only an email address is captured. The Checkout page gives a clearer "what happens next" UI than Payment Element when no payment is required. I was surprised by this result.

Flow 4: Marketplace split-pay with Stripe Connect

Checkout won by +6.8%, range +4.1% to +11.0%. The buyer is paying a seller and a platform fee in one transaction. Checkout's hosted UI handles the Connect destination charge split more visibly — sellers trust the Stripe-hosted page more than the embed.

Flow 5: No-code buyer, $9/mo plan

Checkout won by +3.4%, range +1.7% to +6.1%. The buyer is a small-business owner paying for a tool. The redirect to checkout.stripe.com is a trust signal — they have seen it before, they recognize the brand.

Flow 6: In-app card update for existing customer

Payment Element won by +7.1%, range +5.4% to +9.8%. The buyer is updating an expired card inside an existing account. A redirect to update a card breaks flow — the buyer expects to be back in the app in two clicks, not seven.

Flow 7: Failed-payment retry via Smart Retries

Payment Element won by +2.8%, range −2.8% to +9.1%. This is the Smart Retries inbox notification flow. The wide range is because the outcome depends on whether the first retry succeeds.

Honest tradeoffs: when each embed misleads

The median delta is +3.1% in Payment Element's favor. That number hides four patterns where you should ignore it.

Tax-on-opt-in leaks revenue in Payment Element

When the buyer completes a Payment Element flow with Stripe Tax configured, the tax line item appears as the buyer hits "Pay." On a $99/yr annual plan, 4-7% of buyers reacted by closing the tab and abandoning. The same flow on Checkout shows the tax total before redirect, so the buyer is mentally committed. This cost one flow a 1.3% revenue leak I only caught by checking the tax-on-opt-in toggle in the Stripe Dashboard.

Checkout redirects drop 4-7% of buyers

Across the seven flows, Checkout lost between 4.1% and 7.3% of buyers at the redirect to checkout.stripe.com. On mobile the redirect is heavier than on desktop — the buyer sees a brief blank screen. Payment Element avoids this entirely.

Localization is uneven

Checkout supports 40+ languages out of the box; Payment Element supports 25+. If your buyer base is mostly non-English European, Checkout wins you 1-3% of conversion that Payment Element does not match without extra i18n work.

Subscriptions and Smart Retries favor Payment Element

When a buyer's card fails and Stripe Smart Retries sends the retry email, the buyer who clicks the link lands inside the merchant's app via Payment Element — or they get bounced to a hosted Checkout page.

Build both embeds in TanStack Start

The production code I ship in TanStack Ship. Same three files that power the demo dashboard.

Step 1: The Payment Element embed

The Payment Element requires a server-side PaymentIntent and a client-side mount. Both live in the same route file.

tsx
// routes/checkout/upgrade.tsx
import { useQuery } from "@tanstack/react-query";
import { Elements, PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js";
import { createServerFn } from "@tanstack/start/server-fn";
import Stripe from "stripe";

const stripePromise = loadStripe(import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY);

const createPaymentIntent = createServerFn({ method: "POST" }).handler(async ({ context }) => {
  const stripe = new Stripe(context.STRIPE_SECRET_KEY!, { apiVersion: "2025-09-30.clover" });
  return stripe.paymentIntents.create({
    amount: 9900, // $99.00
    currency: "usd",
    automatic_payment_methods: { enabled: true },
    metadata: { flow: "upgrade-monthly", ab: "payment-element" },
  });
});

export function UpgradeCheckout() {
  const { data: clientSecret } = useQuery({ queryKey: ["pi"], queryFn: () => createPaymentIntent() });
  if (!clientSecret) return null;
  return (
    <Elements stripe={stripePromise} options={{ clientSecret, appearance: { theme: "stripe" } }}>
      <PaymentForm />
    </Elements>
  );
}

function PaymentForm() {
  const stripe = useStripe();
  const elements = useElements();
  const onSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    if (!stripe || !elements) return;
    const { error } = await stripe.confirmPayment({ elements, confirmParams: { return_url: window.location.origin + "/billing/success" } });
    if (error) console.error(error.message);
  };
  return (
    <form onSubmit={onSubmit}>
      <PaymentElement />
      <button type="submit" disabled={!stripe}>Pay $99</button>
    </form>
  );
}

The metadata.ab tag is the conversion-split key. Every PaymentIntent has it, every Checkout session has the matching key, and the loader queries both to compute the delta.

Step 2: The Checkout redirect handler

Checkout is two routes: one creates the session and redirects, the other handles the success-callback webhook. The creation step is server-side only.

typescript
// server/checkout/create-session.ts
import { createServerFn } from "@tanstack/start/server-fn";
import Stripe from "stripe";

export const createCheckoutSession = createServerFn({ method: "POST" }).handler(async ({ context }) => {
  const stripe = new Stripe(context.STRIPE_SECRET_KEY!, { apiVersion: "2025-09-30.clover" });
  const session = await stripe.checkout.sessions.create({
    mode: "subscription",
    line_items: [{ price: "price_monthly_99", quantity: 1 }],
    success_url: context.origin + "/billing/success?session_id={CHECKOUT_SESSION_ID}",
    cancel_url: context.origin + "/billing/cancel",
    automatic_tax: { enabled: true },
    metadata: { flow: "upgrade-monthly", ab: "checkout" },
  });
  return { url: session.url };
});

The automatic_tax toggle is the one I tripped on in Flow 2 — see the tradeoffs above.

Step 3: The conversion-split recorder

The split recorder is a Drizzle table that joins PaymentIntent and Checkout session outcomes by the metadata.ab tag.

typescript
// schema/checkout-ab.ts (Drizzle + Cloudflare D1)
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const checkoutAbEvents = sqliteTable("checkout_ab_events", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  flow: text("flow").notNull(),
  abArm: text("ab_arm", { enum: ["checkout", "payment-element"] }).notNull(),
  outcome: text("outcome", { enum: ["started", "succeeded", "abandoned"] }).notNull(),
  stripeRef: text("stripe_ref"),
  amountCents: integer("amount_cents"),
  occurredAt: integer("occurred_at").notNull(),
});

A nightly query computes the conversion delta per flow. Same pattern I ship in the PLG metrics dashboard and the MRR dashboard.

Where each embed genuinely wins

A summary, naming both cases without hiding them.

Pick Checkout when: the buyer is starting a free trial with no card, the flow is a marketplace split-pay with Stripe Connect, the buyer base is non-English European, or the buyer is a no-code user paying $9/mo and the Stripe-hosted page is a trust signal.

Pick Payment Element when: the flow is an in-app card update for an existing customer, the SaaS is B2B and the buyer is a founder, the SaaS is B2C and the buyer expects wallets without a redirect, or the SaaS uses Smart Retries and needs the buyer to stay inside the product during a retry.

Ship both when: the SaaS has a first-time-signup flow (Checkout) and an in-app upgrade flow (Payment Element). Most non-trivial SaaS products fall in this bucket. TanStack Ship ships both routes pre-wired — see the pricing page.

FAQ

Does Payment Element work with subscriptions?

Yes. Payment Element supports mode: "subscription" PaymentIntents the same way Checkout does. The auto-tax line item, the trial-period config, and Smart Retries all work inside the embed. The tradeoff is that the buyer never sees a Stripe-hosted confirmation page — your success-callback route is what they land on.

Can I switch from Checkout to Payment Element without breaking existing customers?

Yes, but plan the migration. Existing customers stay on Checkout until they upgrade or change card; new customers see Payment Element. The webhook handler does not care which surface the customer came from.

Which embed is faster to build?

Checkout is faster the first time (one server route, one redirect). Payment Element is faster to maintain across multiple flows. For a single-flow SaaS, Checkout wins on day-one speed. For two or more checkout surfaces, Payment Element wins on long-term maintenance.

What about Apple Pay and Google Pay?

Both embeds support both wallets. Checkout shows them as buttons on the hosted page. Payment Element shows them inside the embed alongside the card form — no redirect kills the wallet choice.

Where does TanStack Ship fit in this?

Both embeds ship in the TanStack Ship starter as pre-wired routes: /checkout/upgrade for Payment Element and /checkout/new for Checkout redirect. The webhook handler, Drizzle schema, and A/B split recorder are all in the same monorepo.


Want both embeds pre-wired into your TanStack Start project? TanStack Ship ships the Payment Element route, the Checkout redirect, the shared webhook handler, and the A/B split recorder — git clone to a working dual-embed setup in under an hour. See the feature list and the changelog for what shipped this quarter.