> ## Documentation Index
> Fetch the complete documentation index at: https://browseruse-0aece648-codex-clarify-browser-infrastructure.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat UI

> Build a chat UI with live browser preview, follow-up tasks, recording, and task messages.

<Card title="Related UI example" icon="github" href="https://github.com/browser-use/chat-ui-example">
  Use the Next.js UI as a reference. Its current URL-parameter transport differs
  from the safer session-state pattern below.
</Card>

This tutorial adapts the [chat-ui-example](https://github.com/browser-use/chat-ui-example) architecture for a Next.js app that lets users chat with a Browser Use agent. Follow the integration on this page rather than copying the example repository's query-parameter transport.

The app has two pages:

1. **Home** — the user types a task, the app creates a session and sends the task.
2. **Session** — live browser preview, task messages, follow-ups, and recording download.

In this pattern, the SDK client lives in one server-only file,
`src/lib/api.ts`. Short calls use Server Actions. The long-running task and its
stop control use separate Route Handlers so the stop request can run while the
task request is still pending.

## Setup

```typescript api.ts theme={null}
import { BrowserUse } from "browser-use-sdk/v3";

// Server-only — no NEXT_PUBLIC_ prefix, never exposed to the browser
const apiKey = process.env.BROWSER_USE_API_KEY ?? "";
const sessionMaxCostUsd = Number(process.env.BROWSER_USE_SESSION_MAX_COST_USD);
if (!Number.isFinite(sessionMaxCostUsd) || sessionMaxCostUsd <= 0) {
  throw new Error("Set BROWSER_USE_SESSION_MAX_COST_USD to a positive number");
}

export const client = new BrowserUse({ apiKey });
export { sessionMaxCostUsd };
```

<Note>
  The API key uses `BROWSER_USE_API_KEY` (no `NEXT_PUBLIC_` prefix) so it stays
  server-side. All SDK calls go through server-only
  [Server Actions](https://nextjs.org/docs/app/guides/forms) or Route Handlers —
  never call the SDK directly from client components.
</Note>

<Warning>
  This is a trusted-user prototype. Before exposing it publicly, authenticate
  every server entry point, verify that each session ID belongs to the caller, and
  enforce rate limits and spend quotas. A server-only API key prevents key
  disclosure; it does not prevent an unauthenticated visitor from spending
  against that key.
</Warning>

***

## 1. Create a session

```typescript actions.ts theme={null}
"use server";
import { client, sessionMaxCostUsd } from "./api";

export async function createSession() {
  const session = await client.sessions.create({
    keepAlive: true,
    enableRecording: true,
    maxCostUsd: sessionMaxCostUsd,
  });
  if (!session.liveUrl) {
    throw new Error("The session did not return a live browser URL");
  }
  return { ...session, liveUrl: session.liveUrl };
}
```

* **`keepAlive: true`** keeps the session open after each task so the user can send follow-ups (default is `false`).
* **`enableRecording: true`** produces an MP4 video of the browser session.
* **`maxCostUsd`** bounds total spend across the keep-alive session, including
  follow-up turns. Set the environment value to the session cap your
  application accepts.
* **`liveUrl`** is checked before the server action returns, so the client receives a string rather than the SDK's nullable response field.

The home page creates the session and navigates with only the opaque session ID.
Keep the signed `liveUrl` and task out of query parameters, which can be copied
into browser history, analytics, and request logs. This prototype uses
`sessionStorage`; a production app should keep this state server-side under the
authenticated user's ownership:

```typescript page.tsx theme={null}
async function handleSend(message: string) {
  const session = await createSession();
  sessionStorage.setItem(
    `browser-use:session:${session.id}`,
    JSON.stringify({ session, initialTask: message }),
  );
  router.push(`/session/${session.id}`);
}
```

***

<a id="2-stream-messages-with-for-await" />

## 2. Run a task through a Route Handler

Consume `client.run()` inside a Route Handler so the API key and SDK client
never enter the browser bundle. A long-running Server Action would block a
second Server Action from the same client, preventing the Stop button from
interrupting the task. The handler collects task messages and returns them with
the final session state:

```typescript app/api/browser-use/run/route.ts theme={null}
import { client, sessionMaxCostUsd } from "@/lib/api";

// Keep the SDK wait below the deployment function limit so cleanup can run.
export const maxDuration = 300;
const taskWaitTimeoutMs = 240_000;

export async function POST(request: Request) {
  const { sessionId, task } = await request.json();
  if (typeof sessionId !== "string" || typeof task !== "string") {
    return Response.json({ error: "Invalid request" }, { status: 400 });
  }

  // Authenticate the caller and verify ownership of sessionId here.
  const run = client.run(task, {
    sessionId,
    // Re-send the same absolute cumulative cap. Omitting it on an existing
    // session refreshes the API budget from the current spend.
    maxCostUsd: sessionMaxCostUsd,
    timeout: taskWaitTimeoutMs,
  });
  const messages = [];

  try {
    for await (const msg of run) {
      messages.push(msg);
    }
  } catch (error) {
    // The SDK timeout only stops polling. Explicitly stop the task before the
    // Route Handler exits so it cannot continue unattended.
    await client.sessions.stop(sessionId, { strategy: "task" }).catch(() => {});
    throw error;
  }

  if (!run.result) throw new Error("Task ended without a session result");
  return Response.json({ messages, session: run.result });
}
```

Call the handler from the client component and update local UI state after it
returns:

```typescript session-context.tsx theme={null}
const submitTask = useCallback(async (task: string) => {
  const response = await fetch("/api/browser-use/run", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ sessionId, task }),
  });
  if (!response.ok) throw new Error("Task request failed");

  const result = await response.json();
  setMessages((prev) => [...prev, ...result.messages]);
  setSession(result.session);
}, [sessionId]);
```

When the `for await` loop ends on the server, `run.result` contains the final session state. This pattern returns the collected messages after completion. For token-by-token or message-by-message browser updates, expose a server Route Handler that encodes the iterator as a `ReadableStream`; do not import the SDK client into a client component.

`client.run()` otherwise waits for up to four hours. This example gives the
Route Handler five minutes and the SDK four, leaving time to stop the task on a
timeout. Set both values to limits supported by your deployment platform. For
tasks longer than one request, persist messages and task state server-side and
resume polling from a queue or worker rather than depending on one HTTP request.

Read and delete the prototype's initial task from `sessionStorage`, then run it:

```typescript session-context.tsx theme={null}
useEffect(() => {
  const key = `browser-use:session:${sessionId}`;
  const saved = sessionStorage.getItem(key);
  if (!saved) return;
  sessionStorage.removeItem(key);
  const { session, initialTask } = JSON.parse(saved);
  setLiveUrl(session.liveUrl);
  setSession(session);
  if (initialTask) submitTask(initialTask);
}, [sessionId, submitTask]);
```

***

## 3. Follow-up tasks

Follow-ups call the same Route Handler with the existing session ID:

```typescript session-context.tsx theme={null}
const sendMessage = useCallback(async (task: string) => {
  await submitTask(task);
}, [submitTask]);
```

The SDK auto-sets `keepAlive: true` when targeting an existing session, so follow-up tasks work without extra config.

***

<a id="4-recording" />

## 4. Finish the session and get the recording

After a task completes, its recording URLs become available on the session.
When the conversation is finished, wait for the recording and then stop the
keep-alive session so its sandbox is released:

```typescript app/api/browser-use/finish/route.ts theme={null}
import { client } from "@/lib/api";

export async function POST(request: Request) {
  const { sessionId } = await request.json();
  if (typeof sessionId !== "string") {
    return Response.json({ error: "Invalid request" }, { status: 400 });
  }

  // Authenticate the caller and verify ownership of sessionId here.
  let recordingUrls: Awaited<ReturnType<typeof client.sessions.waitForRecording>>;
  let session: Awaited<ReturnType<typeof client.sessions.stop>>;
  try {
    recordingUrls = await client.sessions.waitForRecording(sessionId);
  } finally {
    // Release the keep-alive sandbox even if recording polling fails.
    session = await client.sessions.stop(sessionId, { strategy: "session" });
  }
  return Response.json({ session, recordingUrls });
}
```

```typescript session-context.tsx theme={null}
const finishSession = useCallback(async () => {
  const response = await fetch("/api/browser-use/finish", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ sessionId }),
  });
  if (!response.ok) throw new Error("Finish request failed");

  const result = await response.json();
  setSession(result.session);
  setRecordingUrls(result.recordingUrls);
}, [sessionId]);
```

`waitForRecording` polls for up to 15 seconds and returns presigned MP4
download URLs. It returns an empty array if the agent never opened a browser.
Stopping the session after the poll destroys its sandbox, so call this route
only when the user is finished with follow-ups.

***

## 5. Stop a task

```typescript app/api/browser-use/stop/route.ts theme={null}
import { client } from "@/lib/api";

export async function POST(request: Request) {
  const { sessionId } = await request.json();
  if (typeof sessionId !== "string") {
    return Response.json({ error: "Invalid request" }, { status: 400 });
  }

  // Authenticate the caller and verify ownership of sessionId here.
  await client.sessions.stop(sessionId, { strategy: "task" });
  return new Response(null, { status: 204 });
}
```

```typescript session-context.tsx theme={null}
const stopTask = useCallback(async () => {
  const response = await fetch("/api/browser-use/stop", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ sessionId }),
  });
  if (!response.ok) throw new Error("Stop request failed");
}, [sessionId]);
```

Because the task and stop controls are independent Route Handler requests, the
stop request can reach the server while the task request is still pending.
Using `strategy: "task"` stops only the current task, keeping the session alive
for follow-ups.

***

## 6. Session page

The session page consumes everything through a context provider:

```typescript session/[id]/page.tsx theme={null}
function SessionPage() {
  const {
    session,
    liveUrl,
    turns,
    isBusy,
    recordingUrls,
    sendMessage,
    stopTask,
    finishSession,
  } = useSession();
  const sessionEnded =
    session != null && ["stopped", "timed_out", "error"].includes(session.status);

  return (
    <div className="flex h-screen w-full overflow-hidden">
      {/* Chat column */}
      <div className="flex-1 flex flex-col min-w-0">
        <ChatMessages turns={turns} isBusy={isBusy} />
        <ChatInput
          onSend={sendMessage}
          onStop={stopTask}
          disabled={sessionEnded}
        />
        <button type="button" onClick={finishSession} disabled={!session || isBusy || sessionEnded}>
          End session and get recording
        </button>
      </div>

      {/* Live browser view — liveUrl came from session creation */}
      <BrowserPanel liveUrl={liveUrl} />
    </div>
  );
}
```

***

## Summary

| Server-side SDK method                               | Entry point and purpose                                                             |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `client.sessions.create()`                           | Server Action: create a session and validate its `liveUrl`                          |
| `client.run()`                                       | Route Handler: send a task and collect messages with `for await`                    |
| `client.sessions.stop(..., { strategy: "task" })`    | Separate Route Handler: stop the current task concurrently                          |
| `client.sessions.stop(..., { strategy: "session" })` | Finish Route Handler: end the browser session after follow-ups                      |
| `client.sessions.waitForRecording()`                 | Finish Route Handler: get MP4 URLs after task completion, before ending the session |
