Browser Agents
Run browser tasks
Submit a task array and receive JSON, an async run ID, or a live SSE stream.
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 event | data payload |
|---|---|
task_event | { taskIndex, type, timestamp, iteration?, data } |
task_result | { taskIndex, ...taskResult } |
done | Full 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.
| Limit | Value |
|---|---|
| Browser tasks per request | 1,000 |
Browser tasks running at once within a run (concurrency) | 1 to 20, default 5 |
| Browser tasks in flight per account, queued or running | Set by your plan; read cap from GET /browser-agent-status |
| AI Gateway requests per workspace | 600 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
| Status | When |
|---|---|
400 | INVALID_TASK, INVALID_URL, MISSING_INSTRUCTION, URL_UNREACHABLE, or BATCH_TOO_LARGE. Nothing was started or charged |
401 | Missing or invalid key |
402 | SUBSCRIPTION_REQUIRED or INSUFFICIENT_CREDITS, with an actionUrl |
429 | RATE_LIMITED or QUEUE_FULL |
500 | Unexpected error. Retrieve the run before resubmitting |
503 | URL_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"}{ "id": "507f1f77bcf86cd799439011", "total": 1, "completed": 1, "results": [ { "url": "https://example.com", "status": "SUCCESS", "data": { "heading": "Example Domain" }, "creditsUsed": 0.1 } ], "totalCreditsUsed": 0.1, "status": "completed"}Your pre.dev workspace API key (pdk_...) from Integrations, Built-in tab: https://pre.dev/projects/integrations?tab=built-in
In: header
Header Parameters
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
Invalid task input, malformed or unresolvable URL, missing instructions/output schema, or oversized batch. No work is started.
TypeScript Definitions
Use the response body type in TypeScript.
application/json
Missing, invalid, or ineligible authentication.
TypeScript Definitions
Use the response body type in TypeScript.
application/json
Insufficient balance or a billing gate.
TypeScript Definitions
Use the response body type in TypeScript.
application/json
Rate or inflight limit reached.
TypeScript Definitions
Use the response body type in TypeScript.
application/json
Request failed.
TypeScript Definitions
Use the response body type in TypeScript.
application/json
Streaming capacity unavailable, or temporary DNS verification failure (URL_CHECK_UNAVAILABLE). No tasks start when input verification fails.
TypeScript Definitions
Use the response body type in TypeScript.