Skip to main content

Browser Agents

Run browser tasks

Submit a task array and receive JSON, an async run ID, or a live SSE stream.

posthttps://api.pre.dev/browser-agentTry it

Always pass a tasks array, including for a single task. Each request creates one run, called a batch in response types. Send your key as described in authentication.

Task controls

Every task needs a full HTTP(S) url and either a nonblank instruction or a non-empty output JSON Schema object. String-valued input is optional and does not replace instructions. Choose mode: "extract" for schema-oriented reads or mode: "agent" for goal-driven navigation; auto lets the service choose.

maxSteps and maxDurationSeconds set per-task budgets. For compatibility with MCP, use 1–50 steps and 5–600 seconds. REST also accepts the older maxIterations and timeoutMs names; those take precedence if both forms are sent. Do not send both forms in new code. Defaults vary with the task and execution path, so set an explicit budget when your workflow depends on one.

{
  "tasks": [{
    "url": "https://example.com",
    "instruction": "Extract the main page heading.",
    "output": {
      "type": "object",
      "properties": { "heading": { "type": "string" } },
      "required": ["heading"]
    },
    "mode": "extract",
    "maxSteps": 10,
    "maxDurationSeconds": 120
  }],
  "concurrency": 5,
  "async": true
}

When output is omitted, schema inference may be attempted. Supply your own schema for a predictable contract; a successful task does not always have data.

Input validation

The service validates the entire batch before charging credits, creating a run, or queueing browser work. Malformed URLs, missing instructions without an output schema, and domains with no DNS address return an actionable error with a code, a zero-based taskIndex, and a field when applicable.

DNS verification has a five-second request budget. Temporary DNS failures return 503 with URL_CHECK_UNAVAILABLE; retry the submission later. No tasks are started when verification fails. This check verifies the hostname, not page availability: authenticated pages and bot-protected sites remain eligible for browser execution.

The dashboard applies the same task and URL format checks before enabling Run. CSV instructions are checked after row-variable interpolation. Output-schema-only extraction remains supported through the API and MCP.

Synchronous

With neither async nor stream enabled, the connection waits for a BatchResult. Use this for short tasks. The quickstart contains complete curl and SDK examples.

Async

With async: true, HTTP 200 returns an initial result such as:

{
  "id": "507f1f77bcf86cd799439011",
  "total": 1,
  "completed": 0,
  "results": [],
  "totalCreditsUsed": 0,
  "status": "processing"
}

Save id, then poll the run or attach an SSE stream. totalCreditsUsed: 0 on this initial response does not mean no credit floor was charged at submission.

Streaming

With stream: true, the response is text/event-stream. If both stream and async are true, streaming takes precedence; the server does not reject that combination. Send only your intended mode for clarity.

curl --fail-with-body -N https://api.pre.dev/browser-agent \
  -H "Authorization: Bearer $PREDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tasks": [{"url":"https://example.com","instruction":"Read the main heading."}],
    "stream": true
  }'
SSE eventdata payload
task_event{ taskIndex, type, timestamp, iteration?, data }
task_result{ taskIndex, ...taskResult }
doneFull BatchResult
error{ error, code?, actionUrl?, taskIndex?, field? }

Lines beginning with : are keepalive comments. The task index is zero-based. Individual task done events inside task_event do not finish the batch; wait for the outer SSE done event and inspect all task statuses.

A submission stream does not provide the initial snapshot event or reliably expose a run ID before completion. Use async submission followed by an existing-run stream when you need a saved ID for recovery. Disconnecting does not cancel the work.

Billing and limits

Every browser task is charged a 0.1-credit floor when you submit it, so you need available credits to start. When the task finishes, it is charged any usage above the floor, whether it succeeded or not. Only an ERROR or EXHAUSTED task keeps the floor and nothing more.

The floor is refunded only when a submission fails before its tasks are queued. Each task’s creditsUsed is its full charge, including the floor, and totalCreditsUsed is the sum for the run. Treat both fields as recorded charges, not a forecast or a reserved amount. Free accounts have a limited browser trial budget in addition to the account credit balance; an exhausted trial returns SUBSCRIPTION_REQUIRED. See plans, credits, and pricing for what a credit is worth.

LimitValue
Browser tasks per request1,000
Browser tasks running at once within a run (concurrency)1 to 20, default 5
Browser tasks in flight per account, queued or runningSet by your plan; read cap from GET /browser-agent-status
AI Gateway requests per workspace600 a minute; 30 a minute on the Free plan. Catalog reads do not count.

Your account’s in-flight cap can be much smaller than the per-request limit, and it counts every submitted task, including queued work. concurrency controls parallelism within the run; it does not let you exceed your account limit.

Use queue status to size batches and errors to handle QUEUE_FULL, RATE_LIMITED, and billing responses. Capacity snapshots are advisory; another submission can consume capacity before yours arrives.

Idempotency

Send an Idempotency-Key header, or idempotencyKey in the JSON body, to recover an existing run from the last 24 hours. The header takes precedence. Use a unique key per logical submission and keep the payload stable across retries.

A matching existing run returns HTTP 200 as JSON, including when stream: true was requested. Check the response content type. The current SDK methods and MCP tool do not expose this option; use direct REST when needed. See retry semantics.

Response schemas

The generated schema below describes the run and task fields. Get run status explains pending entries, task outcomes, retry counts, and timelines. The completed count alone is not a reliable completion signal; use batch status.

Errors

StatusWhen
400INVALID_TASK, INVALID_URL, MISSING_INSTRUCTION, URL_UNREACHABLE, or BATCH_TOO_LARGE. Nothing was started or charged
401Missing or invalid key
402SUBSCRIPTION_REQUIRED or INSUFFICIENT_CREDITS, with an actionUrl
429RATE_LIMITED or QUEUE_FULL
500Unexpected error. Retrieve the run before resubmitting
503URL_CHECK_UNAVAILABLE (retry later), or streaming capacity is full (submit with async: true and poll)

Each status has an example body in the response examples. See errors and retries.

curl --fail-with-body https://api.pre.dev/browser-agent \  -H "Authorization: Bearer $PREDEV_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "tasks": [    {      "url": "https://example.com",      "instruction": "Extract the main page heading.",      "output": {        "type": "object",        "properties": {          "heading": {            "type": "string"          }        },        "required": [          "heading"        ]      },      "mode": "extract",      "maxSteps": 10,      "maxDurationSeconds": 60    }  ],  "async": true}'
const body = JSON.stringify({  "tasks": [    {      "url": "https://example.com",      "instruction": "Extract the main page heading.",      "output": {        "type": "object",        "properties": {          "heading": {            "type": "string"          }        },        "required": [          "heading"        ]      },      "mode": "extract",      "maxSteps": 10,      "maxDurationSeconds": 60    }  ],  "async": true})fetch("https://api.pre.dev/browser-agent", {  method: "POST",  headers: {    "Content-Type": "application/json"  },  body})
package mainimport (  "fmt"  "net/http"  "io/ioutil"  "strings")func main() {  url := "https://api.pre.dev/browser-agent"  body := strings.NewReader(`{    "tasks": [      {        "url": "https://example.com",        "instruction": "Extract the main page heading.",        "output": {          "type": "object",          "properties": {            "heading": {              "type": "string"            }          },          "required": [            "heading"          ]        },        "mode": "extract",        "maxSteps": 10,        "maxDurationSeconds": 60      }    ],    "async": true  }`)  req, _ := http.NewRequest("POST", url, body)  req.Header.Add("Content-Type", "application/json")  res, _ := http.DefaultClient.Do(req)  defer res.Body.Close()  body, _ := ioutil.ReadAll(res.Body)  fmt.Println(res)  fmt.Println(string(body))}
import requestsurl = "https://api.pre.dev/browser-agent"body = """{  "tasks": [    {      "url": "https://example.com",      "instruction": "Extract the main page heading.",      "output": {        "type": "object",        "properties": {          "heading": {            "type": "string"          }        },        "required": [          "heading"        ]      },      "mode": "extract",      "maxSteps": 10,      "maxDurationSeconds": 60    }  ],  "async": true}"""response = requests.request("POST", url, data = body, headers = {  "Content-Type": "application/json"})print(response.text)
import java.net.URI;import java.net.http.HttpClient;import java.net.http.HttpRequest;import java.net.http.HttpResponse;import java.net.http.HttpResponse.BodyHandlers;import java.time.Duration;import java.net.http.HttpRequest.BodyPublishers;var body = BodyPublishers.ofString("""{  "tasks": [    {      "url": "https://example.com",      "instruction": "Extract the main page heading.",      "output": {        "type": "object",        "properties": {          "heading": {            "type": "string"          }        },        "required": [          "heading"        ]      },      "mode": "extract",      "maxSteps": 10,      "maxDurationSeconds": 60    }  ],  "async": true}""");HttpClient client = HttpClient.newBuilder()  .connectTimeout(Duration.ofSeconds(10))  .build();HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()  .uri(URI.create("https://api.pre.dev/browser-agent"))  .header("Content-Type", "application/json")  .POST(body)  .build();try {  HttpResponse<String> response = client.send(requestBuilder.build(), BodyHandlers.ofString());  System.out.println("Status code: " + response.statusCode());  System.out.println("Response body: " + response.body());} catch (Exception e) {  e.printStackTrace();}
using System;using System.Net.Http;using System.Text;var body = new StringContent("""{  "tasks": [    {      "url": "https://example.com",      "instruction": "Extract the main page heading.",      "output": {        "type": "object",        "properties": {          "heading": {            "type": "string"          }        },        "required": [          "heading"        ]      },      "mode": "extract",      "maxSteps": 10,      "maxDurationSeconds": 60    }  ],  "async": true}""", Encoding.UTF8, "application/json");var client = new HttpClient();var response = await client.PostAsync("https://api.pre.dev/browser-agent", body);var responseBody = await response.Content.ReadAsStringAsync();
use reqwest::{Result, Client};async fn main() -> Result<()> {  let client = Client::new();  let url = "https://api.pre.dev/browser-agent";  let body = r#"{    "tasks": [      {        "url": "https://example.com",        "instruction": "Extract the main page heading.",        "output": {          "type": "object",          "properties": {            "heading": {              "type": "string"            }          },          "required": [            "heading"          ]        },        "mode": "extract",        "maxSteps": 10,        "maxDurationSeconds": 60      }    ],    "async": true  }"#;  let res = client    .post(url)    .header("Content-Type", "application/json")    .body(body)    .send()    .await?    .text()    .await?;  println!("{}", res);  Ok(())}

{  "id": "507f1f77bcf86cd799439011",  "total": 1,  "completed": 0,  "results": [],  "totalCreditsUsed": 0,  "status": "processing"}

POST
/browser-agent

Authorization

AuthorizationBearer <token>

Your pre.dev workspace API key (pdk_...) from Integrations, Built-in tab: https://pre.dev/projects/integrations?tab=built-in

In: header

Header Parameters

Idempotency-Key?string

Optional user-scoped 24-hour retry lookup. Reuse only for the same logical submission; concurrent first requests can still duplicate work.

Request Body

application/json

TypeScript Definitions

Use the request body type in TypeScript.

Response Body

Batch result, asynchronous ID, or SSE stream. Inspect Content-Type before parsing. A done batch can contain failed tasks; inspect each task status.

TypeScript Definitions

Use the response body type in TypeScript.

application/json

application/json

application/json

application/json

application/json

application/json

Was this page helpful?