Developer Resources · Checkout Session
Create Checkout Session (Next.js App Router)
Step-by-step guide to implement a Checkout Session endpoint using the Next.js App Router API routes
Overview
This guide shows how to build a Next.js App Router API route that creates a Checkout Session and returns a redirectUrl to send your customer to Monime's hosted checkout.
Prerequisites
- Access Token (server-side) and Space ID
-
Create tokens: Personal Access Tokens
-
Find your Space ID: Create a Monime Space
(see Developer section → Space list)
-
- Next.js 13+ with the App Router
Environment variables
Create .env.local:
MONIME_ACCESS_TOKEN=mon_test_xxxxxxxxxxxxxxxxxxxxxxxxx
MONIME_SPACE_ID=spc-xxxxxxxxxxxxxxxxNever expose tokens in the browser.
Implement the API route
Create app/api/checkout/route.ts. Scroll through the steps to see what each part of the route does.
Describe your line items
Each item is a
customline with a name, a quantity and a price in SLE minor units. Monime derives the session total from these items:price.value × quantity.Read credentials and the order
Read your access token and Space ID from server-side environment variables, then parse the order your UI sent.
callbackStateis optional and is echoed back via callbacks for correlation.Validate the request
Reject requests without line items before calling Monime.
Build redirect URLs and an idempotency key
Build
successUrlandcancelUrlon the server so they can't be tampered with. Derive one stableIdempotency-Keyper logical checkout: retrying the sameorderIdreuses the key, and a different checkout needs a differentorderId.Create the checkout session
Call
POST /v1/checkout-sessionswith your token, Space ID and idempotency key.Handle errors
Surface a failed request to your UI instead of redirecting the customer.
Return the hosted checkout URL
Store the mapping from your order to the session ID for later verification, then return the
redirectUrlso your UI can send the customer to Monime's hosted checkout.
import { createHash } from 'crypto'
type LineItem = {
type: 'custom'
name: string
price: { currency: 'SLE'; value: number }
quantity: number
description?: string
reference?: string
}
export async function POST(req: Request) {
const token = process.env.MONIME_ACCESS_TOKEN!
const spaceId = process.env.MONIME_SPACE_ID!
const { name, orderId, lineItems, metadata, callbackState } = (await req.json()) as {
name: string
orderId: string
lineItems: LineItem[]
metadata?: Record<string, string>
callbackState?: string
}
if (!Array.isArray(lineItems) || lineItems.length === 0) {
return new Response(JSON.stringify({ error: 'lineItems is required' }), { status: 400 })
}
// Build redirect URLs on the server to prevent tampering
const appUrl = process.env.APP_URL || 'https://example.com'
const successUrl = `${appUrl}/api/checkout/success?orderId=${encodeURIComponent(orderId)}`
const cancelUrl = `${appUrl}/checkout/cancelled?orderId=${encodeURIComponent(orderId)}`
const idempotencyKey = createHash('sha256')
.update(`checkout:${orderId}`)
.digest('hex')
const res = await fetch('https://api.monime.io/v1/checkout-sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Monime-Space-Id': spaceId,
'Idempotency-Key': idempotencyKey,
'Content-Type': 'application/json',
},
body: JSON.stringify({ name, successUrl, cancelUrl, lineItems, metadata, callbackState }),
})
if (!res.ok) {
const error = await res.text()
return new Response(JSON.stringify({ error }), { status: 500 })
}
const data = await res.json()
// TODO: Persist mapping of orderId -> checkoutSessionId for later verification
// await db.orders.update(orderId, { checkoutSessionId: data?.result?.id })
return Response.json({ redirectUrl: data?.result?.redirectUrl, sessionId: data?.result?.id })
}Call the endpoint from your UI
Example client-side handler using fetch:
function toMinorUnits(amountSLE: number) {
return Math.round(amountSLE * 100)
}
async function startCheckout() {
const payload = {
name: 'Order #1001',
orderId: 'ord_1001',
lineItems: [
{ type: 'custom', name: 'Starter bundle', price: { currency: 'SLE', value: toMinorUnits(25) }, quantity: 1 },
],
metadata: { cartId: 'cart_123' },
callbackState: 'state_cart_123',
}
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
})
if (!res.ok) {
console.error('Checkout failed', await res.text())
return
}
const { redirectUrl } = await res.json()
if (redirectUrl) window.location.href = redirectUrl
}Handle successUrl with a server redirect
Monime sends a POST request to the successUrl. Point it to a Next.js API route that will:
- Verify the session and update your server-side state
- Redirect the user to your success UI page
Create app/api/checkout/success/route.ts:
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
const token = process.env.MONIME_ACCESS_TOKEN!
const spaceId = process.env.MONIME_SPACE_ID!
const url = new URL(req.url)
const orderId = url.searchParams.get('orderId')
if (!orderId) return new Response('Missing orderId', { status: 400 })
// 1) Load order and get stored checkoutSessionId
// const order = await db.orders.findOne(orderId)
// if (!order?.checkoutSessionId) return new Response('Order not found', { status: 404 })
// 2) Optionally verify status with the API
// const res = await fetch(`https://api.monime.io/v1/checkout-sessions/${order.checkoutSessionId}`, {
// headers: {
// Authorization: `Bearer ${token}`,
// 'Monime-Space-Id': spaceId,
// },
// })
// const data = await res.json()
// const status = data?.result?.status // e.g., 'completed'
// await db.orders.update(orderId, { status })
// 3) Redirect to your success UI
const uiUrl = new URL(`/checkout/success?orderId=${encodeURIComponent(orderId)}`, process.env.APP_URL || 'https://example.com')
return NextResponse.redirect(uiUrl)
}Notes:
- You can embed extra context in the
successUrlquery (e.g.,orderId). - Persist the session ID when creating it, so you can look it up here and confirm status.
- Prefer webhooks for authoritative status; the success redirect is for UX continuity.
Webhooks (recommended)
Use webhooks to reliably update your system about payment outcomes, even if the customer closes the browser before redirection.
Troubleshooting
- 400 Validation: ensure
lineItemsis non-empty and URLs are absolute - 401/403 Auth: use a valid server-side token and correct
Monime-Space-Id - 409 Conflict: reuse
Idempotency-Keyonly to retry the same logical request