Burrowbox Blog
← All posts

Embed your AI agent's live view in your app so customers can watch it work

· The Burrowbox team · 7 min read

#How to embed an AI agent's live view

To embed an AI agent's live view in your app, create a signed, short-lived link on your server with POST /api/machines/{id}/live-view, put the returned URL in an <iframe>, and lock it to your own origin with allowedOrigins. The link is watch-only by default, and input is blocked on the machine itself. You can crop the stream to a single element or a rectangle, and switch to interactive: true when a person needs to take over.

Below, we build it step by step in Node, from the server route to a takeover flow. The reference for every field is Embed the live view.

#Why let customers watch

If your product runs an agent on your customers' accounts, they'll want to see what it's doing before they trust it with more. A screenshot afterwards helps, but a live picture of the agent's browser lets them catch a wrong page while it's happening, and step in when the agent gets stuck on a login or a judgment call.

The examples below assume each customer has their own machine, as in machines for your customers and our post on giving every user a cloud computer.

The link is created with your API key, so it has to happen on your server. Your tmk_ key must never reach the browser (Authentication).

// server.js (Node 18+, Express)
import express from "express";
const app = express();

app.get("/api/agent-view", async (req, res) => {
  const customer = await currentCustomer(req); // your auth
  const r = await fetch(`https://burrowbox.dev/api/machines/${customer.machineId}/live-view`, {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.BURROWBOX_KEY}`, "Content-Type": "application/json" },
    body: JSON.stringify({
      mode: "browser",          // or "desktop", or "app" for one window
      interactive: false,       // watch only
      ttlSeconds: 900,          // 15 minutes
      allowedOrigins: ["https://app.example.com"],
    }),
  });
  const { url, expiresAt } = await r.json();
  res.json({ url, expiresAt });
});

What the fields do:

  • mode: desktop streams the whole screen, browser streams the machine's browser, and app streams one window, cropped. With app, pass app (part of the window's class or title, such as gimp), or it defaults to the focused window.
  • interactive: false (the default) means watch only.
  • ttlSeconds: from 60 to 86,400. The default is 3,600.
  • allowedOrigins: the origins allowed to embed the link. Burrowbox enforces it with a Content-Security-Policy: frame-ancestors header, which tells the browser which pages may frame the viewer (MDN). Leave it out and any site can embed the link.
  • showControls: true by default. It shows a small status pill and a fullscreen button.

The response has the url, expiresAt, mode, interactive, and a ready-made iframe string.

#Step 2: put it in an iframe

<iframe
  id="agent-view"
  style="width: 100%; aspect-ratio: 16 / 10; border: 0"
  allow="clipboard-read; clipboard-write; fullscreen">
</iframe>
<script type="module">
  const { url } = await fetch("/api/agent-view").then((r) => r.json());
  document.getElementById("agent-view").src = url;
</script>

Watch-only is enforced on the machine, so editing the page in dev tools doesn't give anyone the mouse. Links are signed and expire on their own, but anyone who has one can watch until then. Keep TTLs short and handle the URL like a session token: don't log it or put it on a page other people can open.

#Step 3: stream only part of the screen

The agent's browser might have other tabs open, or a page with data the customer doesn't need. Use selector (browser mode) or region (desktop or browser) to stream part of the machine.

// Only the #agenda element, following it as the page scrolls or changes
const agendaOnly = { mode: "browser", selector: "#agenda", interactive: false };

// The top-left quarter of the desktop, in screen pixels
const topLeft = { mode: "desktop", region: { x: 0, y: 0, width: 720, height: 450 } };

Send either object as the body of the same POST /api/machines/{id}/live-view call.

For browser, a region is in CSS pixels of the 1280×800 browser viewport. For desktop, it's in screen pixels.

The machine cuts out the area before anything is sent, so pixels outside it never reach the viewer, even if someone inspects the page.

A few details from the docs:

  • Until the selected element is on screen, the viewer shows "Waiting for that part of the page".
  • In an interactive area, clicks and typing work inside it and clicks outside it are dropped.
  • A browser area can't navigate, go back or switch tabs, and it doesn't show the page's URL or title.

You can test a crop before writing code: in the dashboard, Share → Area → Part of it… lets you drag a rectangle on a live screenshot or enter a selector.

#Step 4: listen to the viewer's events

The viewer posts messages to the page that embeds it. Check the origin before reading anything. MDN's advice for message listeners is to always verify the sender's identity using origin (MDN).

window.addEventListener("message", async (e) => {
  if (e.origin !== "https://burrowbox.dev" || e.data?.source !== "burrowbox") return;
  switch (e.data.type) {
    case "connected":    showStatus(`Live (${e.data.mode})`); break;
    case "disconnected": showStatus("Reconnecting…"); break; // it reconnects on its own
    case "url":          showStatus(`On ${e.data.title}`); break; // browser mode: e.data.url too
    case "expired": {
      const { url } = await fetch("/api/agent-view").then((r) => r.json());
      document.getElementById("agent-view").src = url;
      break;
    }
  }
});

Refreshing on expired lets you keep TTLs short without the view going blank for someone who leaves the tab open. Your route still checks that the customer is signed in and should see that machine, so a refresh can't outlive their access.

#Step 5: let a person take over

When the agent reaches something it shouldn't do alone, such as an SSO prompt or a payment confirmation, create a second link with interactive: true and a short TTL, and show it in place of the watch-only one.

const takeover = { mode: "browser", interactive: true, ttlSeconds: 600, allowedOrigins: ["https://app.example.com"] };

The customer types into the machine's browser directly, so their password never goes through the model. Browser cookies are kept when a machine stops, so they usually sign in once. Our ChatGPT agent post walks through this takeover pattern. For logins the agent can do on its own, the vault and browser_login are often simpler, and our 2FA login tutorial covers that path.

The agent can also ask for help itself:

  • The live-view endpoint accepts the machine's own mcpToken as well as your API key, so an agent holding only its machine token can create a link and send it to you to show the customer.
  • An agent connected to the platform MCP with your API key can call machine_live_view, which takes the same options, including region and selector (MCP).

Either way, route the link through your app to the right customer. An interactive link gives whoever holds it the mouse and keyboard until it expires.

#Building your own player

If you'd rather build your own player than use the iframe, the three WebSocket streams behind it (desktop over VNC, window events, and browser frames) are documented in Live view. Those streams use the dashboard session, so proxy them from your backend with your API key.

#Key takeaways

  • Create live-view links on your server. The API key stays there.
  • Set allowedOrigins and a short ttlSeconds, and refresh the link on expired.
  • Watch-only is the default and is enforced on the machine.
  • selector and region crop on the machine, so hidden pixels never leave it.
  • Switch to interactive: true only for the step that needs a person.

Create an account to try it, or start with Embed the live view.

#FAQ

#Can the customer click inside the embedded live view?

Only if you create the link with interactive: true. With the default false, input is blocked on the machine, not just in the page.

The link is signed and expires on its own, but anyone holding it can watch until then. Create it on your server for an authenticated customer, keep ttlSeconds short, and set allowedOrigins so other sites can't frame it.

#How do I show only one part of the agent's screen?

Pass selector (a CSS selector, browser mode only) or region (a rectangle in whole pixels) when you create the link. The machine crops the stream before sending it.

The viewer posts an expired message to your page. Ask your server for a new link and set it as the iframe's src.

#Sources