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_body{
"id": "507f1f77bcf86cd799439011",
"total": 1,
"completed": 0,
"results": [],
"totalCreditsUsed": 0,
"status": "processing"
}Run 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_body{
"id": "507f1f77bcf86cd799439011",
"total": 1,
"completed": 0,
"results": [],
"totalCreditsUsed": 0,
"status": "processing"
}tasks array, including for a single task. Each request creates one run, called a batch in response types. Authentication uses Authorization: Bearer $PREDEV_API_KEY or x-api-key.
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
- The default request ceiling is 1,000 tasks. Your account’s in-flight cap can be much smaller and counts every submitted task, including queued work.
concurrencydefaults to 5 and is clamped to 1–20. It controls parallelism within the run; it does not let you exceed your account limit.- Submission checks the credit floor of 0.1 credits per task. Successful tasks settle at at least that floor, with usage increasing for more complex work. Failed tasks settle at zero and their prepaid floor is refunded.
- Browser billing uses $0.10 per credit. Treat the returned
creditsUsedandtotalCreditsUsedas the recorded task 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.
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.Authorizations
Use a pre.dev API key from https://pre.dev/projects/key.
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?

