Skip to main content
POST
/
api
/
v1
/
browser-agents
curl --request POST \
  --url https://api.pre.dev/api/v1/browser-agents \
  --header 'Authorization: Bearer <token>' \
  --header 'Content-Type: application/json' \
  --data '
{
  "tasks": [
    {
      "url": "https://example.com",
      "instruction": "Extract the page heading.",
      "output": {
        "type": "object",
        "properties": {
          "heading": {
            "type": "string"
          }
        },
        "required": [
          "heading"
        ]
      }
    }
  ]
}
'
{
  "id": "<string>",
  "total": 123,
  "completed": 123,
  "results": [
    {
      "url": "<string>",
      "instruction": "<string>",
      "input": {},
      "status": "SUCCESS",
      "data": "<unknown>",
      "creditsUsed": 123,
      "durationMs": 123,
      "error": "<string>",
      "events": [
        {
          "type": "navigation",
          "data": {},
          "ts": "2023-11-07T05:31:56Z"
        }
      ]
    }
  ],
  "totalCreditsUsed": 123,
  "status": "processing",
  "createdAt": "2023-11-07T05:31:56Z",
  "completedAt": "2023-11-07T05:31:56Z",
  "liveEvents": [
    [
      {
        "type": "navigation",
        "data": {},
        "ts": "2023-11-07T05:31:56Z"
      }
    ]
  ],
  "error": "<string>"
}
Run one or more browser-agent tasks in parallel. One request, one clean response — or a live SSE stream, or an async handle to poll.
The tasks field is always an array, even for a single task — so one endpoint covers both “run one task” and “run 1000 tasks in parallel”. No separate batch vs. single-task API.

Overview

  • Method: POST
  • Path: /api/v1/browser-agents
  • Cost: billed per successful task. Failed tasks are free.
  • Max tasks per request: 1000
  • Max in-flight tasks per user: 5000
  • Per-task default timeout: 240000 ms

Headers

HeaderRequiredDescription
AuthorizationBearer YOUR_API_KEY
Content-Typeapplication/json

Request Body

FieldTypeRequiredDescription
tasksTask[]Array of tasks. 1 ≤ length ≤ 1000.
concurrencyintegerParallel workers within this run. 120. Default 5.
asyncbooleanIf true, returns { id, status: "processing" } immediately. Poll GET /:id. Default false.
streambooleanIf true, returns an SSE stream with per-step events + final results. Default false. Mutually exclusive with async.

Task object

FieldTypeRequiredDescription
urlstring (URI)Starting page URL the agent navigates to.
instructionstringNatural-language goal. What should the agent accomplish on this page?
inputRecord<string, string>String values the agent should use during the run — form values, credentials, search queries.
outputJSON SchemaSchema describing the shape of data to extract. If omitted, the agent returns unstructured text.
successConditionstringNatural-language assertion. Task is marked SUCCESS only if this holds at the end.
timeoutMsintegerPer-task max runtime in milliseconds. Default 240000.

Response Modes

Sync (default)

Default behavior. The request holds the HTTP connection until every task in the run completes, then returns the full BatchResult. Best for: small runs (≤ 50 tasks), interactive scripts, jobs where you want one clean response.
curl -X POST https://api.pre.dev/api/v1/browser-agents \
  -H "Authorization: Bearer $PREDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tasks": [
      { "url": "https://example.com", "instruction": "Extract the page heading." }
    ]
  }'
Response (200):
{
  "id": "65f8a9d2c1e4b5a6f7e8d9c0",
  "total": 1,
  "completed": 1,
  "results": [
    {
      "url": "https://example.com",
      "instruction": "Extract the page heading.",
      "status": "SUCCESS",
      "data": { "heading": "Example Domain" },
      "creditsUsed": 0.11,
      "durationMs": 4820
    }
  ],
  "totalCreditsUsed": 0.11,
  "status": "completed",
  "createdAt": "2026-04-16T18:22:10.224Z",
  "completedAt": "2026-04-16T18:22:15.044Z"
}

Async (async: true)

Returns immediately with a batchId. Poll GET /api/v1/browser-agents/:id for progress. Best for: large runs, long-running tasks, fire-and-forget jobs.
curl -X POST https://api.pre.dev/api/v1/browser-agents \
  -H "Authorization: Bearer $PREDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tasks": [{ "url": "https://example.com", "instruction": "Extract the heading." }],
    "async": true
  }'
Response (200):
{
  "id": "65f8a9d2c1e4b5a6f7e8d9c0",
  "total": 1,
  "completed": 0,
  "results": [],
  "totalCreditsUsed": 0,
  "status": "processing"
}

Stream (stream: true)

Returns text/event-stream with per-step events. Each frame has a taskIndex tying it back to a task in the run. Best for: live UI progress, debugging, watching what the agent is doing in real time. SSE frames:
EventWhenPayload
task_eventAgent performs a step (navigation, plan, action, screenshot, validation){ taskIndex, type, data }
task_resultA single task finishes{ taskIndex, ...TaskResult }
doneEntire run finishesFull BatchResult
errorFatal error aborted the run{ error }
Keepalive :keepalive comments are sent every 10s to stop intermediaries from closing the connection.
curl -N -X POST https://api.pre.dev/api/v1/browser-agents \
  -H "Authorization: Bearer $PREDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tasks": [{ "url": "https://example.com", "instruction": "Extract the heading." }],
    "stream": true
  }'
Example frames:
:ok

event: task_event
data: {"taskIndex":0,"type":"navigation","data":{"url":"https://example.com"}}

event: task_event
data: {"taskIndex":0,"type":"screenshot","data":{"url":"https://..."}}

event: task_result
data: {"taskIndex":0,"status":"SUCCESS","data":{"heading":"Example Domain"},"durationMs":4820}

event: done
data: {"id":"65f8...","total":1,"completed":1,"results":[...],"status":"completed"}
If a pod is already serving its SSE cap (5000 concurrent streams), new stream requests get 503. Fall back to async: true + polling.

Response Schemas

BatchResult

FieldTypeDescription
idstringRun id (Mongo ObjectId). Use with GET /:id.
totalintegerNumber of tasks in the run.
completedintegerNumber of tasks finished (any status).
resultsTaskResult[]Per-task results, aligned by taskIndex.
totalCreditsUsednumberSum of credits billed across the run. 1 credit = $0.10, floor 0.1 per billed task.
status"processing" | "completed" | "failed"Run state.
createdAtISO-8601Run creation time.
completedAtISO-8601Run completion time. Omitted while processing.
liveEventsRunnerEvent[][]Only when fetching with includeEvents=true — in-flight event streams for tasks that haven’t yet completed.
errorstringSet only when status === "failed".

TaskResult

FieldTypeDescription
urlstringStarting URL (echoed from the request).
instructionstringTask instruction (echoed).
inputRecord<string, string>Task input (echoed).
statusTaskStatusSee task statuses.
dataanyExtracted data, validated against the task’s output schema. null if no output was specified or the task failed.
creditsUsednumberCredits billed for this task. Floor 0.1 (= $0.01) for SUCCESS; scales up with task complexity. Zero for non-SUCCESS statuses.
durationMsintegerWall-clock runtime.
errorstringFailure reason, when status is not SUCCESS.
eventsRunnerEvent[]Full step timeline. Only returned when fetching with includeEvents=true.

Task statuses

StatusMeaningBilled?
SUCCESSTask completed; output schema (if any) validated.
PENDINGTask queued, not yet started (async/polling response only).
ERRORTask errored during execution.
TIMEOUTTask hit timeoutMs.
BLOCKEDTarget site blocked the agent (bot protection, geo-block, etc.).
CAPTCHA_FAILEDCAPTCHA challenge couldn’t be solved.
LOOPAgent got stuck in a redirect or action loop.
NO_TARGETRequired element wasn’t found on the page.

Status Codes

CodeMeaning
200Sync result, async stub, or start of SSE stream.
400Missing tasks, task without url, or tasks.length > 1000.
401Missing or invalid bearer token.
402Insufficient credits. Top up.
429User’s in-flight queue depth exceeded. Retry later or reduce concurrency.
503SSE capacity exceeded on this pod. Retry with async: true and poll.

Code Examples

Multiple tasks with concurrency

curl -X POST https://api.pre.dev/api/v1/browser-agents \
  -H "Authorization: Bearer $PREDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tasks": [
      { "url": "https://news.ycombinator.com", "instruction": "Extract the top 5 story titles." },
      { "url": "https://www.reddit.com/r/programming", "instruction": "Extract the top 5 post titles." }
    ],
    "concurrency": 2
  }'

Structured extraction with output schema

curl -X POST https://api.pre.dev/api/v1/browser-agents \
  -H "Authorization: Bearer $PREDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tasks": [
      {
        "url": "https://news.ycombinator.com",
        "instruction": "Extract the top 5 stories.",
        "output": {
          "type": "object",
          "properties": {
            "stories": {
              "type": "array",
              "items": {
                "type": "object",
                "properties": {
                  "title": { "type": "string" },
                  "points": { "type": "number" }
                },
                "required": ["title", "points"]
              }
            }
          },
          "required": ["stories"]
        }
      }
    ]
  }'

Python — sync + async + streaming

import json
import time
import requests

API_KEY = "YOUR_API_KEY"
BASE = "https://api.pre.dev/api/v1/browser-agents"
HDR = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}


def run_sync(tasks, concurrency=5):
    r = requests.post(BASE, headers=HDR, json={"tasks": tasks, "concurrency": concurrency})
    r.raise_for_status()
    return r.json()


def run_async(tasks, concurrency=5, poll_every=5):
    r = requests.post(BASE, headers=HDR, json={"tasks": tasks, "concurrency": concurrency, "async": True})
    r.raise_for_status()
    batch_id = r.json()["id"]
    while True:
        poll = requests.get(f"{BASE}/{batch_id}", headers=HDR).json()
        if poll["status"] in ("completed", "failed"):
            return poll
        print(f"{poll['completed']}/{poll['total']} done")
        time.sleep(poll_every)


def run_stream(tasks):
    with requests.post(BASE, headers=HDR, json={"tasks": tasks, "stream": True}, stream=True) as r:
        r.raise_for_status()
        event = None
        for line in r.iter_lines(decode_unicode=True):
            if line is None or line.startswith(":"):
                continue
            if line.startswith("event: "):
                event = line[len("event: "):].strip()
            elif line.startswith("data: "):
                data = json.loads(line[len("data: "):])
                yield event, data


# sync
result = run_sync([{"url": "https://example.com", "instruction": "Extract the heading."}])
print(result["results"][0]["data"])

# stream
for event, data in run_stream([{"url": "https://example.com", "instruction": "Extract the heading."}]):
    if event == "task_event":
        print(f"[task {data['taskIndex']}] {data.get('type')}")
    elif event == "task_result":
        print(f"[task {data['taskIndex']}] {data['status']}{data.get('data')}")
    elif event == "done":
        print("batch done:", data["id"])
        break

Node.js — sync + streaming

const API_KEY = process.env.PREDEV_API_KEY;
const BASE = "https://api.pre.dev/api/v1/browser-agents";
const HDR = {
  "Authorization": `Bearer ${API_KEY}`,
  "Content-Type": "application/json",
};

async function runSync(tasks, concurrency = 5) {
  const res = await fetch(BASE, {
    method: "POST",
    headers: HDR,
    body: JSON.stringify({ tasks, concurrency }),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
  return res.json();
}

async function* runStream(tasks) {
  const res = await fetch(BASE, {
    method: "POST",
    headers: HDR,
    body: JSON.stringify({ tasks, stream: true }),
  });
  if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);

  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";
  let event = null;

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    let idx;
    while ((idx = buffer.indexOf("\n")) !== -1) {
      const line = buffer.slice(0, idx).trim();
      buffer = buffer.slice(idx + 1);
      if (!line || line.startsWith(":")) continue;
      if (line.startsWith("event: ")) event = line.slice(7).trim();
      else if (line.startsWith("data: ")) yield { event, data: JSON.parse(line.slice(6)) };
    }
  }
}

// sync
const result = await runSync([
  { url: "https://example.com", instruction: "Extract the heading." },
]);
console.log(result.results[0].data);

// stream
for await (const { event, data } of runStream([
  { url: "https://example.com", instruction: "Extract the heading." },
])) {
  if (event === "task_event") console.log(`[task ${data.taskIndex}]`, data.type);
  if (event === "task_result") console.log(`[task ${data.taskIndex}]`, data.status, data.data);
  if (event === "done") break;
}

TypeScript — fully typed client

type TaskStatus =
  | "SUCCESS" | "PENDING" | "ERROR" | "TIMEOUT"
  | "BLOCKED" | "CAPTCHA_FAILED" | "LOOP" | "NO_TARGET";

interface Task {
  url: string;
  instruction?: string;
  input?: Record<string, string>;
  output?: Record<string, unknown>;
  successCondition?: string;
  timeoutMs?: number;
}

interface TaskResult {
  url: string;
  instruction?: string;
  input?: Record<string, string>;
  status: TaskStatus;
  data?: unknown;
  creditsUsed: number;
  durationMs: number;
  error?: string;
}

interface BatchResult {
  id: string;
  total: number;
  completed: number;
  results: TaskResult[];
  totalCreditsUsed: number;
  status: "processing" | "completed" | "failed";
  createdAt?: string;
  completedAt?: string;
  error?: string;
}

interface BatchRequest {
  tasks: Task[];
  concurrency?: number;
  async?: boolean;
  stream?: boolean;
}

export class BrowserAgents {
  constructor(private apiKey: string, private base = "https://api.pre.dev/api/v1/browser-agents") {}

  private headers() {
    return {
      "Authorization": `Bearer ${this.apiKey}`,
      "Content-Type": "application/json",
    };
  }

  async run(req: BatchRequest): Promise<BatchResult> {
    const res = await fetch(this.base, { method: "POST", headers: this.headers(), body: JSON.stringify(req) });
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    return res.json();
  }
}

Error Handling

402 Insufficient credits

You need at least 1 credit per task upfront, even though only SUCCESS tasks are actually billed. Top up at pre.dev/billing.

429 Queue depth exceeded

Your account has more than 5000 in-flight tasks. Wait for some to drain, or lower concurrency.

503 SSE capacity exceeded

The serving pod is already at its SSE connection cap. Retry the request with "async": true and poll — the underlying work isn’t affected.

400 / invalid task shape

Every task needs a url. If you get 400, check you’re not sending tasks: { ... } (a single object) — it must always be an array, even for one task.

Next: Task Status

Fetch results for an async or historical task submission, with the full per-step event timeline.

Authorizations

Authorization
string
header
default:YOUR_API_KEY
required

API key for authentication. Get your API key from https://pre.dev/projects/playground (Solo) or https://pre.dev/enterprise/dashboard?page=api (Enterprise). Use format: Bearer YOUR_API_KEY

Body

application/json
tasks
object[]
required
Required array length: 1 - 1000 elements
concurrency
integer

How many tasks to run in parallel. Default: 5.

Required range: 1 <= x <= 20
async
boolean
default:false

If true, returns { id, status: 'processing' } immediately — poll GET /:id for progress.

stream
boolean
default:false

If true, returns an SSE stream with task_event, task_result, done, and error frames.

Response

Batch result (sync mode) or batch stub (async mode).

Run summary. The schema is named BatchResult for backwards compatibility with older clients.

id
string

Run id (24-char Mongo ObjectId).

total
integer

Total tasks in the run.

completed
integer

Number of tasks that have finished (any status).

results
object[]

Per-task results aligned by taskIndex. In-progress runs return PENDING stubs for tasks that haven't started; the stub has only url, instruction, input, and status: "PENDING".

totalCreditsUsed
number

Sum of credits billed across all tasks in the run.

status
enum<string>
Available options:
processing,
completed,
failed
createdAt
string<date-time>
completedAt
string<date-time>

Populated once status !== "processing".

liveEvents
object[][]

Only present when includeEvents=true on GET /:id. Per-task in-flight event streams for tasks that haven't yet finished. Aligned by index with results; completed tasks get an empty array.

error
string

Set only when status === "failed".