Agents SDK

Frontend Integration

Add an agent chat to your Next.js app with the React SDK — install, create a token route, drop in the chat component.

Install

pnpm add @21st-sdk/nextjs
# also installs @21st-sdk/react, ai, @ai-sdk/react

This also installs @21st-sdk/react with all UI components. Peer dependencies: ai, @ai-sdk/react.

Token route

The SDK exchanges your secret API key for short-lived JWT tokens server-side, so credentials are never exposed to the browser.

app/api/an-token/route.ts
// app/api/an-token/route.ts
import { createTokenHandler } from "@21st-sdk/nextjs/server"

export const POST = createTokenHandler({
  apiKey: process.env.API_KEY_21ST!,
})
Important: Set API_KEY_21ST in your .env.local. Get your key from the dashboard. Never expose this key in client-side code.

Chat component

Create a chat instance with createAgentChat, connect it to useChat from AI SDK, and render AgentChat.

app/page.tsx
"use client"

import { useChat } from "@ai-sdk/react"
import { createAgentChat, AgentChat } from "@21st-sdk/nextjs"
import "@21st-sdk/react/styles.css"

const chat = createAgentChat({
  agent: "my-agent",
  tokenUrl: "/api/an-token",
})

export default function Page() {
  const { messages, input, handleInputChange, handleSubmit, status, stop, error } =
    useChat({ chat })

  return (
    <AgentChat
      messages={messages}
      onSend={(msg) => handleSubmit(undefined, { body: msg })}
      status={status}
      onStop={stop}
      error={error ?? undefined}
    />
  )
}

Don't forget to import @21st-sdk/react/styles.css — it provides the base styles for the chat UI.

createAgentChat options

const chat = createAgentChat({
  // Required
  agent: "my-agent",

  // Token exchange — pick one:
  tokenUrl: "/api/an-token",         // POST endpoint returning { token, expiresAt }
  // getToken: async () => "...",     // custom token fetcher

  // Optional
  apiUrl: "https://relay.an.dev",    // default relay URL
  sandboxId: "sbx_...",              // persistent sandbox for file/session sharing
  threadId: "thr_...",               // specific thread within sandbox
  onFinish: () => {},                // called when agent finishes
  onError: (error) => {},            // called on error
})
OptionDescription
agentAgent slug from the dashboard (required)
tokenUrlPOST endpoint that returns { token, expiresAt }. Tokens are cached for 1 min.
getTokenCustom async function returning a token string. Alternative to tokenUrl.
sandboxIdPersistent sandbox ID — lets the agent access the same filesystem across sessions.
threadIdThread within the sandbox — separate conversation with shared files.

AgentChat props

The AgentChat component renders the full chat UI — messages, input bar, tool visualizations, and streaming.

PropDescription
messagesChat messages from useChat() (required)
onSendCalled when user sends a message (required)
statusChat status from useChat() (required)
onStopStop generation callback (required)
themeCSS variable overrides for light/dark themes
colorMode"light" | "dark" | "auto"
classNamesPer-element CSS class overrides (root, messageList, inputBar, etc.)
slotsReplace sub-components: InputBar, UserMessage, AssistantMessage, ToolRenderer
toolRenderersCustom renderers for specific tool names
modelSelectorShow a model picker if the agent allows model switching
attachmentsFile upload configuration (allowed types, max size)

Custom tool renderers

When your agent calls a custom tool, render the result with your own React component instead of the default JSON view.

components/weather-renderer.tsx
import type { CustomToolRendererProps } from "@21st-sdk/react"

function WeatherRenderer({ name, input, output, status }: CustomToolRendererProps) {
  if (status === "pending" || status === "streaming") {
    return <div>Fetching weather for {input.city as string}...</div>
  }

  const data = output as { temp: number; condition: string }
  return (
    <div className="rounded-lg border p-3">
      <p className="font-medium">{input.city as string}</p>
      <p>{data.temp}°F — {data.condition}</p>
    </div>
  )
}

// Pass to AgentChat
<AgentChat
  toolRenderers={{ get_weather: WeatherRenderer }}
  {...rest}
/>
PropTypeDescription
namestringTool name
inputRecord<string, unknown>Tool input arguments
outputunknown | undefinedTool output (undefined while pending)
status"pending" | "streaming" | "success" | "error"Execution status

Per-message options

Override agent runtime settings for individual messages — switch models, limit cost, or restrict tools for specific tasks.

import { useChat } from "@ai-sdk/react"

const { handleSubmit } = useChat({ chat })

// Override agent options for a specific message
handleSubmit(undefined, {
  body: {
    role: "user",
    content: "Review this code for bugs",
    options: {
      model: "claude-opus-4-6",
      maxTurns: 4,
      maxBudgetUsd: 0.5,
      disallowedTools: ["Bash", "Write"],
      systemPrompt: {
        type: "preset",
        preset: "claude_code",
        append: "You are reviewing code. Do not edit files.",
      },
    },
  },
})

Persistent sandboxes and threads

For apps where the agent needs to remember files or hold multiple conversations, create a sandbox on the server and pass its ID to the chat. Use threads for separate conversations within the same sandbox.

app/page.tsx
"use client"

import { useState, useEffect, useMemo } from "react"
import { useChat } from "@ai-sdk/react"
import { createAgentChat, AgentChat } from "@21st-sdk/nextjs"
import "@21st-sdk/react/styles.css"

export default function Page() {
  const [sandboxId, setSandboxId] = useState<string | null>(null)
  const [threadId, setThreadId] = useState<string | null>(null)

  // Create sandbox on mount
  useEffect(() => {
    const stored = localStorage.getItem("sandboxId")
    if (stored) {
      setSandboxId(stored)
      return
    }
    fetch("/api/agent/sandbox", { method: "POST" })
      .then((r) => r.json())
      .then(({ sandboxId }) => {
        localStorage.setItem("sandboxId", sandboxId)
        setSandboxId(sandboxId)
      })
  }, [])

  // Create or load thread
  useEffect(() => {
    if (!sandboxId) return
    fetch(`/api/agent/threads?sandboxId=${sandboxId}`)
      .then((r) => r.json())
      .then(({ threads }) => {
        if (threads.length > 0) {
          setThreadId(threads[0].id)
        } else {
          return fetch("/api/agent/threads", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify({ sandboxId, name: "Main" }),
          }).then((r) => r.json())
            .then(({ id }) => setThreadId(id))
        }
      })
  }, [sandboxId])

  const chat = useMemo(() => {
    if (!sandboxId || !threadId) return null
    return createAgentChat({
      agent: "my-agent",
      tokenUrl: "/api/an-token",
      sandboxId,
      threadId,
    })
  }, [sandboxId, threadId])

  const { messages, handleSubmit, status, stop, error } = useChat({
    chat: chat ?? undefined,
  })

  if (!chat) return <div>Loading...</div>

  return (
    <AgentChat
      messages={messages}
      onSend={(msg) => handleSubmit(undefined, { body: msg })}
      status={status}
      onStop={stop}
      error={error ?? undefined}
    />
  )
}

The sandbox and thread API routes use the Server SDK. See the backend integration page for the full API.

What's next

Frontend Integration - 21st Agents SDK Docs