Embedding lets you drop a live, interactive device running your app into any web page with a single iframe — your marketing site, your docs, a sales demo page, wherever. Visitors can tap around your real app without installing anything.
Embedding is available on the Team and Business plans. If you’re on Free or Solo, upgrade to create an embed key. If your organization needs custom terms, contact support@vibeview.io.
Create an embed key
Go to Sharing, click Share an app, pick the app, choose Embed on a website, and click Continue. Creating, editing, revoking and deleting embeds needs the Developer or Admin role; Viewers can see the list. The first screen asks for a Name (for an app called Atlas it starts as Atlas embed, then becomes Atlas on example.com once you add a domain, until you type your own), the App and the Allowed domains. Everything else — build, device options, session limits, visitor identity, audio and agent control — is under More options. Each embed controls one embeddable demo:
- Name — a label to help you tell keys apart (e.g. “Docs demo”).
- App — the app the embed streams. Every session started with this key runs this app.
- Build — Latest build (auto) plays the newest build, preferring a release build when there is one; Pin to a build always plays the build you pick, whatever is uploaded later. A React Native debug build loads its JavaScript from a Metro dev server, which visitors don’t have, so they’d see an error screen instead of your app. The form warns you when the build you chose (or, on Latest, every build of the app) is one of these, and marks them needs Metro in the build list. Pin a release build for a demo that must not change under you. Picking a specific build turns on Keep for it, so it isn’t removed while the embed uses it (see How long builds are kept).
- Allowed domains — the domains permitted to embed this key. Type a domain and press Enter (or comma) to add it; pasting a list splits it into entries automatically. On the Team plan this is required and covers one domain (a domain and its subdomains count as one;
localhostdoesn’t count); on Business it’s optional. See Domain allowlisting below. - Max session duration (optional) — how long, in seconds, a visitor can play once your app is on screen. Time spent waiting in the queue or for the device to start doesn’t count toward it.
- Max concurrent sessions (optional) — the most sessions this key can have running at once. Visitors who tap start while the key is at its limit wait in a queue and start automatically when a slot frees up; if the wait line itself is full, they’re asked to try again in a few minutes.
- Device models (optional) — pick from a list of the device models currently available to your organization. Select none and visitors get a phone-size device (never a foldable); select one or more and visitors get any available device from your selection (pick a specific model to stream a different form factor, for example a tablet).
- Let visitors choose the device — when on, visitors can pick which device to run your app on. A picker only appears when you’ve selected two or more device models; with one model (or none) selected, visitors get a default device with no picker.
- Max sessions per visitor — how many sessions one identified visitor may have running at once, from 1 to 10 (default 1). A visitor already at the limit is handed their existing session back rather than refused or queued. It only bites on visitors your page identifies — see Identifying your visitors below.
- Require a visitor identity — off by default. When on, sessions start only for visitors the embedding page has identified; a page that presents nothing is refused. Leave it off unless every page using this key signs its visitors.
- Allow agent control (admins only) — off by default. When on, your own team can drive sessions started with this key programmatically, using your organization’s credentials. It changes nothing for visitors. See Allow agent control below.
Click Create, and the key appears in your list along with a ready-to-copy iframe snippet. You’re also shown the key’s visitor signing secret, once — see Identifying your visitors for what it’s for and why it isn’t shown twice.
Editing a key
Each active key in your list has an Edit action. Editing opens the same form pre-filled with the key’s current settings, so you can change its name, app, build, allowed domains, device options, max session duration, max concurrent sessions, max sessions per visitor, and whether a visitor identity is required, at any time. Changes apply to new sessions started after you save. Retargeting a key to a different app is allowed — handy when a successor app replaces the one you originally embedded, without re-embedding a new key on every page. A pinned build belongs to the old app, so retargeting switches the key back to Latest build (auto) unless you pin a build of the new app.
The snippet
Each active key shows its embed snippet with a Copy button:
<iframe
src="https://vibeview.io/embed/ek_live_xxxxxxxxxxxxxxxxxxxxxxxx"
style="width:100%;max-width:380px;aspect-ratio:380/820;border:0"
allow="clipboard-write"
></iframe>
Paste it anywhere you can embed an iframe. The frame fills the width it’s given, up to 380 px, and keeps a typical phone’s proportions, so it fits a phone-width page without clipping the device. Change max-width (and aspect-ratio, for a tablet) to suit your layout.
A key for an Apple TV, Android TV or Roku app gets a landscape snippet instead, sized for a TV screen:
<iframe
src="https://vibeview.io/embed/ek_live_xxxxxxxxxxxxxxxxxxxxxxxx"
style="width:100%;max-width:960px;aspect-ratio:16/9;border:0"
allow="clipboard-write"
></iframe>
If the key has Audio turned on (see Audio below), the snippet’s allow attribute also includes autoplay for sound and microphone for the microphone — for Sound + microphone, allow="clipboard-write; autoplay; microphone". Keep the allow attribute as generated.
Test it before you go live
You don’t need to publish anything to try an embed:
- Add
localhostto the key’s allowed domains (on Team it doesn’t use up your one domain). - Save the snippet in an
.htmlfile and serve it from a local web server, for examplepython3 -m http.server 8000in that folder, then openhttp://localhost:8000/your-file.html. Any port works.
Two things that look like they should work but don’t:
http://127.0.0.1is a different hostname fromlocalhost. Uselocalhost, or add127.0.0.1to the allowed domains as well.- Opening the file straight from disk (
file://) gives the embed no domain to check, so a key with allowed domains refuses it. Serve the page instead.
If the page isn’t allowed, the embed says so straight away (Unavailable — This embed is not available on this site.) instead of offering Tap to Start.
Revoking a key from Sharing is permanent: the key stops working immediately, any page using it stops loading, and it can’t be re-enabled. Create a new key if you need one later.
Delete removes a key from Sharing for good, whether it is active or already revoked. Deleting an active key also stops it working immediately and ends any open session started from it. Past sessions stay in your session history and usage.
Foldable devices
If a key’s device selection includes a foldable (the iPhone Duo, or an Android foldable such as the Pixel 9 Pro Fold), the embedded session starts closed, on the smaller cover screen, the same as on the Devices page. Add a posture query parameter to the iframe src to start it already open or partially open instead:
<iframe
src="https://vibeview.io/embed/ek_live_xxxxxxxxxxxxxxxxxxxxxxxx?posture=open"
width="380"
height="820"
></iframe>
Accepted values are open, partial, and closed. It has no effect on a non-foldable device.
While the device is still connecting, the embedded frame is already shaped like the screen the session will use — the narrow cover screen or the wider open one — and it reshapes if the device is folded or unfolded during the session. Size the iframe for whichever screen your demo spends most of its time on.
Audio
By default, embedded sessions are muted — visitors see the device but can’t hear it, and can’t use its microphone. Set Audio on the key (in the create or edit form) to Sound only, Microphone only or Sound + microphone to let visitors turn those on for themselves with the Sound button on the embed. Nothing turns on automatically: visitors still choose to enable sound, and to pick a microphone (their browser asks their permission). Sound works on iPhone/iPad, Apple TV, Android, Android TV and Roku, and the microphone on iPhone/iPad and Android phones and tablets (iOS and tvOS 18 or later on Apple devices). See Device audio for what visitors see and hear once it’s on.
Domain allowlisting
If you set allowed domains on a key, only pages served from those domains can start a session with it — an embed on any other site is refused with “This embed is not available on this site.”
Each entry is either an exact hostname or a wildcard:
example.commatches onlyexample.comitself.docs.example.commatches only that subdomain.*.example.commatches every subdomain (docs.example.com,app.example.com, …) but notexample.comitself — add both entries to cover a whole site.
You can paste full URLs — the scheme, path, and port are stripped automatically. Entries that are too broad (such as * or *.com) are rejected.
Plan limits. On the Team plan, every key must list at least one allowed domain, and your organization’s keys can together cover one domain — a base domain plus its subdomains and wildcard count as a single domain, so example.com, docs.example.com, and *.example.com all fit within the limit. localhost and 127.0.0.1 never count toward the limit, so you can keep a local test page working next to your live site. On Business, domains are unlimited, and you may also leave the list empty to allow embedding from anywhere.
Allowlisting checks the referring page’s domain. Some browsers and privacy settings strip the referrer entirely before it reaches VibeView — in that case, a page on an allowed domain can still be refused with “This embed is not available on this site.” because there’s no referrer to check against the list. If you rely on an allowlist and see this happen on a legitimate page, check whether your site (or a browser extension your visitors use) is suppressing the referrer. On Business you can leave the allowlist empty if referrer suppression is not something you control.
Identifying your visitors
By default every visit to your embed is anonymous. VibeView has no idea who tapped start, so a visitor who reloads the page gets a brand-new session on a brand-new device, and the limits you set on a key apply to the key as a whole rather than to any one person.
If you already know who your visitors are — because they sign in to your product before they reach the page the demo is on — you can tell VibeView which one of them is starting the session. Two things change once you do:
- A returning visitor lands back in the session they already have running instead of burning another device. Reload the page, close the tab by accident and reopen it, switch away and come straight back — same live session, same device, the app exactly where they left it. This is what Max sessions per visitor of 1 (the default) gives you: a visitor is handed a session back once they are at their limit, so on a key set to 2 or more a reload starts a second session until they reach it.
- Limits can apply per person. Max sessions per visitor counts that one visitor’s sessions rather than everyone’s, so one person can’t quietly occupy your whole allowance.
Because that decision hands one visitor access to a running session, VibeView won’t take your word for who they are: your server vouches for the visitor with a short-lived signed assertion, and VibeView refuses anything it can’t verify.
Your signing secret
Every embed key has its own signing secret. It’s generated when you create the key and shown to you exactly once, in a panel right after you click Create — copy it then and store it wherever your server keeps its other secrets. It can’t be viewed again afterwards.
If you lose it, or you want to retire it, use Rotate secret in the embed’s details on Sharing. Only an organization admin can rotate, because rotation is immediate and org-wide: every page still signing with the old secret stops starting identified sessions the moment you confirm, and stays broken until you deploy the new one. Rotate when you’re ready to ship the replacement in the same breath. Anonymous embeds (pages that don’t sign visitors) never use the secret and keep working through a rotation. The new secret is shown once too, on the same terms.
Keys you created before this feature existed have no secret yet — rotate once to issue their first one.
The secret must never reach the browser. Sign on your server, in the code that renders the page. The embed key itself can’t do this job: it’s public, it sits in the iframe URL on your page, and anyone can read it. A secret pasted into browser JavaScript is published to every visitor who opens devtools, and a visitor holding it can sign themselves as anybody else.
For the assertion format itself — what to sign, with which algorithm, and how long it may live — email support@vibeview.io and we’ll send you the current specification with a sample for your language.
Handing it to the embed
The assertion goes on the end of the embed URL, after a #:
<iframe
src="https://vibeview.io/embed/ek_live_xxxxxxxxxxxxxxxxxxxxxxxx#visitor_token=eyJhbGciOi..."
width="380"
height="820"
></iframe>
Build that src on the server as you render the page — the same place you signed the assertion. Everything else about the snippet stays as it is.
Two things follow from putting it after the #:
- It stays in the address bar, which is what makes a reload work — the page reads it again and lands the visitor back in their session. Re-signing means rendering the page again; changing the address bar by hand doesn’t re-sign anything.
- It never reaches your own web server, so it can’t turn up in your access logs or in the referrer of anything your page loads afterwards. It does reach VibeView when the session starts, so treat it as a credential on your side too: anything that records your page’s outgoing traffic in full — an APM agent, a logging proxy, an error reporter — can still capture it. VibeView scrubs it from its own logs. Sign a fresh, short-lived one every time you render the page.
Leaving it off changes nothing. A page with no assertion starts an anonymous session exactly as it always has, and you can move your pages over one at a time. The one exception is a key with Require a visitor identity turned on, which is described below.
Requiring an identity
Require a visitor identity is a per-key setting, off by default. With it on, a session can only start when the page presents an assertion that verifies — no assertion is refused just as firmly as a bad one.
Turn it on once every page embedding that key identifies its visitors. Until then it costs you nothing to leave off, but leave it off and your per-visitor limit is only advisory: a visitor who edits the URL in devtools to drop the assertion is anonymous again, and anonymous visitors have no per-person limit to reach. It’s the setting that turns Max sessions per visitor from a courtesy into a limit.
A key that has no signing secret cannot verify anything, so turning the setting on is refused with a message saying so — otherwise the key would be left unable to start a session for anyone at all. Keys created before visitor identity existed are the only ones in that state; an admin rotating the key’s secret once clears it, and the setting saves normally afterwards.
When something is wrong
Whatever the problem, it stops before a device is handed out, so a refused visitor never costs you streaming minutes. What they see depends on which thing went wrong:
- “This preview link has expired — reload the page” — the assertion is past its expiry, or your server’s clock is running ahead of real time (a minute of drift either way is tolerated; more than that isn’t). Reloading is genuinely the fix for the first: your server signs a fresh one as it renders. If visitors hit this on a page they’ve had open a while, consider re-rendering the iframe with a freshly signed assertion when the page regains focus, rather than signing only at first render.
- “This visitor identity could not be verified” — the assertion didn’t check out. In practice that’s signing with the wrong (usually a rotated-away) secret, signing for a different embed key than the one in the URL, or an assertion that doesn’t match the format. Reloading won’t help until you fix the signing side.
- “This demo is not configured to accept visitor identities” — the key has no signing secret. Rotate the key’s secret once to issue one.
- “This demo requires a visitor identity” — Require a visitor identity is on and the page presented nothing at all. Check that the assertion is actually on the URL your server rendered, after the
#. - “You already have a session running — it will free up shortly” — the visitor is at their Max sessions per visitor limit, and none of the sessions they hold can be handed back. In practice this only happens right after you retarget a key to a different app: their existing session is running the old app, so it can’t be resumed, but it still counts until it ends.
A value that isn’t even shaped like an assertion — empty, truncated, or carrying characters an assertion never contains — is ignored rather than treated as a failure, and the visitor gets an ordinary anonymous session (or, on a key that requires an identity, the message above). A bug in your page’s URL building can’t leave a visitor staring at a broken demo; it does log a warning to the browser console, which is the quickest way to spot one.
What a resumed session does and doesn’t do
- A resume allocates nothing. No new device, no place in the queue, no second session against your plan’s concurrent limit. That’s why a visitor at their Max sessions per visitor limit is handed their session back rather than being turned away or queued — queueing them would be asking them to wait for a device that only frees when they close the very session they’re trying to reach.
- The clock keeps running. If the key has a Max session duration, it’s measured from when the session first started, not from the resume. Someone who reloads at minute nine of a ten-minute demo gets one more minute, not ten — otherwise the limit would be a suggestion.
- Two different people never share a session. A resume matches on the visitor you signed for, so one visitor never reaches another’s session, and a visitor’s session on one embed key is invisible to every other key.
- Coming straight back works; coming back later doesn’t. An identified session survives a refresh, a tab closed by accident, and a second tab opened alongside the first — that’s the point of it. It is not a bookmark: a session nobody is connected to is cleaned up within a couple of minutes, so a visitor who wanders off and returns later starts a fresh one. An anonymous session is unchanged: closing the page ends it right away, because nobody could ever reach that session again.
- Two tabs are one session. With the default Max sessions per visitor of 1, a second tab on the same page is handed the same running session rather than a second device — and closing one tab does not take the other down with it.
- Ending is still ending. Once a session has finished — the visitor closed it, a limit was reached, or it timed out — it isn’t resumed. The next visit starts a fresh one.
Allow agent control
Allow agent control is a per-key setting that is off by default. When it’s on, someone in your organization holding a Developer or Admin credential can drive a session started from this key — reading the screen, tapping, typing, scrolling — with the commands described in AI Agent Control. They target the visitor’s existing session by its id: no second session is started and no extra device is used, and the visitor watches every action happen live on the device in front of them. Useful for guiding someone through your demo, or for checking that an embedded demo still works end to end.
Only an organization admin can change the setting. Other members see it on the key’s form but can’t edit it. Keys with it on show an Agent control pill beside Active in your key list, so you can tell at a glance which demos are drivable. Changes take effect immediately, including on sessions that are already running — switching it back off stops an in-progress agent at its next command.
Revoking a key does the same thing, and so does disabling one: a key that isn’t active can’t be driven, whatever this setting says. Any of the three is an immediate stop, not just a block on starting new sessions.
The setting grants nothing to the people viewing your embed. Driving a session takes an organization credential the embedded page never has, so no visitor and no page you paste the snippet into can do it, and turning the setting on doesn’t change anything a visitor can tap, see, or reach. The confinement described below stays in place either way: what your team can do on an embed session is a restricted set of commands that can’t leave your app — no home button, no deep links.
What visitors experience
Visitors see a Tap to Start button. Once tapped:
-
If a device is free, the session connects and your app loads on it automatically.
-
If the session can’t start straight away, the visitor is queued and sees their position in line; the session starts the moment it’s their turn. When the reason for the wait is known, the queue card says so in one line:
- All devices are in use right now — every device that can run your app is busy with another session.
- A device is being prepared and will be ready shortly — a device is being reset after the previous session, or the devices that run your app are briefly out of service for an update or restart.
- The most sessions this app can run at once are already running — your plan’s limit on sessions running at the same time has been reached; the next visitor starts when one of your sessions ends.
- The most people who can play at once are already playing — the embed key’s own Max concurrent sessions limit has been reached.
When there’s no specific reason to give, the card shows only the position.
-
While the device starts, the screen says what it’s waiting for: Starting device… until the device is up, then Opening app… until your app is in the foreground. A start that runs long adds a line saying it can take up to a minute when devices are busy.
-
The embed stays covered until the configured app is in the foreground. If the app exits or loses the foreground, VibeView covers the embed while restoring the app.
-
On a phone or tablet browser, a swipe on the device drives the device; it doesn’t scroll your page, and a swipe that runs past the edge of the embed doesn’t carry on into your page either.
-
Apple TV, Android TV and Roku sessions are driven from the keyboard: arrow keys move, Enter selects, Esc goes back. The embed only hears keys while it has keyboard focus, so whenever a live TV session doesn’t have it (the visitor clicked elsewhere on your page, say), the device is covered by a light Click to use your keyboard prompt. One click hands focus back and the prompt goes away. See Keyboard focus on TV embeds to hand focus over from your own page.
-
On a phone or tablet, which has no keyboard, a TV session shows a small remote under the TV instead: Back, the arrows and OK. There is no Home button, because the session stays inside your app.
-
While the session is live, the embed keeps the visitor focused on your app — system navigation that would exit your app (like a home button) is blocked, so visitors stay on the experience you set up for them.
-
If the key has a max session duration, the visitor sees a countdown, second by second, in the final minute before the session ends. The duration counts from the moment your app is on screen, so a visitor who waited in the queue still gets their full demo.
-
When the session ends (visitor closes it, or a cap is reached), the visitor sees Demo ended with a Replay? button that starts a new session straight away. If the visitor’s connection to the device drops and can’t be restored, the session ends the same way, with a line saying the connection was lost.
-
If the device can’t be started, the visitor sees Couldn’t start the device (with the reason, when there is one) and Try again, which starts a new session straight away. The host page receives an
errorevent followed bysession:ended. The same card appears for a session that ends before the device ever showed a picture. -
If you revoke the embed key while someone is playing, their session ends with This demo is no longer available and no Replay? button. A page that loads a revoked or deleted key shows the same message instead of Tap to Start.
-
If the visitor closes the page or navigates away, the session ends automatically a moment later.
-
iPhone and iPad sessions appear inside that model’s own device frame, with the screen’s real rounded corners, so the embed looks like the hardware your app runs on. Apple TV and Android TV sessions appear inside a TV frame, and Android phones and foldables inside a neutral phone or foldable frame. When the key runs on a specific device model (or the visitor picks one), the Tap to Start screen is already drawn in that frame; otherwise it uses a generic device frame until the session names its device.
Usage and limits
Your organization pays for every embed session. Visitors never sign in or pay; each session they play uses your organization’s streaming minutes, exactly as if you had played it yourself. Embed sessions are ordinary VibeView sessions from a billing standpoint: the streaming time they use (from the moment a device is assigned, including its start-up — never time spent waiting in the queue) counts toward your plan’s included streaming minutes, and each running embed session counts toward your plan’s concurrent session limit. Once you’re past your included minutes, streaming continues based on your organization’s overage settings (see Billing and Plan and usage).
The max concurrent sessions and max session duration fields on an embed key are optional limits you can set in addition to your plan’s own limits — they cap what that one key can do, on top of whatever your plan already allows. Reaching a concurrent-session limit — the key’s own or your plan’s — doesn’t turn visitors away: they wait in a queue and start automatically when a slot frees up. A visitor who is still waiting doesn’t count toward your plan’s limit. Streaming minutes are different: once your organization has used its included minutes and streaming can’t continue on overage (overage is turned off, or your usage credit can’t cover it), new visitors can’t start a session.
Page events (postMessage)
The embedded page sends postMessage events to the parent page so you can react to what’s happening in the demo — for example, to show your own “loading” UI or track analytics.
Every event has the same envelope:
{ "source": "vibeview-embed", "version": 1, "type": "session:started", ...extraFields }
type is one of:
| Type | Meaning |
|---|---|
ready | The embedded page has loaded and is showing the start screen. |
session:queued | The visitor is waiting in queue. Includes position, always 1 or higher, and no session id. Sent at most once per session: a visitor returning to a session that’s still being set up gets it only once that session turns out to be waiting for a device, and session:started follows when it’s ready. |
session:started | A device has been assigned to the visitor and is starting. Includes sessionId. The app is usually on screen 10–40 seconds later; wait for session:ready before hiding your own loading UI. |
session:ready | Your app is on screen and the visitor can use it. Includes sessionId. Sent once per session. |
session:ended | The session ended, however it ended. Sent once per session. Includes reason (see below) and sessionId. |
error | Something went wrong starting or running the session. Includes reason. |
session:ended’s reason says why. The most common values:
reason | Why the session ended |
|---|---|
stopped | The visitor stopped it, or your page sent stop. |
demo_session_limit | The key’s max session duration was reached. |
link_revoked | You revoked or deleted the key. |
inactivity_timeout | The visitor stopped interacting for longer than your organization’s idle timeout. |
stopped_by_admin | VibeView support stopped the session. |
worker_lost | The device stopped responding and the session couldn’t continue. |
device_crashed | The device stopped unexpectedly during the session (occasionally also a device someone in the session turned off, when that couldn’t be confirmed). |
device_turned_off | Someone in the session turned the device off from its own menu (for example “slide to power off”). |
device_restarted | Someone in the session restarted the device from its own menu (for example Restart in an Apple TV’s Settings or an Android phone’s power menu). |
device_storage_limit | The device filled its storage allowance (iPhone, iPad and Apple TV: 25 GB, less when the device host is low on space). |
app_not_allowed | An app that isn’t allowed on VibeView was installed on the device (see Restricted apps). |
connection_lost | The visitor’s connection to the device dropped and couldn’t be restored. |
session_failed | The device couldn’t start the session (an error event comes first). |
ended | The session ended but the reason wasn’t reported; treat it like any other end. |
Other values can appear (for example when your organization’s streaming minutes run out); handle any session:ended as the end of the session whatever its reason.
Example listener:
// Select the iframe whose events this page should handle.
const iframe = document.querySelector('iframe[src^="https://vibeview.io/embed/"]');
window.addEventListener('message', (event) => {
if (event.origin !== 'https://vibeview.io') return;
if (!iframe || event.source !== iframe.contentWindow) return;
if (event.data?.source !== 'vibeview-embed' || event.data.version !== 1) return;
console.log('VibeView embed event:', event.data.type, event.data);
});
You can also send commands from the parent page into the embed:
iframe.contentWindow.postMessage(
{ source: 'vibeview-embed-host', type: 'start' },
'https://vibeview.io',
);
iframe.contentWindow.postMessage(
{ source: 'vibeview-embed-host', type: 'stop' },
'https://vibeview.io',
);
start begins a session as if the visitor tapped the button themselves (useful if you want your own call-to-action to trigger it). stop ends the current session early.
Keyboard focus on TV embeds
A TV session takes the visitor’s keyboard, and a browser only sends keys to the frame that has focus. If your page moves focus somewhere else — or you start the session from your own button with start — hand focus to the embed yourself by focusing the iframe:
iframe.focus();
That’s all it takes; there is no postMessage command for it, because focusing the iframe element from your page is the one way browsers reliably allow. The embed’s Click to use your keyboard prompt goes away as soon as the frame has focus.