DEV Community

Cover image for Accepting Payments with Stripe Hosted and Embedded Checkout in Next.js
Anjan Shomodder
Anjan Shomodder

Posted on

Accepting Payments with Stripe Hosted and Embedded Checkout in Next.js

Stripe Checkout is one of the easiest ways to start accepting payments in your web app. In this post, we’ll walk through integrating both Stripe’s hosted checkout page and the embedded checkout experience in a Next.js project—no database required, just a simple product list and a checkout button.

You can also check the video tutorial here:

How Stripe Checkout Works

The flow is straightforward:

  1. Your server (Next.js API or server action) sends product details (name, price, currency, etc.) to Stripe.
  2. Stripe creates a Checkout Session and returns a session object containing a unique URL (for hosted) or a client secret (for embedded).
  3. Redirect the user to this URL (hosted) or render the payment form in your app (embedded).
  4. User completes payment on Stripe’s secure page or embedded form.
  5. Stripe redirects the user back to your site (success or cancel URL).

Project Structure

If you want to learn how to setup stripe with Next.js, you can check the video tutorial here:

Here’s a quick look at the relevant files:

  • src/app/products.js: Exports an array of product objects (name, price, quantity, image, description).
  • src/components/ProductList.jsx: Renders the product list.
  • src/components/CheckoutButton.jsx: Triggers the checkout session.
  • src/actions/createCheckoutSession.js: Server action to create a Stripe Checkout Session.
  • src/app/embeded/page.js: Implements the embedded checkout UI.

Creating a Checkout Session

The core logic lives in createCheckoutSession.js:

export async function createCheckoutSession({ ui_mode = 'hosted' }) {
  const sessionParams = {
    payment_method_types: ['card'],
    mode: 'payment',
    currency: 'usd',
    ui_mode,
    line_items: products.map(product => ({
      price_data: {
        currency: 'usd',
        unit_amount: product.price,
        product_data: {
          name: product.name,
          description: "product.description,"
          images: [product.image],
        },
      },
      quantity: product.quantity,
    })),
    customer_email: 'thatanjan@gmail.com',
    allow_promotion_codes: true,
  }

  if (ui_mode === 'hosted') {
    sessionParams.success_url = 'http://localhost:3000/success'
    sessionParams.cancel_url = 'http://localhost:3000/cancel'
  } else {
    sessionParams.return_url = 'http://localhost:3000/success'
  }

  const session = await stripe.checkout.sessions.create(sessionParams)

  if (ui_mode !== 'hosted') {
    return session.client_secret
  }

  if (session.url) {
    redirect(session.url)
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • We define a function that creates a Stripe Checkout Session. The ui_mode parameter controls whether we use the hosted Stripe page or an embedded version.
  • sessionParams is an object that configures the checkout session:
    • payment_method_types: Only allows card payments.
    • mode: 'payment': Indicates a one-time payment (not a subscription).
    • currency: Sets the currency (USD in this example).
    • line_items: Maps each product to Stripe’s required format, including price, name, description, and image.
    • customer_email: Optionally pre-fills the email field at checkout.
    • allow_promotion_codes: Lets users enter promo codes at checkout.
  • Depending on the ui_mode, we set either success_url and cancel_url (for hosted) or return_url (for embedded).
  • We call stripe.checkout.sessions.create to create the session.
  • If using embedded mode, we return the client_secret for use with Stripe’s embedded checkout component.
  • If using hosted mode, we redirect the user to the Stripe-hosted checkout page using the session URL.

Triggering Checkout from the UI

The CheckoutButton component calls the server action. When clicked, it creates a session and redirects the user to Stripe’s hosted page.

'use client'

const CheckoutButton = props => {
  const [isLoading, setIsLoading] = useState(false)

  const onClick = async () => {
    setIsLoading(true)

    await createCheckoutSession({})
    setIsLoading(false)
  }

  return (
    <Button onClick={onClick}>{isLoading ? 'Loading...' : 'Checkout'}</Button>
  )
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The button’s click handler calls createCheckoutSession.
  • For hosted mode, the user is redirected to Stripe’s secure payment page.

Handling Currencies

Stripe expects prices in the smallest currency unit (e.g., cents for USD). For non-decimal currencies like Japanese Yen, 1 JPY = 1 unit.

Explanation:

  • Always multiply your price by 100 for currencies like USD or EUR (e.g., $10.00 → 1000).
  • For currencies without subunits, just use the integer value.

Customer Creation

By default, Stripe creates a “guest” customer for each payment. You can control this by passing customer_email, or by using the customer_creation parameter for more advanced scenarios.

Explanation:

  • Passing customer_email pre-fills the email field and links the payment to that email.
  • Setting customer_creation: 'always' forces Stripe to create a new customer record for every checkout.

Using Stripe Embedded Checkout

Besides the hosted checkout page, Stripe also offers an embedded checkout experience. This allows you to display the payment form directly within your app, keeping users on your site during the payment process.

Here’s how you can implement it in Next.js:

'use client'

import {
  EmbeddedCheckout,
  EmbeddedCheckoutProvider,
} from '@stripe/react-stripe-js'

import { createCheckoutSession } from '@/actions/createCheckoutSession'
import { stripePromise } from '@/lib/stripeClient'

export default function Checkout() {
  return (
    <div>
      <h1 className='text-2xl font-bold mb-4'>Embedded Checkout</h1>

      <EmbeddedCheckoutProvider
        stripe={stripePromise}
        options={{
          fetchClientSecret: () =>
            createCheckoutSession({ ui_mode: 'embedded' }),
        }}
      >
        <EmbeddedCheckout />
      </EmbeddedCheckoutProvider>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • The component uses Stripe’s EmbeddedCheckoutProvider and EmbeddedCheckout from @stripe/react-stripe-js.
  • stripePromise is your Stripe publishable key, loaded with Stripe’s JS SDK.
  • The options prop includes a fetchClientSecret function, which calls your server action (createCheckoutSession) with ui_mode: 'embedded'. This returns a client_secret needed to initialize the embedded checkout.
  • The EmbeddedCheckout component renders the Stripe payment form directly inside your app, providing a seamless user experience.

When to use Embedded Checkout?

  • If you want to keep users on your site instead of redirecting them to Stripe.
  • For a more integrated look and feel, while still letting Stripe handle all sensitive payment details.

Conclusion

Stripe’s hosted and embedded checkout options make it fast and secure to accept payments in your Next.js app. You don’t need to handle sensitive card data or build a custom payment form—Stripe takes care of it all. For more advanced use cases, explore custom flows using Stripe Elements.


Want to see the full code?

Check out the GitHub repo and try it yourself!

If you found this helpful, let me know in the comments or reach out with questions. Happy coding!

Top comments (0)