Lovely RouterDocs
Get a key
Browser agent

Live events & screen

Stream what the agent is doing, each step as it finishes and the browser's screen while a run works, instead of waiting for the result.

A run takes seconds to minutes. Instead of waiting for the end, stream it: you see what the agent is doing right now, each step as it finishes, the run's state whenever it changes and, if you ask, the browser's screen. The stream ends with the finished run, answer, data and charge_usd included.

Streams use Server-Sent Events (text/event-stream), the format LLM APIs use for streaming. Watching a run is free.

Stream a new run#

Add "stream": true when you create a run. The response is the event stream, with status 200, instead of the 202 JSON. Add "screen": true to get the browser's screen too.

curl -N https://lovelyrouter.com/v1/browser/runs \
  -H "Authorization: Bearer $LOVELY_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "goal": "Find the price of the book \"A Light in the Attic\" and whether it is in stock.",
    "start_url": "https://books.toscrape.com/",
    "stream": true
  }'

The stream looks like this (abridged):

SSE
event: run
data: {"id":"brun_3f9a1c7e0b2d4e6f8a9b0c1d","object":"browser_run","status":"running","steps":0,"trace":[],"poll":"/v1/browser/runs/brun_3f9a1c7e0b2d4e6f8a9b0c1d?wait=30","events":"/v1/browser/runs/brun_3f9a1c7e0b2d4e6f8a9b0c1d/events","charge_usd":null,"reserved_usd":0.445,…}

event: activity
data: {"step":0,"kind":"starting","text":"Opening https://books.toscrape.com/"}

event: activity
data: {"step":1,"kind":"reading","text":"Reading the page"}

event: activity
data: {"step":1,"kind":"acting","text":"click link \"A Light in the ...\" → https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"}

event: step
data: {"step":1,"url":"https://books.toscrape.com/","action":"click:e54","description":"click link \"A Light in the ...\" → https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html","ms":2140,"error":null}

event: run
data: {"id":"brun_3f9a1c7e0b2d4e6f8a9b0c1d","status":"running","steps":1,…}

event: activity
data: {"step":2,"kind":"answering","text":"Reading the result and writing the answer"}

event: step
data: {"step":2,"url":"https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html","action":"done","description":"Read the result and wrote the answer","ms":2875,"error":null}

event: run
data: {"id":"brun_3f9a1c7e0b2d4e6f8a9b0c1d","status":"succeeded","answer":"\"A Light in the Attic\" costs £51.77 and is in stock (22 available).","charge_usd":0.012,"reserved_usd":0,…}

event: end
data: {"id":"brun_3f9a1c7e0b2d4e6f8a9b0c1d"}

The first run event carries what the 202 response would: poll, events, and webhook_secret when you passed a webhook. Errors before the run starts (400, 402, 429 and so on) come back as JSON with their status, the same as without stream.

Follow a run that is already going#

Open GET /v1/browser/runs/{id}/events at any time: from another process, after a dropped connection, or for a run you started without stream. Add ?screen=1 for the screen. Several streams can watch the same run.

cURL
curl -N "https://lovelyrouter.com/v1/browser/runs/brun_3f9a1c7e0b2d4e6f8a9b0c1d/events?screen=1" \
  -H "Authorization: Bearer $LOVELY_API_KEY"

Every stream starts with a run event that has the full trace, so reconnecting loses nothing. A finished run sends that run event (and its last screen, with screen) followed by end.

Events#

EventWhendata
runFirst, after every step, when a pause begins or ends, and lastThe run object with charge_usd and reserved_usd. The first one has trace.
activityThe agent starts somethingstep (the step in progress), kind and text
stepA step finishedOne trace entry: step, url, action, description, ms, error
frameWith screen: on connect, then whenever the page changesdata, a base64 JPEG of the browser, and at (ISO 8601)
endThe run is over; the stream closes nextid

Activity kinds:

kindThe agent is
startingstarting the browser or opening start_url
readingreading the page
waitingwaiting for a page that has not loaded, or loading it again
decidingchoosing the next action
thinkinglooking more closely before it decides (accurate mode may use a screenshot)
checkingchecking a form's values before it submits
actingdoing the action text describes: a click, typing, a choice, an upload
answeringreading the result and writing the answer
extractingturning what it read into data

Lines that start with : are keep-alive comments, sent every 15 seconds while nothing else happens. Ignore them.

Watch the screen#

With "screen": true (or ?screen=1), frame events carry the browser's screen as base64 JPEG, usually 20 to 60 KB each. A frame arrives when you connect and whenever the page changes, at most about 4 a second; a page that holds still sends nothing new. If your reader falls behind, frames are skipped, never other events.

To show it on a web page, put the frame in an image:

JavaScript
img.src = `data:image/jpeg;base64,${frame.data}`;

For a single picture without a stream, GET /v1/browser/runs/{id}/screenshot returns the screen as it is now.

Pauses for a person#

The stream stays open while a run is waiting_for_human. The run event that starts the pause has needs; answer it with POST /v1/browser/runs/{id}/reply from anywhere, and the stream carries on with a run event back at running. With screen, you also see what a person does in the browser during a takeover. See Human in the loop.

Streams in a web app#

Keep your API key on your server. Browsers' EventSource can't send an Authorization header, so open the stream from your server and pass it through to your page unchanged, with content-type: text/event-stream. Your page can then use EventSource on your own URL.

Reconnect#

A stream can drop: a network blip, a proxy timeout, a deploy on our side. The run keeps working. Open GET /v1/browser/runs/{id}/events again and replace your state with the first run event. EventSource reconnects by itself, after 3 seconds. Stop when you have seen end or a run with a final status.