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
}'import requests
url = "https://api.pre.dev/browser-agent"
payload = { "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
}
] }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
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
}
]
})
};
fetch('https://api.pre.dev/browser-agent', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.pre.dev/browser-agent",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'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
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.pre.dev/browser-agent"
payload := strings.NewReader("{\n \"tasks\": [\n {\n \"url\": \"https://example.com\",\n \"instruction\": \"Extract the main page heading.\",\n \"output\": {\n \"type\": \"object\",\n \"properties\": {\n \"heading\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"heading\"\n ]\n },\n \"mode\": \"extract\",\n \"maxSteps\": 10,\n \"maxDurationSeconds\": 60\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.pre.dev/browser-agent")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"tasks\": [\n {\n \"url\": \"https://example.com\",\n \"instruction\": \"Extract the main page heading.\",\n \"output\": {\n \"type\": \"object\",\n \"properties\": {\n \"heading\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"heading\"\n ]\n },\n \"mode\": \"extract\",\n \"maxSteps\": 10,\n \"maxDurationSeconds\": 60\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pre.dev/browser-agent")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"tasks\": [\n {\n \"url\": \"https://example.com\",\n \"instruction\": \"Extract the main page heading.\",\n \"output\": {\n \"type\": \"object\",\n \"properties\": {\n \"heading\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"heading\"\n ]\n },\n \"mode\": \"extract\",\n \"maxSteps\": 10,\n \"maxDurationSeconds\": 60\n }\n ]\n}"
response = http.request(request)
puts response.read_bodyRun browser tasks
Submit a task array and receive JSON, an async run ID, or a live SSE stream.
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
}'import requests
url = "https://api.pre.dev/browser-agent"
payload = { "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
}
] }
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
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
}
]
})
};
fetch('https://api.pre.dev/browser-agent', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.pre.dev/browser-agent",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'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
]
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.pre.dev/browser-agent"
payload := strings.NewReader("{\n \"tasks\": [\n {\n \"url\": \"https://example.com\",\n \"instruction\": \"Extract the main page heading.\",\n \"output\": {\n \"type\": \"object\",\n \"properties\": {\n \"heading\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"heading\"\n ]\n },\n \"mode\": \"extract\",\n \"maxSteps\": 10,\n \"maxDurationSeconds\": 60\n }\n ]\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.pre.dev/browser-agent")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"tasks\": [\n {\n \"url\": \"https://example.com\",\n \"instruction\": \"Extract the main page heading.\",\n \"output\": {\n \"type\": \"object\",\n \"properties\": {\n \"heading\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"heading\"\n ]\n },\n \"mode\": \"extract\",\n \"maxSteps\": 10,\n \"maxDurationSeconds\": 60\n }\n ]\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.pre.dev/browser-agent")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"tasks\": [\n {\n \"url\": \"https://example.com\",\n \"instruction\": \"Extract the main page heading.\",\n \"output\": {\n \"type\": \"object\",\n \"properties\": {\n \"heading\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"heading\"\n ]\n },\n \"mode\": \"extract\",\n \"maxSteps\": 10,\n \"maxDurationSeconds\": 60\n }\n ]\n}"
response = http.request(request)
puts response.read_bodytasks 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
}
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 acode, 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 neitherasync nor stream enabled, the connection waits for a BatchResult. Use this for short tasks. The quickstart contains complete curl and SDK examples.
Async
Withasync: true, HTTP 200 returns an initial result such as:
{
"id": "507f1f77bcf86cd799439011",
"total": 1,
"completed": 0,
"results": [],
"totalCreditsUsed": 0,
"status": "processing"
}
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
Withstream: 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? } |
: 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 anERROR 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. |
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 anIdempotency-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. Thecompleted 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) |
Authorizations
Your pre.dev workspace API key (pdk_...) from Integrations, Built-in tab: https://pre.dev/projects/integrations?tab=built-in
Headers
Optional user-scoped 24-hour retry lookup. Reuse only for the same logical submission; concurrent first requests can still duplicate work.
Body
One or more tasks. Default server limit is 1,000, configurable by deployment; BATCH_TOO_LARGE reports the active limit.
1A full HTTP(S) URL and either nonblank instructions or a non-empty output schema are required. The entire batch is validated before billing or queueing.
- Option 1
- Option 2
Show child attributes
Show child attributes
Requested parallelism; defaults to 5 and is clamped to 1–20. Account and service capacity may reduce effective parallelism.
1 <= x <= 20Return the batch ID immediately, then poll or attach to its stream.
Return SSE; takes precedence over async.
Optional retry lookup key. Idempotency-Key header takes precedence. User-scoped lookup lasts 24 hours; concurrent first requests are not atomically deduplicated.
Response
Batch result, asynchronous ID, or SSE stream. Inspect Content-Type before parsing. A done batch can contain failed tasks; inspect each task status.
24-character record ID.
^[a-fA-F0-9]{24}$Ordered by task index. Slots can be null or partial until a result arrives.
Show child attributes
Show child attributes
processing, completed, failed Counts result slots, including pending/running placeholders or nulls. Do not use this as the completion signal.
Per-task events when includeEvents=true and live events are available.
Show child attributes
Show child attributes
Was this page helpful?

