> For the complete documentation index, see [llms.txt](/llms.txt)

# Agent Run API

> **BETA**
> The Agent Run API is currently in beta. Parameters and response shapes may change in future releases.

> The Agent Run API is only available for Cloud plans. [Contact us for more information here.](https://www.browserless.io/contact/)

Run a managed AI agent that drives a real browser to complete a task described in plain English. The agent plans and executes its own browser steps (navigate, click, type, read) and returns a structured, LLM-ready result. You can optionally constrain the run with a JSON output schema, a starting URL, a navigation allowlist, an authentication profile, or webhook notifications.

The browser and the model are fully managed. You do not supply an AI model key or run a browser yourself.

**Endpoints**

- **Start a run:** `POST /agent/run`
- **Get run status and result:** GET /agent/run/&#123;id&#125;
- **List all runs:** `GET /agent/run`
- **Cancel or delete a run:** DELETE /agent/run/&#123;id&#125;
- Auth: `token` query parameter (`?token=`)
- Content-Type: `application/json`
- Response: `application/json`

- A Browserless API token from your [account
  dashboard](https://browserless.io/account/)

## Quickstart

Submit a task inside the query. The response returns a run ID immediately while the agent runs asynchronously in the background.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Go to https://example.com and tell me the exact text of the top-level heading."
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query:
    "Go to https://example.com and tell me the exact text of the top-level heading.",
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Go to https://example.com and tell me the exact text of the top-level heading."
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response** — `200 OK`

```js
{
  "id": "run_abc123def456",
  "status": "pending"
}
```

Use the `id` to poll for status and the result via `GET /agent/run/&#123;id&#125;`. The `query` must be at least 10 characters.

## Polling for results

Poll GET /agent/run/&#123;id&#125; on an interval until `status` reaches a terminal value (`succeeded`, `failed`, `timed_out`, or `stopped`). A single request right after submitting usually returns `pending`. Each poll returns the steps the agent has taken so far (short, human-readable rationales), so you can watch its progress.

  
**cURL:**

```sh
# Repeat this request on an interval until "status" is terminal
# (succeeded, failed, timed_out, or stopped).
curl --request GET \
  --url 'https://production-sfo.browserless.io/agent/run/run_abc123def456?token=YOUR_API_TOKEN_HERE'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const runId = "run_abc123def456";
const url = `https://production-sfo.browserless.io/agent/run/${runId}?token=${TOKEN}`;
const TERMINAL = ["succeeded", "failed", "timed_out", "stopped"];

const poll = async () => {
  while (true) {
    const response = await fetch(url);
    const result = await response.json();
    console.log(result.status, result.steps?.at(-1) ?? "");
    if (TERMINAL.includes(result.status)) return result;
    await new Promise((resolve) => setTimeout(resolve, 3000));
  }
};

const result = await poll();
console.log(result.data);
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
run_id = "run_abc123def456"
url = f"https://production-sfo.browserless.io/agent/run/{run_id}?token={TOKEN}"
TERMINAL = {"succeeded", "failed", "timed_out", "stopped"}

while True:
    result = requests.get(url).json()
    print(result["status"], (result.get("steps") or [""])[-1])
    if result["status"] in TERMINAL:
        break
    time.sleep(3)

print(result["data"])
```

  

**Response**

```js
{
  "id": "run_abc123def456",
  "status": "succeeded",
  "data": {
    "answer": "The exact text of the top-level heading is: Example Domain"
  },
  "error": null,
  "steps": [
    "Opening the example homepage",
    "Reading the main heading",
    "Closing the completed browser session"
  ],
  "turns": [
    {
      "turnId": "turn_0a1b2c3d4e5f",
      "query": "Go to https://example.com and tell me the exact text of the top-level heading.",
      "status": "succeeded",
      "data": {
        "answer": "The exact text of the top-level heading is: Example Domain"
      },
      "steps": ["Opening the example homepage", "Reading the main heading"],
      "units": 4
    }
  ],
  "created_at": "2025-06-30T10:00:00.000Z",
  "completed_at": "2025-06-30T10:00:18.000Z",
  "expires_at": "2025-07-07T10:00:18.000Z"
}
```

By default `data` is `{ "answer": "<the agent's answer>" }`. When you provide a [`responseSchema`](#structured-output), `data` is shaped to match it instead. Results are retained for **7 days** after completion (see `expires_at`).

## Human handoff

Set `onHumanNeeded: "pause"` when starting a run to let a person handle a login wall or step-up MFA in the same browser session. The default, `"fail"`, ends the run instead of pausing for help.

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "Sign in to my account and summarize my recent orders.",
    "startUrl": "https://example.com/login",
    "onHumanNeeded": "pause",
    "timeout": 300000
  }'
```

Keep polling the run. `needs_human` is a paused, non-terminal status. Its response includes a `humanRequest` object:

```json
{
  "reason": "step_up_mfa",
  "instructions": "Enter the verification code, then choose Done.",
  "liveURL": "https://production-sfo.browserless.io/live/index.html?i=...",
  "expiresAt": "2026-10-01T12:05:00.000Z"
}
```

Open `humanRequest.liveURL` to interact with the page. The handoff viewer hides its browser navigation controls and resizes the remote viewport to fit the viewer. Its **Instructions** panel contains **Done** and **Couldn't finish**. If there are no custom instructions, the panel shows “Finish interacting with the page, then choose Done.” If the panel is collapsed, use **Show instructions** to reopen it.

![LiveURL Instructions panel requesting an MFA code, with Couldn't finish and Done buttons](/img/agent-human-handoff-instructions.jpg)

- Choose **Done** after completing the requested step. The agent inspects the current page and continues the original task in the same session. This does not mark the run as succeeded; keep polling for its final result.
- Choose **Couldn't finish** if you cannot complete the step. The run fails rather than resuming automation.
- Use `DELETE /agent/run/{id}` to cancel while paused. The run becomes `stopped`.

The original run and browser-session deadlines keep counting down during the handoff. The live URL expires no later than either deadline. Opening the viewer or choosing **Done** does not extend the timeout or reserve time for the resumed agent. There is no `humanTimeoutMs` option or minimum pause/resume window; choose a sufficient `timeout` when starting the run.

### Handoff notifications and outcomes

A webhook can notify your application when the run pauses. With `onHumanNeeded: "pause"`, omitted or empty `webhook.events` includes `needs_human` alongside the terminal events. If you supply a nonempty event list, include `needs_human` explicitly to receive handoff notifications.

A handoff notification has `type: "needs_human"`, `success: null`, the run `id`, and the `reason`, optional `instructions`, `liveURL`, and `expiresAt` fields at the top level. It is not a completion notification: continue polling or wait for a terminal webhook.

After the handoff ends, subsequent poll responses retain only `humanRequest.reason` and `humanRequest.outcome`; the URL, instructions, and link expiry are removed. Outcomes are `resumed`, `userFailed`, `expired`, `stopped`, or `closed`. Use the run's `status` to determine whether it is still active or has finished.

## Structured output

Pass a JSON Schema in `responseSchema` to ask the agent to return its answer in a specific shape. This is best-effort: the agent is instructed to conform to the schema, but conformance is not strictly enforced, so validate the result on your side.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Go to https://news.ycombinator.com and list the titles of the top 3 stories.",
  "responseSchema": {
    "type": "object",
    "properties": {
      "stories": { "type": "array", "items": { "type": "string" } }
    },
    "required": ["stories"]
  }
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query:
    "Go to https://news.ycombinator.com and list the titles of the top 3 stories.",
  responseSchema: {
    type: "object",
    properties: {
      stories: { type: "array", items: { type: "string" } },
    },
    required: ["stories"],
  },
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Go to https://news.ycombinator.com and list the titles of the top 3 stories.",
    "responseSchema": {
        "type": "object",
        "properties": {
            "stories": {"type": "array", "items": {"type": "string"}}
        },
        "required": ["stories"]
    }
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Page captures

Add `formats` to capture the agent's current tab when the run ends. Choose any combination of `html`, `markdown`, `links`, and `screenshot`. Omit it or pass an empty array for no captures; duplicate names are ignored. Captures supplement the agent's `data` and `steps`, rather than replacing them.

Replace `TOKEN` with your API token:

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "query": "Open https://example.com and tell me the main heading",
    "formats": ["html", "markdown", "links", "screenshot"]
  }'
```

Poll the returned run ID as usual. Its completed result includes these top-level fields, also included in webhook notifications:

```json
{
  "id": "run_abc123def456",
  "status": "succeeded",
  "data": { "answer": "Example Domain" },
  "error": null,
  "steps": ["Opening example.com", "Reading the heading"],
  "html": "<html><body><h1>Example Domain</h1></body></html>",
  "markdown": "Example Domain\n==============",
  "links": [],
  "screenshot": "iVBORw0KGgo…"
}
```

The screenshot is a **visible-viewport PNG encoded as base64**, not a URL or a full-page image. The example abbreviates the base64. Each capture is capped at **5 MiB, or 5,242,880 bytes**; the screenshot cap applies to its base64 string, and the links cap to the serialized JSON array. Oversized captures are omitted, never truncated.

Capture is attempted on any terminal outcome, including failures and timeouts, when the worker-owned browser is still available. Cancellation can become visible before capture finishes. Capture failures do not change the run status or the agent's answer. Captures have a total 15-second budget and are not repeated on polling.

All four fields are always present and nullable. Unrequested formats are `null`. When a requested capture is missing, `formatErrors` maps its name to one of these reasons; the key is omitted when no requested capture is missing. Captures are not repeated inside `turns`.

| Reason | Meaning |
| --- | --- |
| `session_unavailable` | The run has no live worker-owned agent connection to capture from. |
| `blocked_saved_credentials` | Saved credentials were resolved in the browser, so all requested page captures are blocked. |
| `too_large` | This capture exceeded the per-format size cap. |
| `capture_failed` | Capture failed or exceeded the total time budget. |
| `not_captured` | A requested capture has not been stored, for example while the run is still active. |

## Guiding the agent

Steer where the agent starts and how far it can wander. `startUrl` sets the first navigation target, `allowedDomains` restricts navigation to an allowlist, and `maxSteps` bounds how many browser actions the agent may take (default `30`, hard cap `60`).

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Find the current price of the Pro plan.",
  "startUrl": "https://www.browserless.io/pricing",
  "allowedDomains": ["browserless.io"],
  "maxSteps": 15
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query: "Find the current price of the Pro plan.",
  startUrl: "https://www.browserless.io/pricing",
  allowedDomains: ["browserless.io"],
  maxSteps: 15,
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Find the current price of the Pro plan.",
    "startUrl": "https://www.browserless.io/pricing",
    "allowedDomains": ["browserless.io"],
    "maxSteps": 15
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Browsing hints

`proxy`, `proxyCountry`, and `stealth` configure the browser session the agent drives. The run applies them when it provisions the session (proxy network, exit country, stealth mode) and repeats them in the agent's instructions. They're fixed for the life of the run; the agent can't change them mid-task. `solveCaptchas` works differently: it tells the agent to use its CAPTCHA-solving tool when it hits one, it is not a session setting.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Check whether this product is in stock and report the price.",
  "startUrl": "https://www.example-shop.com/product/123",
  "proxy": "residential",
  "proxyCountry": "US",
  "stealth": true,
  "solveCaptchas": true
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query: "Check whether this product is in stock and report the price.",
  startUrl: "https://www.example-shop.com/product/123",
  proxy: "residential",
  proxyCountry: "US",
  stealth: true,
  solveCaptchas: true,
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Check whether this product is in stock and report the price.",
    "startUrl": "https://www.example-shop.com/product/123",
    "proxy": "residential",
    "proxyCountry": "US",
    "stealth": True,
    "solveCaptchas": True
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

## Using a profile

Run against authenticated pages by passing a saved [profile](/baas/features/authenticated-profiles) name in the `profile` field. The agent's browser loads the profile's cookies, `localStorage`, and `IndexedDB` before it starts, so it browses as the logged-in user.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Open my dashboard and tell me how many open tickets I have.",
  "startUrl": "https://app.example.com/dashboard",
  "profile": "acme-prod"
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query: "Open my dashboard and tell me how many open tickets I have.",
  startUrl: "https://app.example.com/dashboard",
  profile: "acme-prod",
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Open my dashboard and tell me how many open tickets I have.",
    "startUrl": "https://app.example.com/dashboard",
    "profile": "acme-prod"
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

> **Tip**
> Create and manage profiles via the [Authenticated Profiles](/baas/features/authenticated-profiles) workflow. The profile name is scoped to your API token.

## Recording the run

Set `replay: true` to record the run as a [session replay](/baas/monitor-sessions/session-replay). The recording shows up in your account dashboard's [Session Replay](https://browserless.io/account/session-replay) list once the run's browser session closes. Recording is off by default.

Unlike the browsing hints, `replay` never reaches the agent's instructions. The run applies it only when it provisions the browser session. We keep it out of the prompt because whether a run gets recorded is your decision, not the model's.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Go to https://example.com and tell me the exact text of the top-level heading.",
  "replay": true
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query:
    "Go to https://example.com and tell me the exact text of the top-level heading.",
  replay: true,
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Go to https://example.com and tell me the exact text of the top-level heading.",
    "replay": True
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

**Response** — `200 OK`

```js
{
  "id": "run_abc123def456",
  "status": "pending"
}
```

Session replay records DOM events, not video. It's a different feature from [Screen Recording](/baas/monitor-sessions/screen-recording), which `/agent/run` doesn't support: a client connected to the browser starts and stops screen recording, and this route has no client connection.

> **Caution**
> A run can finish without a recording even when you set `replay: true`. The run normally provisions its own browser session and applies `replay` to it. If that provisioning fails, the run falls back to a session it doesn't control, the flag can't reach that session, and the run completes unrecorded. Neither the run's result nor its webhook payload tells you when this happens.

## Webhook notifications

Receive a callback when a run finishes instead of polling. Two terminal events can fire: `succeeded` (the run finished with a result) and `failed` (the run failed, timed out, or was stopped). Subscribing to both covers every terminal outcome. The non-terminal `needs_human` event reports a pause for human input; see [Handoff notifications and outcomes](#handoff-notifications-and-outcomes). Provide an HTTPS URL and, optionally, the subset of events you want.

  
**cURL:**

```sh
curl --request POST \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE' \
  --header 'Content-Type: application/json' \
  --data '{
  "query": "Summarize the latest blog post on browserless.io.",
  "webhook": {
    "url": "https://your-server.com/webhook",
    "events": ["succeeded", "failed"]
  }
}'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}`;
const headers = {
  "Content-Type": "application/json",
};

const data = {
  query: "Summarize the latest blog post on browserless.io.",
  webhook: {
    url: "https://your-server.com/webhook",
    events: ["succeeded", "failed"],
  },
};

const startRun = async () => {
  const response = await fetch(url, {
    method: "POST",
    headers: headers,
    body: JSON.stringify(data),
  });

  const result = await response.json();
  console.log(result);
};

startRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}"
headers = {
    "Content-Type": "application/json"
}

data = {
    "query": "Summarize the latest blog post on browserless.io.",
    "webhook": {
        "url": "https://your-server.com/webhook",
        "events": ["succeeded", "failed"]
    }
}

response = requests.post(url, headers=headers, json=data)
result = response.json()

print(result)
```

  

When the run finishes, Browserless `POST`s the following payload to your webhook URL. For these terminal notifications, `type` is `succeeded` or `failed` (a timed-out or stopped run is delivered as `failed`), and `success` mirrors it as a boolean. A handoff notification instead has `type: "needs_human"` and `success: null`, with the [handoff fields described above](#handoff-notifications-and-outcomes):

```js
{
  "success": true,
  "type": "succeeded",
  "id": "run_abc123def456",
  "data": { "answer": "..." },
  "error": null,
  "html": null,
  "markdown": null,
  "links": null,
  "screenshot": null
}
```

If you requested [page captures](#page-captures), the webhook includes their values in `html`, `markdown`, `links`, and `screenshot`, plus `formatErrors` when a requested capture is missing. Without `formats`, all four values are `null` and `formatErrors` is absent.

The webhook URL must be `https://` and must not resolve to a private or internal network address. If the operator has configured a signing key, deliveries include an `X-Browserless-Signature: sha256=<hmac>` header you can use to verify authenticity.

## Cancelling a run

Send a `DELETE` with the run ID. A pending, running, or `needs_human` run is cancelled (its in-flight work is aborted and the run moves to `stopped`); a run that has already finished is deleted.

**Cancel an active run** (200):

```js
{
  "status": "stopped"
}
```

**Delete a finished run** (200):

```js
{
  "status": "deleted"
}
```

If the run reaches a terminal state at the same moment you cancel, the API returns a `409 Conflict`:

```js
{
  "id": "run_abc123def456",
  "status": "succeeded",
  "message": "Run is already succeeded"
}
```

  
**cURL:**

```sh
curl --request DELETE \
  --url 'https://production-sfo.browserless.io/agent/run/run_abc123def456?token=YOUR_API_TOKEN_HERE'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const runId = "run_abc123def456";
const url = `https://production-sfo.browserless.io/agent/run/${runId}?token=${TOKEN}`;

const cancelRun = async () => {
  const response = await fetch(url, {
    method: "DELETE",
  });

  const result = await response.json();
  console.log(result);
};

cancelRun();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
run_id = "run_abc123def456"
url = f"https://production-sfo.browserless.io/agent/run/{run_id}?token={TOKEN}"

response = requests.delete(url)
result = response.json()

print(result)
```

  

## Listing all runs

List your account's agent runs, newest first. Results are paginated — pass the `nextCursor` from a response to fetch the next page.

**Query parameters**

| Parameter | Type     | Default | Description                                                                                                                      |
| --------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `token`   | `string` | —       | Your API token (required).                                                                                                       |
| `limit`   | `number` | `20`    | Results per page (1–100).                                                                                                        |
| `cursor`  | `string` | —       | Opaque pagination cursor from `nextCursor` in a previous response. A malformed cursor is ignored and the first page is returned. |
| `status`  | `string` | —       | Filter by status: `"pending"`, `"running"`, `"succeeded"`, `"failed"`, `"timed_out"`, `"stopped"`, or `"capped"`.                |

  
**cURL:**

```sh
curl --request GET \
  --url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE&limit=20'
```

  
  
**Javascript:**

```js
const TOKEN = "YOUR_API_TOKEN_HERE";
const url = `https://production-sfo.browserless.io/agent/run?token=${TOKEN}&limit=20`;

const listRuns = async () => {
  const response = await fetch(url);
  const result = await response.json();
  console.log(result);
};

listRuns();
```

  
  
**Python:**

```python

TOKEN = "YOUR_API_TOKEN_HERE"
url = f"https://production-sfo.browserless.io/agent/run?token={TOKEN}&limit=20"

response = requests.get(url)
result = response.json()

print(result)
```

  

**Response**

```js
{
  "runs": [
    {
      "id": "run_abc123def456",
      "status": "succeeded",
      "created_at": "2025-06-30T10:00:00.000Z",
      "completed_at": "2025-06-30T10:00:18.000Z"
    },
    {
      "id": "run_def456abc789",
      "status": "running",
      "created_at": "2025-06-30T10:05:00.000Z",
      "completed_at": null
    }
  ],
  "nextCursor": "eyJpZCI6InJ1bl9hYmMxMjNkZWY0NTYifQ"
}
```

## Request body

### POST /agent/run

| Field            | Type       | Required | Default  | Description                                                                                                       |
| ---------------- | ---------- | -------- | -------- | ----------------------------------------------------------------------------------------------------------------- |
| `query`          | `string`   | Yes      | —        | The natural-language task for the agent. Trimmed; must be at least 10 characters.                                 |
| `timeout`        | `number`   | No       | `900000` | Per-turn timeout in milliseconds, clamped to `[10000, 900000]` (15-minute hard cap).                              |
| `onHumanNeeded` | `string` | No | `"fail"` | `"pause"` enables [human handoff](#human-handoff) for login walls and step-up MFA without extending the timeout; `"fail"` ends the run instead. |
| `maxSteps`       | `number`   | No       | `30`     | Soft cap on browser actions the agent may take, clamped to `[1, 60]`.                                             |
| `responseSchema` | `object`   | No       | —        | JSON Schema for the agent's final answer. Best-effort — validate the result on your side.                         |
| `formats`        | `string[]` | No       | —        | Page captures: `html`, `markdown`, `links`, `screenshot`. Omit or pass `[]` for none. See [Page captures](#page-captures). |
| `startUrl`       | `string`   | No       | —        | First navigation target. Must be an `http(s)` URL and must not point to a private/internal network address.       |
| `allowedDomains` | `string[]` | No       | —        | Navigation allowlist passed to the agent.                                                                         |
| `proxy`          | `string`   | No       | —        | Route the agent's browser session through a `"residential"` or `"datacenter"` proxy. Applied at session creation. |
| `proxyCountry`   | `string`   | No       | —        | Two-letter ISO country code for the proxy exit (e.g. `"US"`). Used with `proxy`.                                  |
| `stealth`        | `boolean`  | No       | —        | Run the agent's browser session in stealth / anti-bot-detection mode.                                             |
| `solveCaptchas`  | `boolean`  | No       | —        | Instruct the agent to solve CAPTCHAs it encounters (a prompt instruction, not a session setting).                 |
| `profile`        | `string`   | No       | —        | Name of a saved [authentication profile](/baas/features/authenticated-profiles) to load before browsing.          |
| `replay`         | `boolean`  | No       | `false`  | Record the run as a session replay in your account dashboard. See [Recording the run](#recording-the-run).        |
| `webhook`        | `object`   | No       | —        | Webhook configuration for terminal states and human handoffs. See below.                                                |

Unknown query parameters and unknown body fields are rejected with a `400`. `proxy`, `proxyCountry`, and `stealth` configure the agent's browser session; `solveCaptchas`, `startUrl`, `allowedDomains`, and `maxSteps` steer the agent through its instructions (see [Browsing hints](#browsing-hints)). `replay` configures the session only and is never passed to the agent.

### webhook

| Field    | Type       | Required | Default                   | Description                                                                                                                                                                    |
| -------- | ---------- | -------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `url`    | `string`   | Yes      | —                         | HTTPS URL to receive the notification. Cannot point to a private/internal network address.                                                                                     |
| `events` | `string[]` | No       | `["succeeded", "failed"]` (also `"needs_human"` when pausing is enabled) | Which events to deliver. `succeeded` fires when the run completes; `failed` fires when the run fails, times out, or is stopped. `needs_human` fires when the run pauses for human input. Must be a subset of `["succeeded", "failed", "needs_human"]`. |

## Response fields

### POST /agent/run (start)

| Field    | Type     | Description                                                                    |
| -------- | -------- | ------------------------------------------------------------------------------ |
| `id`     | `string` | The unique run ID (prefixed `run_`). Use it to poll for status and the result. |
| `status` | `string` | Initial run status — `"pending"`.                                              |

### GET /agent/run/&#123;id&#125; (status and result)

| Field          | Type             | Description                                                                                                                                                             |
| -------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | `string`         | The run ID.                                                                                                                                                             |
| `status`       | `string`         | Run status. See [Run statuses](#run-statuses).                                                                                                                          |
| `data`         | `object \| null` | The agent's final result from the latest turn: `{ "answer": string }` by default, or your `responseSchema` shape when provided. `null` until the run produces a result. |
| `error`        | `string \| null` | Error message if the run failed. `null` on success.                                                                                                                     |
| `humanRequest` | `object` | Present when a handoff has been requested. While paused: `reason` (`"login_wall"` or `"step_up_mfa"`), optional `instructions`, `liveURL`, and ISO 8601 `expiresAt`. Afterward: `reason` and `outcome` only. See [Human handoff](#human-handoff). |
| `steps`        | `string[]`       | Short human-readable rationales streamed as the agent works.                                                                                                            |
| `html`         | `string \| null` | Requested HTML from the agent's final current tab. |
| `markdown`     | `string \| null` | Requested Markdown converted from that HTML. |
| `links`        | `string[] \| null` | Requested links extracted from that HTML. |
| `screenshot`   | `string \| null` | Requested viewport PNG encoded as base64. |
| `formatErrors` | `object` | Present only for missing requested captures; maps format names to [reason codes](#page-captures). |
| `turns`        | `AgentTurn[]`    | The turn transcript. In v1 a run has a single turn. See below.                                                                                                          |
| `created_at`   | `string`         | ISO 8601 timestamp when the run was created.                                                                                                                            |
| `completed_at` | `string \| null` | ISO 8601 timestamp when the run finished. `null` while active.                                                                                                          |
| `expires_at`   | `string \| null` | ISO 8601 timestamp when the result expires (7 days after completion). `null` while active.                                                                              |

### AgentTurn

| Field    | Type             | Description                                                                                                          |
| -------- | ---------------- | -------------------------------------------------------------------------------------------------------------------- |
| `turnId` | `string`         | The turn ID (prefixed `turn_`).                                                                                      |
| `query`  | `string`         | The task submitted for this turn.                                                                                    |
| `status` | `string`         | Turn status: `"running"`, `"succeeded"`, `"failed"`, or `"timed_out"` (`"capped"` is reserved for a future release). |
| `data`   | `object \| null` | The turn's result (same shape as the top-level `data`).                                                              |
| `steps`  | `string[]`       | Step rationales for this turn.                                                                                       |
| `units`  | `number`         | Billing units charged for this turn, derived from the model tokens it consumed.                                      |

### GET /agent/run (list all)

| Field        | Type                 | Description                                                               |
| ------------ | -------------------- | ------------------------------------------------------------------------- |
| `runs`       | `AgentRunListItem[]` | Array of runs, newest first.                                              |
| `nextCursor` | `string \| null`     | Cursor for fetching the next page. `null` when there are no more results. |

### AgentRunListItem

| Field          | Type             | Description                                                       |
| -------------- | ---------------- | ----------------------------------------------------------------- |
| `id`           | `string`         | The run ID.                                                       |
| `status`       | `string`         | Run status.                                                       |
| `created_at`   | `string`         | ISO 8601 timestamp when the run was created.                      |
| `completed_at` | `string \| null` | ISO 8601 timestamp when the run finished. `null` if still active. |

## Run statuses

| Status      | Description                                                                                                                             |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `pending`   | Accepted and queued; the agent has not started yet.                                                                                     |
| `running`   | The agent is actively driving the browser.                                                                                              |
| `needs_human` | Paused for human interaction through `humanRequest.liveURL`; the original deadline still applies. |
| `succeeded` | The run completed and produced a result in `data`.                                                                                      |
| `failed`    | The run ended with an error (see `error`).                                                                                              |
| `timed_out` | The run exceeded its `timeout`.                                                                                                         |
| `stopped`   | The run was cancelled via `DELETE`.                                                                                                     |
| `capped`    | Reserved for a future release (strict cost cap). Not emitted in v1, though it is accepted as a `status` filter value when listing runs. |

## Error responses

| Status                    | Description                                                                                                                                                                                                                        |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400 Bad Request`         | Invalid body or query parameters, a `query` shorter than 10 characters, an unknown query/body field, or a task rejected by the relevance/abuse gate (not a browser task, or a prohibited use). Rejected tasks do not create a run. |
| `401 Unauthorized`        | Missing or invalid API token.                                                                                                                                                                                                      |
| `403 Forbidden`           | The API key is not account-scoped. Agent runs are account-owned and require an account-scoped key.                                                                                                                                 |
| `404 Not Found`           | The run ID does not exist or belongs to another token.                                                                                                                                                                             |
| `409 Conflict`            | (`DELETE`) the run reached a terminal state concurrently with your cancel; or the reserved `POST /agent/run/{id}/continue` endpoint, which is not enabled in v1.                                                                   |
| `429 Too Many Requests`   | Concurrent agent-run limit reached for your plan.                                                                                                                                                                                  |
| `503 Service Unavailable` | The agent-run service is suspended, unavailable, or temporarily degraded.                                                                                                                                                          |

## FAQ & Troubleshooting

<details>
<summary>How is this different from the MCP <code>browserless_agent</code> tool?</summary>

They split the reasoning differently. With `/agent/run`, **Browserless's** managed agent does the thinking: you POST a task in natural language, it browses autonomously, and you poll for the structured result. With the [MCP Browser Agent](/mcp/browser-agent), **your** AI model does the thinking: it reads page snapshots, plans, and drives the browser step by step through MCP tool calls. Use `/agent/run` from any HTTP client with no model of your own in the loop; use the MCP tool when an MCP-connected assistant is doing the reasoning.

</details>

<details>
<summary>Why am I getting a <code>401 Unauthorized</code> error?</summary>

Your API token is missing or invalid. Include it as a `?token=` query parameter. Verify the token in your [account dashboard](https://browserless.io/account/).

</details>

<details>
<summary>Why am I getting a <code>403 Forbidden</code> error?</summary>

Agent runs are account-owned and require an account-scoped API key. If your token is not mapped to an account, the API rejects the request with `403`. Contact support if you believe your key should be account-scoped.

</details>

<details>
<summary>My run finished with status <code>succeeded</code> but <code>data.answer</code> is empty</summary>

The agent completed but did not produce answer text — often because the task was ambiguous or the target page could not be reached. Refine the `query`, set a `startUrl`, or inspect `steps` to see what the agent did.

</details>

<details>
<summary>My task was rejected with a <code>400</code> before a run was created</summary>

Every task passes a relevance and abuse gate first. Tasks that don't require a browser (e.g. "write me a poem") and tasks that request a prohibited use are rejected with a `400` and no run is created. Rephrase the task as a concrete browsing objective.

</details>

<details>
<summary>I set <code>replay: true</code> but no recording shows up</summary>

Wait until the run reaches a terminal status, then check [Session Replay](https://browserless.io/account/session-replay) again: the recording uploads only after the run's browser session closes. If it's still missing, the run probably fell back to a session that `replay` can't reach (see [Recording the run](#recording-the-run)). The run's result is unaffected. Send `replay` as a JSON boolean: the API rejects a string such as `"true"` with a `400`.

</details>

## Next steps

  <a href="/rest-apis/crawl" className="next-step-card-link">
    
      <h3 className="next-step-card-title">Crawl API</h3>
      
        asynchronously crawl and scrape a whole site
      
    
  </a>
  <a href="/rest-apis/intro" className="next-step-card-link">
    
      <h3 className="next-step-card-title">REST API overview</h3>
      
        introduction to all available endpoints
      
    
  </a>