NewDash Dots are hereMini apps in Dash, with payments built inLearn more

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

Environment variables

Create .env.local:

Terminal
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. Scroll through the steps to see what each part of the route does.

  1. Describe your line items

    Each item is a custom line with a name, a quantity and a price in SLE minor units. Monime derives the session total from these items: price.value × quantity.

  2. Read credentials and the order

    Read your access token and Space ID from server-side environment variables, then parse the order your UI sent. callbackState is optional and is echoed back via callbacks for correlation.

  3. Validate the request

    Reject requests without line items before calling Monime.

  4. Build redirect URLs and an idempotency key

    Build successUrl and cancelUrl on the server so they can't be tampered with. Derive one stable Idempotency-Key per logical checkout: retrying the same orderId reuses the key, and a different checkout needs a different orderId.

  5. Create the checkout session

    Call POST /v1/checkout-sessions with your token, Space ID and idempotency key.

  6. Handle errors

    Surface a failed request to your UI instead of redirecting the customer.

  7. Return the hosted checkout URL

    Store the mapping from your order to the session ID for later verification, then return the redirectUrl so your UI can send the customer to Monime's hosted checkout.

app/api/checkout/route.ts
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:

TypeScript
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:

  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:

TypeScript
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.

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