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

<Tip>
  **🌟 Preview: Browser Functions** - Deploy your web automation code directly on Browserbase with browser functions. Scale your `act()` automations in the cloud with zero infrastructure setup. Reach out to [hello@browserbase.com](mailto:hello@browserbase.com) to get beta access.
</Tip>

## 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-2.5-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-2.5-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"
        )

        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-2.5-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.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
        npm 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.
