> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stagehand.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploying Stagehand

> Deploy your AI agents and automations to the cloud

Browserbase recommends that you deploy Stagehand as a [Browserbase Function](https://docs.browserbase.com/platform/functions/quickstart). The Function runs next to its browser session on Browserbase, and it reads your API key from an encrypted project secret. You do not need a separate server, a Chrome binary, or a secrets store.

Stagehand also runs on other platforms. If Functions is not a good fit for you, see [Other deployment options](#other-deployment-options).

## Prerequisites

* A Browserbase API key from [Settings](https://www.browserbase.com/settings)
* Node.js 22.18 or later and pnpm
* The `browse` CLI:

```bash theme={null}
npm install -g browse
```

## Deploy a Stagehand Function

These steps start from a template project. The template contains a starter Function that you replace with your own Stagehand code before you publish.

<Steps>
  <Step title="Initialize a Functions project">
    ```bash theme={null}
    browse functions init my-stagehand-function
    cd my-stagehand-function
    ```

    The initializer installs Stagehand and creates a starter Stagehand Function in `index.ts`.
  </Step>

  <Step title="Upload the Stagehand extension">
    Stagehand needs its extension in the browser session, and a session cannot get an extension after it starts. Upload the extension one time for your project:

    ```bash theme={null}
    browse cloud extensions upload node_modules/@browserbasehq/stagehand/dist/assets/stagehand-extension.zip
    ```

    In `index.ts`, replace `your-extension-id` with the `id` from the output.
  </Step>

  <Step title="Replace the starter Function">
    This is the starter Function in `index.ts`. Replace the template with the Stagehand code that you want to deploy. Keep the `defineFn` call. If you change the name `my-function`, use the new name in the steps that follow.

    ```typescript theme={null}
    import { defineFn } from "@browserbasehq/sdk-functions";
    import { browserbase, Stagehand } from "@browserbasehq/stagehand";
    import { z } from "zod/v4";

    defineFn(
      "my-function",
      async (context) => {
        const browser = await browserbase.connect({
          // The local dev server has no secrets, so fall back to .env.
          apiKey:
            context.secrets.BROWSERBASE_API_KEY ?? process.env.BROWSERBASE_API_KEY!,
          sessionId: context.session.id,
        });
        // In this example, Stagehand uses the Model Gateway where Browserbase charges for the tokens
        const stagehand = await Stagehand.create({ browser });
        const page = (await browser.context.activePage())!;

        await page.goto("https://news.ycombinator.com");
        const { data } = await stagehand.extract(
          "Extract the top 3 stories with their rank, title, and link URL.",
          z.object({
            stories: z
              .array(z.object({ rank: z.number(), title: z.string(), url: z.string() }))
              .max(3),
          }),
        );

        await stagehand.close();
        return {
          message: "Successfully fetched top Hacker News stories",
          timestamp: new Date().toISOString(),
          results: data.stories,
        };
      },
      {
        // Upload the Stagehand extension once, then paste its ID here:
        //   browse cloud extensions upload node_modules/@browserbasehq/stagehand/dist/assets/stagehand-extension.zip
        sessionConfig: { extensionId: "your-extension-id" },
      },
    );
    ```

    Stagehand doesn't need a model API key, and when you omit it, Stagehand uses the [Model Gateway](/v4/configuration/models#model-gateway). Browserbase charges for the tokens.
  </Step>

  <Step title="Test locally">
    Export your key, then start the development server:

    ```bash theme={null}
    export BROWSERBASE_API_KEY=your_browserbase_api_key
    browse functions dev index.ts
    ```

    In another terminal, invoke the Function by the name that you gave to `defineFn`:

    ```bash theme={null}
    curl --request POST \
      --url http://127.0.0.1:14113/v1/functions/my-function/invoke \
      --header 'Content-Type: application/json' \
      --data '{}'
    ```
  </Step>

  <Step title="Publish">
    ```bash theme={null}
    browse functions publish index.ts
    ```

    The output contains `builtFunctions`. Copy the `id` of `my-function`. This is the Function ID.

    <Warning>
      You're looking for `builtFunctions[].id` for this step.
    </Warning>

    If the build fails, open the build in the Browserbase dashboard to see the build logs.
  </Step>

  <Step title="Create and attach the secret">
    Create a project secret for your Browserbase API key. The name of the secret is the key in `context.secrets`:

    ```bash theme={null}
    browse cloud secrets create BROWSERBASE_API_KEY --env BROWSERBASE_API_KEY
    ```

    The CLI encrypts the value on your machine before it sends it. Copy the `id` of the secret, then attach it to the Function:

    ```bash theme={null}
    browse functions secrets attach <functionId> <secretId>
    browse functions secrets list <functionId>
    ```

    The `list` output must show the secret. Attached secrets take effect on the next invocation.
  </Step>

  <Step title="Invoke the Function">
    ```bash theme={null}
    browse functions invoke <functionId>
    ```

    The command waits for the invocation and prints its result. You can also invoke the Function from the Browserbase dashboard.
  </Step>
</Steps>

## Troubleshooting

* **`function or secret not found` when you attach a secret**: an ID is incorrect. Use `browse cloud secrets list` to verify your secret IDs and the Functions dashboard to verify your Function ID.
* **`Secret "BROWSERBASE_API_KEY" is not set`**: the secret is not attached to this Function. Run `browse functions secrets list <functionId>` to view attached secrets and `browse functions secrets attach <functionId> <secretId>` to attach the missing secret.

## Features

* **No local browsers needed**: Browserbase provides the browser, so your Function ships without a Chrome binary.
* **Encrypted secrets**: your API key stays in a Browserbase project secret, and the Function reads it from `context.secrets`.
* **No model provider setup**: the Model Gateway picks a model for each call, and Browserbase charges for the tokens. You only need your Browserbase API key.

## Other deployment options

<Accordion title="Deploy on Vercel">
  Run Stagehand on Browserbase inside a Vercel Function. This guide shows a minimal HTTP endpoint you can call directly or on a schedule.

  <Warning>
    Every request to this endpoint opens a Browserbase session and spends model tokens. Vercel serves production deployments publicly, so gate the handler before you deploy it. The handlers below reject any request that does not present `Authorization: Bearer $CRON_SECRET`, the same header Vercel sends on cron invocations.
  </Warning>

  <Steps>
    <Step title="Install Vercel CLI">
      To download and install Vercel CLI, run one of the following commands:

      ```bash theme={null}
      pnpm i -g vercel
      # npm i -g vercel
      # yarn global add vercel
      # bun add -g vercel
      ```
    </Step>

    <Step title="Project layout">
      <Tabs>
        <Tab title="TypeScript">
          ```text theme={null}
          your-project/
            api/
              run.ts
            package.json
            tsconfig.json
            vercel.json
          ```
        </Tab>

        <Tab title="Python">
          ```text theme={null}
          your-project/
            api/
              run.py
            requirements.txt
            vercel.json
          ```
        </Tab>

        <Tab title="Go">
          ```text theme={null}
          your-project/
            api/
              run.go
            go.mod
            vercel.json
          ```
        </Tab>
      </Tabs>

      Create the structure with:

      <Tabs>
        <Tab title="TypeScript">
          ```bash theme={null}
          mkdir -p api
          touch api/run.ts package.json vercel.json tsconfig.json
          ```
        </Tab>

        <Tab title="Python">
          ```bash theme={null}
          mkdir -p api
          touch api/run.py requirements.txt vercel.json
          ```
        </Tab>

        <Tab title="Go">
          ```bash theme={null}
          mkdir -p api
          touch api/run.go vercel.json
          go mod init example.com/bb-stagehand-on-vercel
          ```
        </Tab>
      </Tabs>
    </Step>

    <Step title="api/run.ts (Node.js runtime)">
      <Tabs>
        <Tab title="TypeScript">
          ```typescript theme={null}
          // api/run.ts
          import type { VercelRequest, VercelResponse } from "@vercel/node";
          import { browserbase, Stagehand } from "@browserbasehq/stagehand";
          import { z } from "zod/v4";

          async function run(): Promise<string> {
            const browser = await browserbase.launch({
              apiKey: process.env.BROWSERBASE_API_KEY!,
              region: "us-west-2",
              browserSettings: {
                blockAds: true,
              },
            });

            const stagehand = await Stagehand.create({
              browser,
              model: {
                modelName: "google/gemini-3.8-flash",
              },
              logging: { level: "warn", format: "json" },
            });

            try {
              const page = await browser.context.activePage();
              if (!page) throw new Error("Stagehand initialized without an active page");

              await page.goto("https://www.stagehand.dev/");
              await stagehand.act("click the evals button");

              const { data } = await stagehand.extract(
                "extract the fastest model",
                z.object({ fastestModel: z.string() }),
              );
              return data.fastestModel;
            } finally {
              await stagehand.close();
            }
          }

          export default async function handler(req: VercelRequest, res: VercelResponse): Promise<void> {
            const secret = process.env.CRON_SECRET;
            if (!secret || req.headers.authorization !== `Bearer ${secret}`) {
              res.status(401).json({ ok: false, error: "unauthorized" });
              return;
            }

            try {
              const data = await run();
              res.status(200).json({ ok: true, data });
            } catch (err: unknown) {
              const msg = err instanceof Error ? err.message : String(err);
              res.status(500).json({ ok: false, error: msg });
            }
          }
          ```
        </Tab>

        <Tab title="Python">
          ```python theme={null}
          # api/run.py
          import json
          import os
          from http.server import BaseHTTPRequestHandler

          import anyio
          from pydantic import BaseModel

          from stagehand import BrowserbaseBrowserSettings, Stagehand, StagehandClientLoggingConfig, browserbase

          class FastestModel(BaseModel):
              fastest_model: str

          async def run() -> str:
              browser = await browserbase.launch(api_key=os.environ["BROWSERBASE_API_KEY"], region="us-west-2", browser_settings=BrowserbaseBrowserSettings.model_validate({"block_ads": True}))

              stagehand = await Stagehand.create(
                  browser=browser,
                  model="google/gemini-3.8-flash",
                  model_api_key=os.environ["GOOGLE_API_KEY"],
                  logging=StagehandClientLoggingConfig(level="warn", format="json"),
              )

              try:
                  page = await browser.context.active_page()
                  if page is None:
                      raise RuntimeError("Stagehand initialized without an active page")

                  await page.goto("https://www.stagehand.dev/")
                  await stagehand.act("click the evals button")

                  result = await stagehand.extract(
                      instruction="extract the fastest model",
                      schema=FastestModel,
                  )
                  return result.data.fastest_model
              finally:
                  await stagehand.close()

          class handler(BaseHTTPRequestHandler):
              def do_POST(self) -> None:
                  secret = os.environ.get("CRON_SECRET")
                  if not secret or self.headers.get("Authorization") != f"Bearer {secret}":
                      self._respond(401, {"ok": False, "error": "unauthorized"})
                      return

                  try:
                      data = anyio.run(run)
                      body = {"ok": True, "data": data}
                      status = 200
                  except Exception as error:
                      body = {"ok": False, "error": str(error)}
                      status = 500

                  self._respond(status, body)

              # Vercel cron jobs invoke the function with GET.
              do_GET = do_POST

              def _respond(self, status: int, body: dict[str, object]) -> None:
                  self.send_response(status)
                  self.send_header("Content-Type", "application/json")
                  self.end_headers()
                  self.wfile.write(json.dumps(body).encode())
          ```
        </Tab>

        <Tab title="Go">
          ```go theme={null}
          // api/run.go
          package main

          import (
          	"context"
          	"encoding/json"
          	"errors"
          	"fmt"
          	"net/http"
          	"os"

          	stagehand "github.com/browserbase/stagehand/packages/sdk-go/v4"
          )

          type fastestModel struct {
          	FastestModel string `json:"fastest_model"`
          }


          func Handler(w http.ResponseWriter, r *http.Request) {
          	w.Header().Set("Content-Type", "application/json")

          	secret := os.Getenv("CRON_SECRET")
          	if secret == "" || r.Header.Get("Authorization") != "Bearer "+secret {
          		w.WriteHeader(http.StatusUnauthorized)
          		_ = json.NewEncoder(w).Encode(map[string]any{"ok": false, "error": "unauthorized"})
          		return
          	}

          	data, err := run(r.Context())
          	if err != nil {
          		w.WriteHeader(http.StatusInternalServerError)
          		_ = json.NewEncoder(w).Encode(map[string]any{"ok": false, "error": err.Error()})
          		return
          	}
          	_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "data": data})
          }

          func run(ctx context.Context) (result string, err error) {
          	apiKey := os.Getenv("BROWSERBASE_API_KEY")
          	modelAPIKey := os.Getenv("GOOGLE_API_KEY")
          	region := stagehand.BrowserbaseRegionUSWest2
          	blockAds := true

          	model := stagehand.ModelConfig{
          		ModelName: "google/gemini-3.8-flash",
          		APIKey:    &modelAPIKey,
          	}

          	browser, err := stagehand.LaunchBrowserbase(ctx, stagehand.BrowserbaseLaunchOptions{
          		APIKey: apiKey,
          		Region: &region,
          		BrowserSettings: &stagehand.BrowserbaseBrowserSettings{
          			BlockAds: &blockAds,
          		},
          	})
          	if err != nil {
          		return "", err
          	}

          	client, err := stagehand.Create(ctx, stagehand.CreateOptions{
          		Browser: browser,
          		Model:   &model,
          		Logging: &stagehand.StagehandClientLoggingConfig{
          			Level:  stagehand.StagehandClientLogLevelWarn,
          			Format: stagehand.StagehandClientLogFormatJSON,
          		},
          	})
          	if err != nil {
          		return "", err
          	}

          	defer func() { err = errors.Join(err, client.Close(ctx)) }()

          	browserContext, err := browser.Context()
          	if err != nil {
          		return "", err
          	}
          	page, err := browserContext.ActivePage(ctx)
          	if err != nil {
          		return "", err
          	}
          	if page == nil {
          		return "", errors.New("Stagehand initialized without an active page")
          	}

          	if _, err := page.Goto(ctx, "https://www.stagehand.dev/", nil); err != nil {
          		return "", err
          	}
          	if _, err := client.Act(ctx, stagehand.ActInstruction("click the evals button"), nil); err != nil {
          		return "", err
          	}

          	extracted, err := stagehand.Extract[fastestModel](
          		ctx,
          		client,
          		"extract the fastest model",
          		nil,
          	)
          	if err != nil {
          		return "", err
          	}

          	return extracted.Data.FastestModel, nil
          }

          func main() {
          	http.HandleFunc("/api/run", Handler)
          	port := os.Getenv("PORT")
          	if port == "" {
          		port = "3000"
          	}
          	fmt.Println("listening on", port)
          	_ = http.ListenAndServe(":"+port, nil)
          }
          ```
        </Tab>
      </Tabs>
    </Step>

    <Step title="package.json">
      <Tabs>
        <Tab title="TypeScript">
          ```json theme={null}
          {
              "name": "bb-stagehand-on-vercel",
              "private": true,
              "type": "module",
              "engines": { "node": ">=22.18" },
              "dependencies": {
                "@browserbasehq/stagehand": "^4.0.0",
                "zod": "^4.0.0"
              },
              "devDependencies": {
                "@types/node": "^22.0.0",
                "@vercel/node": "^3.2.20",
                "typescript": "^5.2.2"
              }
          }
          ```
        </Tab>

        <Tab title="Python">
          ```text theme={null}
          # requirements.txt
          stagehand
          anyio
          ```
        </Tab>

        <Tab title="Go">
          ```text theme={null}
          // go.mod
          module example.com/bb-stagehand-on-vercel

          go 1.26.0

          require github.com/browserbase/stagehand/packages/sdk-go/v4 v4.0.0
          ```
        </Tab>
      </Tabs>
    </Step>

    <Step title="tsconfig.json">
      <Tabs>
        <Tab title="TypeScript">
          ```json theme={null}
          {
            "compilerOptions": {
              "target": "ES2022",
              "module": "ES2022",
              "moduleResolution": "node",
              "outDir": ".vercel/output/functions",
              "strict": true,
              "esModuleInterop": true,
              "skipLibCheck": true,
              "types": ["node"]
            },
            "include": ["api/**/*.ts"]
          }
          ```
        </Tab>

        <Tab title="Python">
          ```text theme={null}
          Not applicable. Python functions on Vercel need no compiler configuration;
          declare your dependencies in requirements.txt instead.
          ```
        </Tab>

        <Tab title="Go">
          ```text theme={null}
          Not applicable. Go functions on Vercel need no compiler configuration;
          declare your dependencies in go.mod instead.
          ```
        </Tab>
      </Tabs>
    </Step>

    <Step title="vercel.json">
      <Tabs>
        <Tab title="TypeScript">
          ```json theme={null}
          {
            "$schema": "https://openapi.vercel.sh/vercel.json",
            "functions": {
              "api/run.ts": {
                "maxDuration": 60
              }
            }
          }
          ```
        </Tab>

        <Tab title="Python">
          ```json theme={null}
          {
            "$schema": "https://openapi.vercel.sh/vercel.json",
            "functions": {
              "api/run.py": {
                "maxDuration": 60
              }
            }
          }
          ```
        </Tab>

        <Tab title="Go">
          ```json theme={null}
          {
            "$schema": "https://openapi.vercel.sh/vercel.json",
            "functions": {
              "api/run.go": {
                "maxDuration": 60
              }
            }
          }
          ```
        </Tab>
      </Tabs>

      See Vercel's [configuring functions](https://vercel.com/docs/functions/configuring-functions) docs for more details.
    </Step>

    <Step title="Link your project">
      Link your local folder to a Vercel project before configuring environment variables:

      ```bash theme={null}
      # authenticate if needed
      vercel login

      # link the current directory to a Vercel project (interactive)
      vercel link
      ```
    </Step>

    <Step title="Environment variables">
      Never commit secrets. Add variables via the Vercel CLI, then read them in your handler and pass them where you launch the browser and create the Stagehand client. `CRON_SECRET` is the secret the handler compares against the `Authorization` header, and Vercel sends it automatically when a cron job invokes the function:

      ```bash theme={null}
      vercel env add BROWSERBASE_API_KEY
      # (and your model key if needed)
      vercel env add GOOGLE_API_KEY

      # the shared secret the handler requires on every request
      # generate one with: openssl rand -hex 32
      vercel env add CRON_SECRET
      ```

      <Tip>
        Using the [Model Gateway](/v4/configuration/models#model-gateway) means `BROWSERBASE_API_KEY` is the only secret you need to deploy.
      </Tip>

      See also: [Browser Configuration](/v4/configuration/browser) for details on required variables.
    </Step>

    <Step title="Test locally">
      Replicate the Vercel environment locally to exercise your Function before deploying. Run from the project root.

      <Tabs>
        <Tab title="TypeScript">
          ```bash theme={null}
          # ensure dependencies are installed
          pnpm install

          # start the local Vercel dev server
          vercel dev --listen 5005
          ```
        </Tab>

        <Tab title="Python">
          ```bash theme={null}
          # ensure dependencies are installed
          pip install -r requirements.txt

          # start the local Vercel dev server
          vercel dev --listen 5005
          ```
        </Tab>

        <Tab title="Go">
          ```bash theme={null}
          # ensure dependencies are installed
          go mod tidy

          # start the local Vercel dev server
          vercel dev --listen 5005
          ```
        </Tab>
      </Tabs>
    </Step>

    <Step title="Deploy">
      ```bash theme={null}
      vercel
      vercel --prod
      ```
    </Step>

    <Step title="Execute the function">
      Every request needs `Authorization: Bearer $CRON_SECRET`. Requests without it get a 401 and never reach Browserbase.
    </Step>

    <Step title="Configure protection bypass for automation">
      If the deployment also sits behind Vercel Deployment Protection, create a Protection Bypass for Automation so scripted callers can reach it:

      1. Generate a 32-character secret (you can use `openssl rand -hex 16`)
      2. Go to your project in Vercel
      3. Navigate to Settings, then Deployment Protection
      4. Add the secret to "Protection Bypass for Automation"

      Then invoke the function with the bypass header:

      ```bash theme={null}
      curl -X POST \
        -H "Authorization: Bearer <your-CRON_SECRET>" \
        -H "x-vercel-protection-bypass: <your-32-character-secret>" \
        https://<your-deployment>/api/run
      ```
    </Step>

    <Step title="Optional: cron on Vercel">
      Hit the same endpoint on a schedule by extending `vercel.json`. Cron invocations arrive as `GET` requests carrying `Authorization: Bearer $CRON_SECRET`, so the handler's secret check passes without extra configuration:

      <Tabs>
        <Tab title="TypeScript">
          ```json theme={null}
          {
            "$schema": "https://openapi.vercel.sh/vercel.json",
            "functions": {
              "api/run.ts": {
                "maxDuration": 60
              }
            },
            "crons": [
              { "path": "/api/run", "schedule": "0 * * * *" }
            ]
          }
          ```
        </Tab>

        <Tab title="Python">
          ```json theme={null}
          {
            "$schema": "https://openapi.vercel.sh/vercel.json",
            "functions": {
              "api/run.py": {
                "maxDuration": 60
              }
            },
            "crons": [
              { "path": "/api/run", "schedule": "0 * * * *" }
            ]
          }
          ```
        </Tab>

        <Tab title="Go">
          ```json theme={null}
          {
            "$schema": "https://openapi.vercel.sh/vercel.json",
            "functions": {
              "api/run.go": {
                "maxDuration": 60
              }
            },
            "crons": [
              { "path": "/api/run", "schedule": "0 * * * *" }
            ]
          }
          ```
        </Tab>
      </Tabs>
    </Step>
  </Steps>

  **Features**

  * **No local browsers needed** with a Browserbase browser. [Browserbase](https://www.browserbase.com/) provides the browsers, so your function ships without a Chrome binary.
  * **Fast functionality**: Offload browser work to Browserbase and return JSON promptly.
  * **Long-running tasks**: Raise `maxDuration` and/or consider Edge runtime limits depending on plan.
  * **Cheap repeat runs**: Turn on [server-side caching](/v4/best-practices/caching) so scheduled invocations replay recorded actions instead of paying for inference every hour.
</Accordion>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.