Skip to main content
pre.dev

Browser Agents

Stream an existing run

Attach to an existing browser run with an initial snapshot, live SSE updates, and terminal status.

gethttps://api.pre.dev/browser-agent/{id}/streamTry it

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

EventPayloadWhen
snapshotFull run result with available timelinesOn 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.

StatusWhen
400id is not a valid run ID
401Missing or invalid key
404No run with this ID in your account
503Streaming capacity is full; poll GET /browser-agent/{id} instead

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

GET
/browser-agent/{id}/stream

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

Path Parameters

id*string

24-character record ID.

Match^[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.

response?string

application/json

application/json

application/json

application/json

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"
Was this page helpful?