---
name: checkout
description: Checkout flow with cart, shipping, Stripe/PayPal payments, and order completion. Use when modifying checkout, debugging payment issues, or implementing checkout features.
---
# Checkout Flow Guide
Complete checkout system with Stripe and PayPal integration.
## Checkout Flow Overview
```
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌─────────┐ ┌──────────┐
│ Cart │───▶│ Customer │───▶│ Shipping │───▶│ Payment │───▶│ Complete │
└─────────┘ └──────────┘ └──────────┘ └─────────┘ └──────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Create Set email Set address Stripe or Create
checkout (guest or + method PayPal order
login)
```
## API Endpoints
| Endpoint | Method | Purpose |
| ------------------------------------------ | ------ | --------------------------- |
| `/api/checkout/create` | POST | Create checkout from cart |
| `/api/checkout/$id` | GET | Get checkout state |
| `/api/checkout/$id/customer` | POST | Set customer email |
| `/api/checkout/$id/shipping-address` | POST | Set shipping address |
| `/api/checkout/$id/shipping-rates` | GET | Get shipping options |
| `/api/checkout/$id/shipping-method` | POST | Select shipping method |
| `/api/checkout/$id/payment/stripe` | POST | Create Stripe PaymentIntent |
| `/api/checkout/$id/payment/paypal` | POST | Create PayPal order |
| `/api/checkout/$id/payment/paypal.capture` | POST | Capture PayPal payment |
| `/api/checkout/$id/complete` | POST | Complete checkout |
## Step 1: Create Checkout
```typescript
// src/routes/api/checkout/create.ts
POST: async ({ request }) => {
const { items } = await request.json()
// items: [{ productId, variantId, quantity }]
// Validate and get current prices
const cartItems = await Promise.all(
items.map(async (item) => {
const [variant] = await db
.select()
.from(productVariants)
.where(eq(productVariants.id, item.variantId))
.limit(1)
const [product] = await db
.select()
.from(products)
.where(eq(products.id, item.productId))
.limit(1)
return {
productId: item.productId,
variantId: item.variantId,
quantity: item.quantity,
title: product.name.en,
variantTitle: variant.title,
sku: variant.sku,
price: parseFloat(variant.price),
imageUrl: null, // Fetch image separately
}
}),
)
const subtotal = cartItems.reduce(
(sum, item) => sum + item.price * item.quantity,
0,
)
const [checkout] = await db
.insert(checkouts)
.values({
cartItems,
subtotal: subtotal.toFixed(2),
total: subtotal.toFixed(2),
currency: 'USD',
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000), // 24 hours
})
.returning()
// Set checkout session cookie
const token = generateCheckoutToken(checkout.id)
const response = successResponse({ checkout }, 201)
setCheckoutCookie(response, token)
return response
}
```
## Step 2: Set Customer
```typescript
// src/routes/api/checkout/$checkoutId/customer.ts
POST: async ({ request, params }) => {
const { checkoutId } = params
const { email, createAccount, password } = await request.json()
// Validate checkout access
const validation = await validateCheckoutAccess(request, checkoutId)
if (!validation.valid) {
return simpleErrorResponse(validation.error, 401)
}
// Check if user exists
const session = await validateSession(request)
if (session.success) {
// Logged in user - link to their customer record
let [customer] = await db
.select()
.from(customers)
.where(eq(customers.userId, session.user.id))
.limit(1)
if (!customer) {
;[customer] = await db
.insert(customers)
.values({
userId: session.user.id,
email: session.user.email,
})
.returning()
}
await db
.update(checkouts)
.set({ customerId: customer.id, email: customer.email })
.where(eq(checkouts.id, checkoutId))
} else {
// Guest checkout
await db
.update(checkouts)
.set({ email })
.where(eq(checkouts.id, checkoutId))
}
return successResponse({ success: true })
}
```
## Step 3: Shipping Address
```typescript
// src/routes/api/checkout/$checkoutId/shipping-address.ts
POST: async ({ request, params }) => {
const { checkoutId } = params
const address = await request.json()
// Validate required fields
const required = [
'firstName',
'lastName',
'address1',
'city',
'country',
'countryCode',
'zip',
]
for (const field of required) {
if (!address[field]?.trim()) {
return simpleErrorResponse(`${field} is required`)
}
}
const [updated] = await db
.update(checkouts)
.set({
shippingAddress: address,
updatedAt: new Date(),
})
.where(eq(checkouts.id, checkoutId))
.returning()
return successResponse({ checkout: updated })
}
```
## Step 4: Shipping Method
```typescript
// GET shipping rates
GET: async ({ request, params }) => {
const rates = await db
.select()
.from(shippingRates)
.where(eq(shippingRates.isActive, true))
.orderBy(asc(shippingRates.position))
return successResponse({ rates })
}
// POST select shipping method
POST: async ({ request, params }) => {
const { checkoutId } = params
const { shippingRateId } = await request.json()
const [rate] = await db
.select()
.from(shippingRates)
.where(eq(shippingRates.id, shippingRateId))
.limit(1)
if (!rate) {
return simpleErrorResponse('Invalid shipping rate')
}
const [checkout] = await db
.select()
.from(checkouts)
.where(eq(checkouts.id, checkoutId))
.limit(1)
const subtotal = parseFloat(checkout.subtotal)
const shippingTotal = parseFloat(rate.price)
const taxTotal = calculateTax(subtotal + shippingTotal)
const total = subtotal + shippingTotal + taxTotal
const [updated] = await db
.update(checkouts)
.set({
shippingRateId: rate.id,
shippingMethod: rate.name,
shippingTotal: shippingTotal.toFixed(2),
taxTotal: taxTotal.toFixed(2),
total: total.toFixed(2),
updatedAt: new Date(),
})
.where(eq(checkouts.id, checkoutId))
.returning()
return successResponse({ checkout: updated })
}
```
## Step 5a: Stripe Payment
```typescript
// src/routes/api/checkout/$checkoutId/payment/stripe.ts
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
POST: async ({ request, params }) => {
const { checkoutId } = params
const [checkout] = await db
.select()
.from(checkouts)
.where(eq(checkouts.id, checkoutId))
.limit(1)
if (!checkout) {
return simpleErrorResponse('Checkout not found', 404)
}
// Create PaymentIntent
const paymentIntent = await stripe.paymentIntents.create({
amount: Math.round(parseFloat(checkout.total) * 100), // cents
currency: checkout.currency.toLowerCase(),
metadata: {
checkoutId: checkout.id,
},
})
return successResponse({
clientSecret: paymentIntent.client_secret,
})
}
```
### Frontend Stripe Integration
```typescript
import { loadStripe } from '@stripe/stripe-js'
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js'
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!)
function CheckoutPayment({ clientSecret }: { clientSecret: string }) {
return (