Stripe Checkout
Integrate Stripe's full payment form into your checkout flow using Stripe Elements and the Hantera Storefront SDK.
Live Example
See this integration in action in the Cart Playground. The source code is available in the hantera-storefront-sdk repository.
Prerequisites
- The
stripePSP app installed and configured on your Hantera instance - Stripe API keys configured in the app's settings
Overview
The Stripe checkout flow works in four stages:
- Load Stripe.js and fetch the public key from your Hantera instance
- Mount Stripe Elements — address and payment form
- Submit payment by calling the Stripe PSP ingress to get a
clientSecret - Confirm payment with Stripe.js, which may redirect the customer for 3D Secure
Step 1: Load Stripe.js
Load Stripe.js dynamically and fetch your publishable key from the Hantera Stripe app's public endpoint:
import Stripe from 'stripe'
// Fetch the publishable key from your Hantera instance
const response = await fetch(`${baseUrl}/ingress/stripe/publicKey`)
const { publicKey } = await response.json()
// Initialize Stripe.js
const stripe = Stripe(publicKey)Endpoint
The Stripe public key endpoint is at /ingress/stripe/publicKey — this is registered by the Stripe PSP app independently of the Commerce app.
Step 2: Create Elements and Mount the Form
Create a Stripe Elements instance and mount the payment and address elements:
const elements = stripe.elements({
mode: 'payment',
amount: paymentTotal, // in minor units, e.g. cart total × 100
currency: cart.currencyCode.toLowerCase(),
})
// Mount the payment element
const paymentElement = elements.create('payment')
paymentElement.mount('#payment-element')
// Optionally mount address elements for shipping and billing
const addressElement = elements.create('address', {
mode: 'shipping',
})
addressElement.mount('#shipping-address')The amount and currency are used for display purposes only — the actual charge amount is determined server-side when the payment intent is created.
Step 3: Sync addresses to the cart
When Stripe's address element completes, push the values to the cart so the server has the right shipping/billing context before payment is created. The Address shape uses name (full name) and countryCode:
import { createCartClient } from '@hantera/storefront-sdk/cart'
const client = createCartClient({ baseUrl })
addressElement.on('change', (event) => {
if (!event.complete) return
const a = event.value.address
client.setAddress(cartId, {
address: {
name: event.value.name,
addressLine1: a.line1,
addressLine2: a.line2,
city: a.city,
state: a.state,
postalCode: a.postal_code,
countryCode: a.country,
},
})
})Step 4: Submit Payment
First validate the Elements form, then POST to the Stripe PSP ingress to create a Stripe PaymentIntent:
// Validate the form
const { error: submitError } = await elements.submit()
if (submitError) {
// Show validation error to customer
return
}
// Create payment intent via the Stripe PSP ingress
const res = await fetch(`${baseUrl}/ingress/commerce/carts/${cartId}/payment/stripe`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
const { clientSecret } = await res.json()The Stripe PSP app's ingress at /ingress/commerce/carts/{cartId}/payment/stripe creates a Stripe PaymentIntent for the current cart total and returns { clientSecret }.
Step 5: Confirm Payment
Use the clientSecret to confirm the payment with Stripe.js. This may trigger 3D Secure authentication or redirect the customer:
const { error } = await stripe.confirmPayment({
elements,
clientSecret,
confirmParams: {
return_url: `${window.location.origin}/order-confirmation?cartId=${cartId}`,
},
})
if (error) {
// Payment failed — show error to customer
// error.type will be 'card_error' or 'validation_error' for recoverable errors
}
// If no error, the customer was redirected to return_urlStep 6: Handle the Return
After Stripe redirects the customer back to your return_url, the cart may not be completed yet — the server processes the payment confirmation asynchronously via webhooks.
Show a loading state and listen for the completion event via SSE:
const eventSource = client.subscribeToCartEvents(cartId, {
onUpdate: (cartData) => {
if (cartData.cartState === 'completed') {
eventSource.close()
// Show order confirmation with cartData
}
},
onError: (data) => {
eventSource.close()
// Handle payment failure
},
})Async Completion
Never assume the cart is completed immediately after redirect. Always use SSE to detect the completed state. The server-to-server payment confirmation may take a few seconds.
Complete Example
import Stripe from 'stripe'
import { createCartClient } from '@hantera/storefront-sdk/cart'
const baseUrl = 'https://core.your-instance.hantera.cloud'
const client = createCartClient({ baseUrl })
// 1. Load Stripe
const keyResponse = await fetch(`${baseUrl}/ingress/stripe/publicKey`)
const { publicKey } = await keyResponse.json()
const stripe = Stripe(publicKey)
// 2. Create Elements
const elements = stripe.elements({
mode: 'payment',
amount: paymentTotal,
currency: 'sek',
})
elements.create('payment').mount('#payment-element')
elements.create('address', { mode: 'shipping' }).mount('#shipping-address')
// 3. On form submit
async function handleCheckout() {
const { error: formError } = await elements.submit()
if (formError) return
// Make sure customer info is on the cart
await client.setEmail(cartId, email)
// Create payment intent via the Stripe PSP ingress
const res = await fetch(`${baseUrl}/ingress/commerce/carts/${cartId}/payment/stripe`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
const { clientSecret } = await res.json()
// 4. Confirm with Stripe
const { error } = await stripe.confirmPayment({
elements,
clientSecret,
confirmParams: {
return_url: `${window.location.origin}/confirmation?cartId=${cartId}`,
},
})
if (error) {
// Show error
}
}
// 5. On return_url page — listen for completion
function waitForCompletion(cartId: string) {
const eventSource = client.subscribeToCartEvents(cartId, {
onUpdate: (cartData) => {
if (cartData.cartState === 'completed') {
eventSource.close()
showOrderConfirmation(cartData)
}
},
})
}Error Handling
| Error Source | When | How to Handle |
|---|---|---|
elements.submit() | Form validation fails | Show inline validation errors |
| Stripe PSP ingress | Server rejects the request | Show error message, allow retry |
confirmPayment() | Card declined, 3DS failed | Show Stripe's error message |
SSE onError | Payment processing failed | Show failure message |