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:
- Your server (Next.js API or server action) sends product details (name, price, currency, etc.) to Stripe.
- Stripe creates a Checkout Session and returns a session object containing a unique URL (for hosted) or a client secret (for embedded).
- Redirect the user to this URL (hosted) or render the payment form in your app (embedded).
- User completes payment on Stripe’s secure page or embedded form.
- 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)
}
}
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 eithersuccess_url
andcancel_url
(for hosted) orreturn_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>
)
}
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>
)
}
Explanation:
- The component uses Stripe’s
EmbeddedCheckoutProvider
andEmbeddedCheckout
from@stripe/react-stripe-js
. -
stripePromise
is your Stripe publishable key, loaded with Stripe’s JS SDK. - The
options
prop includes afetchClientSecret
function, which calls your server action (createCheckoutSession
) withui_mode: 'embedded'
. This returns aclient_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)