# Create Checkout Session (Next.js App Router) (/docs/developer-resources/guides/checkout-session/create-checkout-session-nextjs)

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

> **Info:** Base URL: `https://api.monime.io` · Auth: Bearer token · Required headers: `Monime-Space-Id`, `Idempotency-Key`

## Prerequisites

- Access Token (server-side) and Space ID
  - Create tokens: [
    Personal Access Tokens](/docs/dashboard-guide/personal-access-tokens)
  - Find your Space ID: [
    Create a Monime Space](/docs/dashboard-guide/creating-a-space)

    (see Developer section → Space list)
- Next.js 13+ with the App Router

### Environment variables

Create `.env.local`:

```bash
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.

```ts title="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 })
}
```

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.

## Call the endpoint from your UI

Example client-side handler using fetch:

```ts
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`:

```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**](/docs/developer-resources/guides/webhook/introduction)

  How webhooks work on Monime

- [**HMAC Verification**](/docs/developer-resources/guides/webhook/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**](/apis/versions/caph-2025-06-20/checkout-session/create-checkout-session)

  Request schema and responses

- [**Full Checkout Session Guide**](/docs/developer-resources/guides/checkout-session/create-checkout-session)

  Deep dive, options, and multi-stack examples
