Agent Run API
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.
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/{id}
- List all runs:
GET /agent/run - Cancel or delete a run: DELETE /agent/run/{id}
- Auth:
tokenquery parameter (?token=) - Content-Type:
application/json - Response:
application/json
- A Browserless API token from your account dashboard
Quickstart
Submit a task inside the query. The response returns a run ID immediately while the agent runs asynchronously in the background.
- cURL
- Javascript
- Python
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."
}'
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();
import requests
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
{
"id": "run_abc123def456",
"status": "pending"
}
Use the id to poll for status and the result via GET /agent/run/{id}. The query must be at least 10 characters.
Polling for results
Poll GET /agent/run/{id} 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
- Javascript
- Python
# 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'
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);
import time
import requests
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
{
"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, 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.
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:
{
"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.

- 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 becomesstopped.
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
- Javascript
- Python
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"]
}
}'
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();
import requests
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:
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:
{
"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
- Javascript
- Python
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
}'
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();
import requests
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
- Javascript
- Python
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
}'
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();
import requests
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 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
- Javascript
- Python
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"
}'
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();
import requests
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)
Create and manage profiles via the 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. The recording shows up in your account dashboard's 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
- Javascript
- Python
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
}'
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();
import requests
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
{
"id": "run_abc123def456",
"status": "pending"
}
Session replay records DOM events, not video. It's a different feature from 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.
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. Provide an HTTPS URL and, optionally, the subset of events you want.
- cURL
- Javascript
- Python
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"]
}
}'
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();
import requests
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 POSTs 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:
{
"success": true,
"type": "succeeded",
"id": "run_abc123def456",
"data": { "answer": "..." },
"error": null,
"html": null,
"markdown": null,
"links": null,
"screenshot": null
}
If you requested 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):
{
"status": "stopped"
}
Delete a finished run (200):
{
"status": "deleted"
}
If the run reaches a terminal state at the same moment you cancel, the API returns a 409 Conflict:
{
"id": "run_abc123def456",
"status": "succeeded",
"message": "Run is already succeeded"
}
- cURL
- Javascript
- Python
curl --request DELETE \
--url 'https://production-sfo.browserless.io/agent/run/run_abc123def456?token=YOUR_API_TOKEN_HERE'
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();
import requests
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
- Javascript
- Python
curl --request GET \
--url 'https://production-sfo.browserless.io/agent/run?token=YOUR_API_TOKEN_HERE&limit=20'
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();
import requests
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
{
"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 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. |
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 to load before browsing. |
replay | boolean | No | false | Record the run as a session replay in your account dashboard. See 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). 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/{id} (status and result)
| Field | Type | Description |
|---|---|---|
id | string | The run ID. |
status | string | Run status. See 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. |
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. |
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
How is this different from the MCP browserless_agent tool?
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, 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.
Why am I getting a 401 Unauthorized error?
Your API token is missing or invalid. Include it as a ?token= query parameter. Verify the token in your account dashboard.
Why am I getting a 403 Forbidden error?
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.
My run finished with status succeeded but data.answer is empty
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.
My task was rejected with a 400 before a run was created
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.
I set replay: true but no recording shows up
Wait until the run reaches a terminal status, then check 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). The run's result is unaffected. Send replay as a JSON boolean: the API rejects a string such as "true" with a 400.