> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://platform.bctrl.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://platform.bctrl.ai/_mcp/server.

# Show Customers a Replay

> Embed a session recording and render the activity feed as a human-readable timeline.

After a [run](/sdk/runs) finishes, give your users proof of what happened: a video-style replay they can scrub, next to a timeline of what the automation did. Both come from the run you already have - nothing extra to capture.

```ts TypeScript
import { Bctrl } from "@bctrl/sdk";

const bctrl = new Bctrl({ apiKey: process.env.BCTRL_API_KEY! });

// ... run finished, runtime stopped ...

// Recordings are processed shortly after the run ends - poll until ready.
let recording = await bctrl.runs.recording(runId, { expiresInSeconds: 86_400 });
while (recording.status === "preparing" || recording.status === "processing") {
  await new Promise((resolve) => setTimeout(resolve, 2_000));
  recording = await bctrl.runs.recording(runId, { expiresInSeconds: 86_400 });
}

console.log(recording.url);        // embed to replay
console.log(recording.durationMs); // session length

// The activity feed is the human-readable timeline of the same run.
for await (const item of bctrl.runs.activity.iter(runId)) {
  console.log(item.time, item.type, item.message);
}
```

```python Python
import time

from bctrl import Bctrl

bctrl = Bctrl()

# ... run finished, runtime stopped ...

# Recordings are processed shortly after the run ends - poll until ready.
recording = bctrl.runs.recording(run_id, expires_in_seconds=86_400)
while recording["status"] in ("preparing", "processing"):
    time.sleep(2)
    recording = bctrl.runs.recording(run_id, expires_in_seconds=86_400)

print(recording["url"])         # embed to replay
print(recording["durationMs"])  # session length

# The activity feed is the human-readable timeline of the same run.
for item in bctrl.runs.activity.list(run_id)["data"]:
    print(item["time"], item["type"], item["message"])
```

Embed the replay exactly like a [live view](/cookbook/embed-live-view):

```html
<iframe src="https://...recording url..." width="1280" height="800" style="border: 0"></iframe>
```

> 📸 **Content TODO:** screenshot of the finished layout - replay iframe on the left, activity timeline rendered as a list on the right. If you have the redesigned activity UI, a capture of a real run (navigation → typing → captcha solve → download) sells it best.

## Rendering the timeline

Activity items are already aggregated for humans - a burst of keystrokes arrives as one evolving `browser.type` row, not five hundred key events. Each item has `time`, `type`, `message`, optional `status`, `severity`, and `durationMs`, and parent/child structure via `children` for grouped steps. Render `message` as the line, `durationMs` as the badge, and you have a customer-grade timeline without writing an aggregator.

If you need the raw record instead - every request, console line, and navigation - use [events](/sdk/events) rather than activity.

## Expiry

Like live view, the recording URL is a time-limited lease (`expiresInSeconds`, up to 24 hours). Mint a fresh one on each page load rather than storing it.

## Next

* [Recording](/sdk/recording) - the recording surface
* [Events & Activity](/sdk/events) - timeline structure and filters
* [Run Files](/sdk/run-files) - ship the run's artifacts alongside the replay