product embeds

Show your customers the browser: live embeds in one API call

BCTRL Team ·

TL;DR — If you build automation for your own customers, you eventually face the question: “what is it actually doing?” BCTRL answers it with three embeds — a live view, a replay, and an activity timeline — each minted as a tokenized, expiring URL with one API call, each safe to put in front of an end-customer who has never heard of BCTRL. With control: "input", the live view stops being a window and becomes a steering wheel: a human can take over mid-run, type the 2FA code, and hand back.

The integration is one POST

Every browser session on BCTRL is a run — recorded from the moment it starts, no instrumentation in your automation code. To show it to someone, mint a viewer URL:

TypeScript
const live = await bctrl.runs.live(runId, {
  control: "none",         // watch only
  expiresInSeconds: 3600,
});

live.url goes into an iframe. That’s the whole frontend integration:

HTML
<iframe src={live.url} width="1280" height="800" style="border:0"></iframe>

There’s no SDK on the client, no websocket plumbing, no auth handshake to build. The URL is the credential — scoped to one run, expiring on the schedule you chose, watch-only unless you said otherwise. You mint one per viewer and let it lapse.

🎬 Content TODO: the hero GIF — an agent filling a form in the embedded live view inside a believable third-party dashboard (fake logo, order table next to the iframe). This image carries the post.

When watching isn’t enough

The same endpoint with control: "input" lets the viewer drive the browser through the iframe. This is the feature that changes support workflows: the automation runs into a login wall, your alert fires with a takeover link in it, a support agent opens the link, types the one-time code, and the automation continues — same browser, same session, nothing restarted.

TypeScript
const takeover = await bctrl.runs.live(runId, {
  control: "input",
  expiresInSeconds: 600,   // takeover leases should be short
});

While a human holds the wheel, hosted invocations on the runtime report runtime.controller_busy rather than fighting for the mouse. When they’re done, the lease expires or you just stop minting input-capable URLs.

🎬 Content TODO: the takeover clip — agent hits a 2FA prompt, human types the code through the embed, agent continues. ~20 seconds. This is the most shareable artifact in the launch.

After the run: replay plus a timeline a customer can read

Live view answers “what is it doing”; the other half of the question is “what did it do.” Two more calls:

TypeScript
const recording = await bctrl.runs.recording(runId, {
  expiresInSeconds: 86_400,
});
// recording.url -> same iframe treatment, scrub-able replay

const activity = await bctrl.runs.activity.list(runId);
// navigation, typing, downloads, captcha solves - aggregated for humans

The activity feed is deliberately not a log file. A burst of five hundred keystrokes arrives as one evolving “typed into search field” row; a captcha solve is one entry with a duration, not forty CDP events. Render message and durationMs per item and you have a timeline an end-customer can read next to the replay — proof of work, in their dashboard, with your logo on the page.

Multi-tenant by construction

The part that makes this white-label rather than just embeddable: subaccounts. Each of your customers gets one — an isolated environment with its own runtimes, runs, vault secrets, and files — and you mint an API key confined to it:

TypeScript
const sub = await bctrl.subaccounts.create({
  name: "Acme Corp",
  externalId: "acme",
  limits: { monthlyCredits: 50_000, maxConcurrentRuntimes: 5 },
});
const key = await bctrl.apiKeys.create({
  name: "acme-key",
  subaccountId: sub.id,
});

A client built on that key lives entirely inside Acme’s world, which means an embed URL minted from it can only ever show Acme’s runs. Tenancy isn’t something you enforce in your application code on top of the API — it’s the shape of the API. The limits are the other half: one customer’s runaway job hits their concurrency ceiling, not your bill. Usage is metered per subaccount, so the invoice line items fall out of one list call.

What we’re not claiming

The viewer pages are hosted by us, unbranded. Custom domains for embed URLs aren’t there yet — it’s on the roadmap, and for iframe-based integration it rarely matters, because your page is the brand and the iframe is just the window. If your use case needs the URL itself to be yours, talk to us.

The fastest way to evaluate this is to run it: the live embed recipe and the customer replay recipe are complete files — start a runtime, mint the URLs, open them in a browser tab. It takes about five minutes to have an agent running in an iframe you control.

📦 Content TODO: the open-source demo repo — a minimal Next.js dashboard (“Acme Ops”) listing a tenant’s runs with live/replay embeds, deployable in one click. Ship it alongside this post and link it here; the repo is the Show HN, the post is the explanation.