Runtime / Mobile
Embed a live device
Put an interactive cloud device on your website or docs, like a video embed
Revyl app embeds let visitors run your app on a real cloud device from any web page. The iframe shows a device frame with a poster image until the visitor clicks start; then a live, interactive session streams into the same frame.
This page is a complete reference: every URL parameter, every postMessage command and event, ready-to-paste recipes for each configuration, limits, and troubleshooting. It is written so you (or your coding agent) can build any embed configuration from this page alone.
How it works
An embed moves through these states:
| State | What the visitor sees |
|---|---|
poster | Device frame with your poster image and a start button |
starting | Waiting for device capacity or for the cloud device to boot |
live | Interactive device stream — visitors can tap, swipe, and type |
ended | Poster again, with a restart button |
at_capacity | "Devices busy" message with retry (all org demo slots are in use) |
error | "Could not start" message with retry |
Session limits:
- Sessions end after 3 minutes idle or 10 minutes of active device time (a countdown shows in the footer). Queue and startup time do not consume the active-session allowance. The startup wait is bounded at 33 minutes, covering the device queue's 30-minute limit plus startup time.
- Concurrent embed sessions share your org's device concurrency. Visitors
beyond your plan's limit wait in the
startingstate until a device frees up; their session starts automatically without a retry. - Up to 50 embeds per app.
- The embed URL is the only credential. It can start a device session on its app and nothing else. Revoking the embed disables the URL immediately.
- Sessions bill to your org like any direct device session.
Create an embed
In the Revyl app, open your app and choose App settings in the sidebar (or Settings on the app's Overview), then find Public embed under General. Choose the embed's Build: Default branch or a branch, then Automatic, Latest build, or Exact build. It starts on Default branch and Automatic. Copy the iframe snippet, or revoke an embed from the same section.
Default branch means the app's default branch when a visitor starts a session. Visitors can't override the saved choice, and an unavailable build doesn't fall back. The choice is fixed when you create the embed; to change it, create a new embed and revoke the old one.
Base snippet:
<iframe
src="https://app.revyl.ai/embed/YOUR_EMBED_ID"
width="390"
height="740"
style="border:0"
allow="fullscreen; clipboard-read; clipboard-write"
></iframe>Any width/height works; the device frame letterboxes to fit. 390×740 matches a phone's aspect ratio plus the footer.
URL parameter reference
All parameters are optional, combine freely, and are purely presentational — none of them changes what the embed is allowed to do. Invalid values are ignored (the default applies). Unknown parameters are ignored.
| Parameter | Accepted values | Default | Effect |
|---|---|---|---|
theme | light, dark | follows the visitor's browser | Pins the embed's text/background colors. Recommended: always set it (see why below). |
accentColor | 3–8 digit hex, URL-encoded (%237C3AED) | Revyl purple gradient | Background color of the start / try-again buttons. |
buttonLabel | text, max 60 chars | "Start live demo" | Label for the start button (and the restart button after a session ends). |
posterTitle | text, max 80 chars | the app's name | Poster headline; also used as the page title. |
posterSubtitle | text, max 120 chars | "Interactive iOS/Android demo…" | Poster subline. |
controls | 0 or false | controls shown | Hides all built-in chrome: poster title/subtitle/buttons, and the ended / at-capacity / error screens. You drive the session and render your own UI via the JavaScript API. |
transparent | 1 or true | opaque background | Removes the embed's page background and the footer's top border so your page shows through. Requires cooperation from your iframe element — see Transparent mode. |
mint | opaque server-minted token | none | Single-use token from the minted session URL API. Never hand-built — use the embed_path the mint call returns. |
Why you should always pin theme: without it, the embed follows what the
browser reports inside the iframe — and for cross-origin iframes, browsers
replace the visitor's OS preference with the embedding page's own
color-scheme CSS. If your site declares a scheme (most do), the embed
inherits that; if not, it follows the visitor's OS and may flip dark on your
light page. theme=light or theme=dark removes the guesswork.
What is never configurable: the "Powered by Revyl" footer, the session countdown, and the visitor's Stop button always render. Anonymous visitors must always be able to end a session they started.
Recipes
Every combination, ready to adapt. Replace YOUR_EMBED_ID and the origin as
needed.
1. Default video-style embed
<iframe
src="https://app.revyl.ai/embed/YOUR_EMBED_ID?theme=light"
width="390" height="740" style="border:0"
allow="fullscreen; clipboard-read; clipboard-write"
></iframe>2. Branded poster
Custom copy and button color on top of your uploaded poster image:
<iframe
src="https://app.revyl.ai/embed/YOUR_EMBED_ID?theme=light&accentColor=%23FF5A5F&buttonLabel=Try%20the%20app&posterTitle=Acme%20Wallet&posterSubtitle=No%20install%20needed"
width="390" height="740" style="border:0"
allow="fullscreen; clipboard-read; clipboard-write"
></iframe>3. Transparent blend-in
The embed shows only the device frame and footer; your page shows through
everywhere else. All three style properties on the iframe are required —
see Transparent mode for why:
<iframe
src="https://app.revyl.ai/embed/YOUR_EMBED_ID?theme=light&transparent=1"
width="390" height="740"
style="border:0;background:transparent;color-scheme:light dark"
allow="fullscreen; clipboard-read; clipboard-write"
></iframe>Pick the theme that keeps the gray footer text readable against your
page background: theme=light (near-black text) for light pages,
theme=dark (light-gray text) for dark pages.
4. Fully host-controlled (your own start button and states)
controls=0 hides every built-in screen; your page renders the UI and
drives the session over postMessage:
<iframe id="revyl-embed"
src="https://app.revyl.ai/embed/YOUR_EMBED_ID?theme=light&controls=0"
width="390" height="740" style="border:0"
allow="fullscreen; clipboard-read; clipboard-write"
></iframe>
<button onclick="document.getElementById('revyl-embed').contentWindow.postMessage({type:'revyl-embed-start'},'*')">
Launch demo
</button>Listen for revyl-embed-ended, revyl-embed-at_capacity, and
revyl-embed-error to render your own overlays or retry buttons — see the
JavaScript API. When a session ends, the embed returns to
the (chromeless) poster; sending revyl-embed-start again restarts in place,
so your restart button can live anywhere on the page.
5. Invisible integration — just the device on your page
Combines 3 and 4: no built-in chrome, no background, only the device frame and the mandatory footer. This is the maximum blend-in configuration:
<iframe id="revyl-embed"
src="https://app.revyl.ai/embed/YOUR_EMBED_ID?theme=light&controls=0&transparent=1"
width="390" height="740"
style="border:0;background:transparent;color-scheme:light dark"
allow="fullscreen; clipboard-read; clipboard-write"
></iframe>6. Site with a light/dark toggle
Pin the initial theme in the URL, then forward your toggle through postMessage so the embed follows it live:
function onMyThemeToggle(next /* "light" | "dark" */) {
document
.getElementById("revyl-embed")
.contentWindow.postMessage({ type: "revyl-embed-theme", theme: next }, "*");
}Transparent mode
With transparent=1 the embed's own background disappears and the footer
loses its top border. The device frame, poster, and footer text still render.
The iframe element on your page must cooperate — all three properties:
style="border:0;background:transparent;color-scheme:light dark"border:0— removes the browser's default inset iframe border.background:transparent— any background you set on the iframe element paints behind the (now transparent) embed.color-scheme:light dark— required for Safari. Browsers paint an opaque canvas behind a cross-origin iframe when their color schemes might conflict. Chrome only blocks on an actual conflict, but Safari requires the embedding element to explicitly declare scheme support; without this property, transparency silently fails in Safari (you get an opaque white box). Declaringlight darkon both sides — the embed already declares it internally — makes the frames compatible in every engine.
Upload a poster image
The poster is shown as the device's screen before the session starts. Upload one with your org API key:
curl -X PUT "https://backend.revyl.ai/api/v1/embeds/YOUR_EMBED_ID/poster" \
-H "Authorization: Bearer $REVYL_API_KEY" \
-F "file=@screenshot.png"PNG, JPEG, GIF, or WebP up to 10 MB. An app screenshot at device aspect ratio (for example 9:19.5) looks best. Re-uploading replaces the current poster.
Launch variables
Embeds can apply your org's launch variables — environment values or iOS launch arguments injected when the app starts, such as a demo-account auth token — so anonymous visitors land in exactly the app state you want.
Attach variables when creating the embed (pass launch_env_var_ids on the
create call, or pick them in the embed dialog). Every session the embed
starts then applies them. Two properties hold by design:
- Values never reach the visitor. Secret values are decrypted server-side, injected at app launch, and scrubbed from logs and streams. The public embed endpoints never return variable ids, keys, or values.
- The selection is fixed by your org. Nothing a visitor (or the embedding page) sends can add, remove, or change which variables apply.
Attaching a secret variable (for example an auth-bypass token) to a public embed means every visitor gets that app state — that's usually the point, but make it a conscious choice.
Minted session URLs (per-session values)
When each session needs different values — say your users preview their own content inside your app — mint a one-time session URL from your backend instead of hard-coding one embed per case:
curl -X POST "https://backend.revyl.ai/api/v1/embeds/YOUR_EMBED_ID/sessions/mint" \
-H "Authorization: Bearer $REVYL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"env_vars": { "PREVIEW_ENTITY_ID": "abc123" },
"launch_env_var_ids": ["<optional stored variable ids>"],
"ttl_seconds": 600
}'Response:
{
"embed_id": "…",
"mint_token": "…",
"embed_path": "/embed/YOUR_EMBED_ID?mint=<token>",
"expires_in_seconds": 600
}Point your iframe at https://app.revyl.ai + embed_path (all other URL
parameters combine with mint as usual). The typical flow:
- Your user clicks "Preview" in your product.
- Your backend authorizes them, then calls the mint API with that user's values.
- Your frontend loads the returned
embed_pathin the iframe (or updates an existing iframe'ssrc).
Semantics:
- Single-use. The token burns when the session starts (or on any invalid redemption). A visitor restarting the demo gets a session with only the embed's stored variables — mint a fresh URL per preview.
- TTL-bound.
ttl_seconds(60–3600, default 600) caps how long an unredeemed URL stays valid. Leaked links die on their own. - Values stay server-side. The token is opaque and carries no encoded data; the browser never sees your values.
- Merge order. Embed-attached variables apply first, minted variable
ids second, inline
env_varslast — later wins per key. - Validation at mint time. Inline keys follow launch-variable key rules
(no
DYLD_/SIMCTL_CHILD_/COGNISIM_prefixes), values are capped at 8 KB, max 20 inline values, and stored-variable ids must belong to your org.
JavaScript API
The embed exchanges postMessage events with the page that embeds it. No
SDK, no keys — the messages below are the entire API.
Commands (your page → the embed)
Send with iframe.contentWindow.postMessage(message, "*"). The embed only
accepts commands from its direct parent window — sibling frames, popups,
and nested frames are ignored.
| Message | Effect |
|---|---|
{ type: "revyl-embed-start" } | Start a session from the poster or ended state. Ignored while a session is already starting or live. |
{ type: "revyl-embed-stop" } | Stop the running session. Ignored unless a session is starting or live. |
{ type: "revyl-embed-theme", theme: "light" } | Switch theme live. theme must be "light", "dark", or "system"; anything else is ignored. |
Events (the embed → your page)
Listen on window and filter by source, since the embed posts with
targetOrigin: "*":
window.addEventListener("message", (event) => {
if (event.source !== frame.contentWindow) return;
// event.data.type as below
});| Event | Fired when |
|---|---|
{ type: "revyl-embed-started" } | The session is live and interactive. |
{ type: "revyl-embed-ended" } | The session finished (visitor stop, idle timeout, or 10-min cap). |
{ type: "revyl-embed-at_capacity" } | No demo device slot is free — offer a retry. |
{ type: "revyl-embed-error", message: "..." } | The session could not start; message has a short reason. |
There is no event for the poster or starting states — drive your loading UI
from the moment you send revyl-embed-start until revyl-embed-started
arrives.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Embed renders dark on my light page (or vice versa) | No theme param, so the embed inherits your page's color-scheme CSS (or the visitor's OS setting). Pin theme=light or theme=dark. |
transparent=1 shows an opaque white box in Safari only | Your iframe element is missing color-scheme:light dark in its style. Safari requires the host-side declaration; Chrome is more permissive. |
transparent=1 shows an opaque box in every browser | The iframe element itself paints a background or border — use style="border:0;background:transparent;color-scheme:light dark" exactly. |
| A border shows around the embed | That's the browser's default iframe border on your page — set border:0 on the iframe. |
| "This live demo is unavailable." | The embed id is wrong, or the embed was revoked. Create a new embed and update the URL. |
| Visitors keep seeing the device starting | Your org's device concurrency may be fully in use. The queued session starts when a device frees up; queue time does not use its 10-minute active-session allowance. |
| Session ends by itself at 10 minutes | By design — the hard cap. Visitors can restart. |
| My timeout overlay / restart button — where does it go? | Anywhere you want: it's your page's UI. On revyl-embed-ended the embed falls back to the poster; send revyl-embed-start to restart in place. |
| Start button on my page does nothing | Commands are only accepted from the embed's direct parent window, and only when no session is active. Check event.source wiring and frame nesting. |
Embeds can be framed by any site (no ancestor allowlist) — access control is the unguessable embed URL plus per-session tokens, not framing restrictions.