> ## Documentation Index
> Fetch the complete documentation index at: https://docs.revyl.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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`    | Start button in loading state while a cloud device boots          |
| `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                              |

Hard limits, always enforced server-side:

* Sessions end after **3 minutes idle** or **10 minutes total** (a countdown
  shows in the footer).
* Concurrent embed sessions share your org's device concurrency. Visitors
  beyond your plan's limit get the `at_capacity` state.
* Up to **50 embeds per app**.
* The embed URL is the only credential. It can start a device session on the
  pinned 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's build detail panel and choose **Embed**.
Pin the embed to a specific build or let it track the latest upload, then
copy the iframe snippet. Revoke it from the same panel at any time.

Base snippet:

```html theme={null}
<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](#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](#transparent-mode).             |
| `mint`           | opaque server-minted token               | none                            | Single-use token from the [minted session URL API](#minted-session-urls-per-session-values). 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

```html theme={null}
<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:

```html theme={null}
<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](#transparent-mode) for why:

```html theme={null}
<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:

```html theme={null}
<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](#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:

```html theme={null}
<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:

```js theme={null}
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:

```html theme={null}
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). Declaring `light dark` on 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:

```bash theme={null}
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](/library/launch-vars) —
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:

```bash theme={null}
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:

```json theme={null}
{
  "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:

1. Your user clicks "Preview" in your product.
2. Your backend authorizes them, then calls the mint API with that user's
   values.
3. Your frontend loads the returned `embed_path` in the iframe (or updates
   an existing iframe's `src`).

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_vars` last — 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: "*"`:

```js theme={null}
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 report "All demo devices are busy"              | Your org's embed concurrency is exhausted. Sessions free up at stop, 3-min idle, or the 10-min cap.                                                  |
| 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.
