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.
Base URL: https://api.monime.io · Auth: Bearer token · Required headers: Monime-Space-Id, Idempotency-Key
Prerequisites
Access Token (server-side) and Space ID
Next.js 13+ with the App Router
Environment variables
Create .env.local:
MONIME_ACCESS_TOKEN = mon_test_xxxxxxxxxxxxxxxxxxxxxxxxx
MONIME_SPACE_ID = spc-xxxxxxxxxxxxxxxx
Never expose tokens in the browser.
Implement the API route
Create app/api/checkout/route.ts:
import { randomUUID } 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 res = await fetch ( 'https://api.monime.io/v1/checkout-sessions' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ token } ` ,
'Monime-Space-Id' : spaceId ,
'Idempotency-Key' : randomUUID (),
'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 })
}
Notes:
Amount is derived from lineItems (price.value × quantity, where value is in SLE minor units).
callbackState is optional and is echoed back via callbacks for correlation.
Always send a new Idempotency-Key (e.g., UUID) per logical attempt.
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
Point the successUrl 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 successUrl query (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.
Webhook Introduction How webhooks work on Monime
HMAC Verification Verify webhook signatures securely
Troubleshooting
400 Validation: ensure lineItems is non-empty and URLs are absolute
401/403 Auth: use a valid server-side token and correct Monime-Space-Id
409 Conflict: reuse Idempotency-Key only to retry the same logical request
Next steps
API Reference Request schema and responses
Full Checkout Session Guide Deep dive, options, and multi-stack examples