Skip to main content

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

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:
  1. Verify the session and update your server-side state
  2. 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.
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