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: trueis only started on one that can; if none is free you get the usual503.
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:
"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.
| status | Meaning |
|---|---|
recording | The session is running and being recorded. |
processing | The session has ended; the file is being finished and uploaded. |
ready | Download or share it. |
failed | The 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. |
expired | Past 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).
# -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.mp4from 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 linkThe 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:
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. Atrecording.expiresAtthe 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):
curl -H "authorization: Bearer cc_live_..." "https://www.clearcotelabs.com/api/v1/browsers/<id>/events?after=0&limit=200"{
"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
}seqnumbers a session's events in order, from 1; there can be gaps.atis when it happened.afterreturns the events after thatseq(default 0, the start);limitis 1 to 500, default 200. Values outside those ranges are a400.nextis theafterfor the following page, ornullwhen 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
afterset to the lastseqyou saw. - A session keeps at most 1000 events; one
truncatedevent 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
| type | data | Meaning |
|---|---|---|
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 events | Events 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.