Browser Agents
Stream an existing run
Attach to an existing browser run with an initial snapshot, live SSE updates, and terminal status.
Submit with async: true, save the run ID, then attach to this endpoint. This lets your application recover results even if the stream disconnects.
curl --fail-with-body -N "https://api.pre.dev/browser-agent/$BATCH_ID/stream" \
-H "Authorization: Bearer $PREDEV_API_KEY"Event contract
| Event | Payload | When |
|---|---|---|
snapshot | Full run result with available timelines | On connection |
batch_meta | { name, taskNames } | Display names become available |
task_event | { taskIndex, type, timestamp, iteration?, data } | A task emits an event |
task_result | { taskIndex, ...taskResult } | A task finishes |
done | { status: "completed" } or { status: "failed" } | The batch reaches a terminal state |
error | { error } | The stream cannot continue |
The existing-run stream’s done contains status only. Fetch the run after done for final data and credit usage. This differs from the submission stream, whose done contains the full batch.
Reconnect and recover
An already-terminal run emits snapshot, then done, then closes. Keepalive lines start with : and are not JSON events. Closing the stream does not cancel the run.
The stream has no event-ID replay contract. After a disconnect, fetch a fresh result or reconnect for a new snapshot; do not assume that every transient event will be replayed. Treat EOF without done as an interrupted stream and check status.
Read the stream in code
Send the key in a header, as on every other endpoint, and read the response body as it arrives. The browser’s EventSource cannot set headers, so use fetch or an HTTP client that streams.
const response = await fetch(`https://api.pre.dev/browser-agent/${batchId}/stream`, {
headers: { Authorization: `Bearer ${process.env.PREDEV_API_KEY}` },
});
if (!response.ok || !response.body) throw new Error(`Stream failed: HTTP ${response.status}`);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
let finished = false;
while (!finished) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
let boundary;
while (!finished && (boundary = buffer.indexOf('\n\n')) !== -1) {
const frame = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
const event = frame.match(/^event: (.+)$/m)?.[1];
const data = frame.match(/^data: (.*)$/m)?.[1];
if (!event || data === undefined) continue; // keepalive comment
if (event === 'error') throw new Error(JSON.parse(data).error);
if (event === 'done') finished = true;
console.log(event, JSON.parse(data));
}
}
// After done (or an interrupted stream), fetch GET /browser-agent/{id} for the final result.import json
import os
import requests
with requests.get(
f"https://api.pre.dev/browser-agent/{batch_id}/stream",
headers={"Authorization": f"Bearer {os.environ['PREDEV_API_KEY']}"},
stream=True,
timeout=(10, 120),
) as response:
response.raise_for_status()
event = None
for line in response.iter_lines(decode_unicode=True):
if line.startswith("event: "):
event = line[len("event: "):]
elif line.startswith("data: ") and event:
data = json.loads(line[len("data: "):])
if event == "error":
raise RuntimeError(data.get("error"))
print(event, data)
if event == "done":
break
# After done (or an interrupted stream), fetch GET /browser-agent/{id} for the final result.The SDKs stream submission responses; use direct HTTP for this existing-run endpoint.
Errors
These statuses arrive before streaming begins. Once headers are sent, handle SSE error frames as failures even with HTTP 200.
| Status | When |
|---|---|
400 | id is not a valid run ID |
401 | Missing or invalid key |
404 | No run with this ID in your account |
503 | Streaming capacity is full; poll GET /browser-agent/{id} instead |
Each status has an example body in the response examples. See errors and retries.
Your pre.dev workspace API key (pdk_...) from Integrations, Built-in tab: https://pre.dev/projects/integrations?tab=built-in
In: header
Path Parameters
24-character record ID.
^[a-fA-F0-9]{24}$Response Body
text/event-stream
SSE stream. Errors after headers arrive as error events, not new HTTP status codes.
TypeScript Definitions
Use the response body type in TypeScript.
application/json
Invalid request.
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
Record unavailable.
TypeScript Definitions
Use the response body type in TypeScript.
application/json
Streaming capacity unavailable; retry later or use polling.
TypeScript Definitions
Use the response body type in TypeScript.
curl -X GET "https://api.pre.dev/browser-agent/string/stream"fetch("https://api.pre.dev/browser-agent/string/stream", { method: "GET"})package mainimport ( "fmt" "net/http" "io/ioutil")func main() { url := "https://api.pre.dev/browser-agent/string/stream" req, _ := http.NewRequest("GET", url, nil) 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/string/stream"response = requests.request("GET", url)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;HttpClient client = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build();HttpRequest.Builder requestBuilder = HttpRequest.newBuilder() .uri(URI.create("https://api.pre.dev/browser-agent/string/stream")) .GET() .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 client = new HttpClient();var response = await client.GetAsync("https://api.pre.dev/browser-agent/string/stream");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/string/stream"; let res = client .get(url) .send() .await? .text() .await?; println!("{}", res); Ok(())}"event: snapshot\ndata: {\"id\":\"64b7f3e1c2a9d5e6f8a01234\",\"status\":\"processing\",\"total\":1,\"completed\":1,\"results\":[{\"status\":\"PENDING\"}]}\n\nevent: batch_meta\ndata: {\"name\":\"Example lookup\",\"taskNames\":[\"Read title\"]}\n\nevent: task_result\ndata: {\"taskIndex\":0,\"status\":\"SUCCESS\",\"data\":{\"title\":\"Example Domain\"},\"creditsUsed\":0.1}\n\nevent: done\ndata: {\"status\":\"completed\"}\n\n"
