Skip to content

Recordings and the event timeline

Two ways to see what a hosted browser did after the fact. A recording is a video of the browser's screen. The event timeline is a list of what happened, with timestamps: pages, tabs, refused commands, agent steps, hand-offs and limits. Both work for sessions you drive and for agent runs.

Record a session

Pass record: true when you create the session or the run. Recording is off by default.

curl -X POST https://www.clearcotelabs.com/api/v1/browsers \
  -H "authorization: Bearer cc_live_..." -H "content-type: application/json" \
  -d '{"record": true, "country": "de"}'

# an agent run: the same field
curl -X POST https://www.clearcotelabs.com/api/v1/runs \
  -H "authorization: Bearer cc_live_..." -H "content-type: application/json" \
  -d '{"task": "Open the pricing page and read the cheapest plan", "url": "https://example.com", "record": true}'
  • The video shows the active tab, as the live view does, at a few frames per second, in an H.264 MP4 of 1280×800 (other window sizes are letterboxed). It is meant for seeing what happened, not for smooth playback.
  • Recording stops when the session ends, or when the file reaches a size cap; the session itself carries on.
  • It is not billed as traffic: the frames are captured on our server, not downloaded from the internet.
  • Only some servers can record. A session with record: true is only started on one that can; if none is free you get the usual 503.

Status

The session view (GET /api/v1/browsers/<id>, and the session of a run) has a recording field: null when the session was not recorded, otherwise:

json
"recording": {
  "status": "ready",              // recording | processing | ready | failed | expired
  "bytes": 18230411,
  "durationSec": 312.4,
  "expiresAt": "2026-10-16T09:20:11.000Z"   // when it stops being available
}

bytes, durationSec and expiresAt are null until the recording is ready.

statusMeaning
recordingThe session is running and being recorded.
processingThe session has ended; the file is being finished and uploaded.
readyDownload or share it.
failedThe recording could not be made or uploaded: the session never started, the server could not finish the file, or it had still not arrived 25 hours after the session ended. There is no file.
expiredPast its retention period: the file is deleted.

The file is uploaded once the session has ended, so a recording turns ready a little after the session does; a long one takes longer. If it has still not arrived 25 hours after the session ended, it is marked failed. To be told instead of polling, add a recording.ready webhook.

Download

GET /api/v1/browsers/<id>/recording answers with a 302 redirect to a private download link that works for at most 5 minutes. Follow it right away rather than storing it. While the recording is recording or processing the answer is 409 with code NOT_READY; a session that was not recorded, whose recording failed, or whose recording has expired is a 404 (NOT_FOUND).

bash
# -L follows the redirect to the file
curl -L -o session.mp4 -H "authorization: Bearer cc_live_..." https://www.clearcotelabs.com/api/v1/browsers/<id>/recording

# or with the SDK's command
clearcote cloud recording <id> -o session.mp4
python
from clearcote.cloud import Cloud

cloud = Cloud()
cloud.browsers.download_recording("bs_…", "session.mp4")   # saves the file
url = cloud.browsers.recording_url("bs_…")                 # or just the short-lived link

The dashboard plays the recording next to the session's details.

Share a replay

To show a recording to someone without an account, make a replay link. It opens a page on this site that plays the video, until the link expires:

bash
curl -X POST -H "authorization: Bearer cc_live_..." -H "content-type: application/json" \
  -d '{"recording": true, "minutes": 120}' https://www.clearcotelabs.com/api/v1/browsers/<id>/share
# { "url": "https://www.clearcotelabs.com/replay/bs_…#t=…", "expiresAt": "…" }

It is the same share call as for a live-view link, with recording: true; minutes sets how long the link works: 1 to 240, default 30, and never past the recording's expiresAt. A recording that is not ready yet answers 409 NOT_READY, and control does not apply (400). The token is after the #, so it never reaches a server log; pass the link on whole. The two kinds of link are kept apart: a replay link cannot open a live view, and a live link cannot open a replay. Anyone holding a replay link can watch the video, so treat it like the recording itself.

Retention and privacy

  • Recordings are stored privately for 14 days from when they turn ready. At recording.expiresAt the download and replay links stop working and the file is deleted soon after; download what you want to keep before then.
  • Download links are only given to you (your API key or the dashboard) and stop working after at most 5 minutes; until then, anyone holding one can use it.
  • A recording shows what the page showed. Anything typed into an ordinary field is on the video, including a secret typed into a field that displays its text. Password fields show dots, as on screen. Leave recording off for runs whose screens you do not want stored.

The event timeline

Every session keeps a timeline. Read it with GET /api/v1/browsers/<id>/events, which works for runs too (a run's id is its session id):

bash
curl -H "authorization: Bearer cc_live_..." "https://www.clearcotelabs.com/api/v1/browsers/<id>/events?after=0&limit=200"
json
{
  "events": [
    { "seq": 1, "at": "2026-10-02T09:14:05.410Z", "type": "session.started", "data": { "kind": "run" } },
    { "seq": 2, "at": "2026-10-02T09:14:05.912Z", "type": "run.started", "data": { "jet": { "version": "0.1.0", "commit": "cd7011c…" } } },
    { "seq": 3, "at": "2026-10-02T09:14:06.730Z", "type": "navigation", "data": { "url": "https://www.gov.uk/", "title": "Welcome to GOV.UK" } },
    { "seq": 4, "at": "2026-10-02T09:14:07.950Z", "type": "run.step",
      "data": { "step": 1, "kind": "click", "action": "Reject additional cookies", "text": null, "probability": 0.85, "url": "https://www.gov.uk/" } },
    …
    { "seq": 19, "at": "2026-10-02T09:14:21.006Z", "type": "session.ended", "data": { "reason": "run_finished" } }
  ],
  "next": null
}
  • seq numbers a session's events in order, from 1; there can be gaps. at is when it happened.
  • after returns the events after that seq (default 0, the start); limit is 1 to 500, default 200. Values outside those ranges are a 400.
  • next is the after for the following page, or null when you have everything stored so far.
  • While a session runs, its events arrive in batches with the browser's usage reports, about every 15 seconds. To follow a live session, keep asking with after set to the last seq you saw.
  • A session keeps at most 1000 events; one truncated event marks where events were dropped. Events are deleted 30 days after the session ends.
from clearcote.cloud import Cloud

cloud = Cloud()
after = 0
while True:
    page = cloud.browsers.events("bs_…", after=after)
    for e in page["events"]:
        print(e["seq"], e["at"], e["type"], e["data"])
    if page["next"] is None:
        break
    after = page["next"]

Event types

typedataMeaning
session.started{ kind }The browser is up. kind is "cdp" for a session you drive, "run" for an agent run.
navigation{ url, title }A tab moved to a new address. At most one per second per tab.
tab.opened{ url }A new tab or pop-up opened.
tab.closed{}A tab closed.
cdp.denied{ method, reason }The hosted browser refused a CDP command, for example a file upload from the server. At most 50 per session.
profile.loaded{ cookies, files }A saved profile was loaded into the browser.
profile.saved{ cookies, bytes }The profile was saved back at the end of the session.
run.started{ jet: { version, commit } }Jet started on a run, and which version it is.
run.step{ step, kind, action, text, probability, url }One step of a run, as in result.steps. Secrets in text show as {{name}}.
run.finished{ status, detail }Jet stopped, with the result's status (done, blocked, needs_input, budget, error).
handoff.requested{ reason }A run's agent asked for a person (handoff: true). A hand-off you start with POST …/handoff is not in the timeline; use the handoff.requested webhook for those.
handoff.done{}A run's hand-off was marked done (the "I'm done" button, the dashboard or the API) and the agent carried on.
recording.started{}Recording began.
limit{ reason }A limit was reached: max_duration, idle_timeout, max_bytes, or max_recording_size (only the recording stopped; the session carries on). Running out of balance shows only in session.ended.
session.ended{ reason }The session ended, and why (the same as the session's endReason).
truncated{} One marker at the first event that was not kept: the session reached 1000 stored eventsEvents were dropped from here on: the session passed 1000 events, or its server could not send them in time. At most one per session.

Event data is kept small: an event whose data is over 2 KB stores { "truncated": true } instead. Secrets never appear in events; where a step typed one, its text shows {{name}}. New event types may be added, so skip types you do not know rather than failing on them.