Skip to main content
Every Theo Browser session response includes an embed_url that renders the full Theo-branded browser chrome (URL bar, live indicator, auto-reconnect UI) in a third-party iframe. No SDK wiring, no manual CDP handling — point an <iframe> at the URL and ship.

Quick start

  1. Create a session from your backend (never expose your THEO_API_KEY to the browser):
import { Theo } from "@hitheo/sdk";
const theo = new Theo({ apiKey: process.env.THEO_API_KEY! });

const session = await theo.browser.create({
  url: "https://example.com",
  region: "us-west-2",
});
return Response.json({ embed_url: session.embed_url });
  1. Drop the URL into an iframe on your frontend:
<iframe
  src="{embed_url}"
  width="100%"
  height="600"
  allow="clipboard-read; clipboard-write"
  sandbox="allow-scripts allow-same-origin"
></iframe>
That’s it. The embed page handles:
  • Fetching the correct live view URL on first paint (no about:blank flash)
  • Listening for browserbase-disconnected postMessages
  • Auto-refetching and rekeying the iframe when the CDP target changes
  • Tight 10 s polling while disconnected, relaxed 30 s while healthy
  • A Theo-branded URL bar + live indicator + reconnect overlay

How the token works

embed_url is a signed, short-lived URL bound to (sessionId, userId):
  • Format: {APP_ORIGIN}/embed/browser/{sessionId}?token=<hmac>
  • Scope: the token is tied to a specific session AND the user who created it
  • Lifetime: 60 minutes (Theo Browser sessions hard-cap at 20 min, so this leaves headroom for keep_alive sessions)
  • Secret: signed with BROWSER_EMBED_TOKEN_SECRET on the server; never exposed to the client
  • Rotation: every GET /api/v1/browser/sessions/:id call mints a fresh token, so long-running dashboards can refresh the iframe on demand
A third party cannot forge a token for a session they don’t own — even if they guess the session id, HMAC validation fails.

Disconnect handling

The embed page already implements the full recovery protocol, but if you’re building a custom iframe wrapper, here is the exact contract:
// Listen on the parent window for the iframe's disconnect event.
window.addEventListener("message", (ev) => {
  if (ev.data === "browserbase-disconnected") {
    // Option A: let Theo handle it — refetch /live?force=1 and rekey.
    // Option B: surface a "reconnecting…" UI to your user.
  }
});
Internally, the embed page force-refetches its live URL the moment this event fires. The server-side session manager bumps a monotonic live_view_version counter every time it resolves a fresh debugger URL — the iframe rekeys on version change, not URL equality, because BrowserBase can return the same URL string while silently swapping the underlying CDP page target.

Security posture

  • Iframe sandbox: allow-scripts allow-same-origin is the minimal set that keeps the DevTools frontend + CDP WebSocket alive. Adding allow-forms / allow-popups has no functional benefit and can trigger stray navigations that retarget the iframe out of the live view.
  • frame-ancestors *: the embed page sets Content-Security-Policy: frame-ancestors * so any origin can iframe it. If you want to restrict which origins can load the embed, terminate the iframe at your own CDN layer and apply your own header.
  • No credentials in the iframe: the embed page never sees your API key. It talks to a companion token-authenticated /embed/browser/:id/live route.

End the session

Always end the session from your backend when the user closes the iframe, otherwise you’re billed for the remaining wall-clock time:
await theo.browser.end(session.session_id);