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).
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)
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.
Webhook notifications
Receive a callback when a run finishes instead of polling. There are two events that 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. 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. type is succeeded or failed (a timed-out or stopped run is delivered as failed), and success mirrors it as a boolean:
{
"success": true,
"type": "succeeded",
"id": "run_abc123def456",
"data": { "answer": "..." },
"error": null
}
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 or running 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). |
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. |
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. |
webhook | object | No | — | Webhook configuration for terminal-state notifications. 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).
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"] | Which events to deliver. succeeded fires when the run completes; failed fires when the run fails, times out, or is stopped. Must be a subset of ["succeeded", "failed"]. |
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. |
steps | string[] | Short human-readable rationales streamed as the agent works. |
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. |
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
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.