Developer docs

Embeds and attribution

One iframe that works on any page builder, a loader for React and AI-built sites, oEmbed for a pasted link, the script that carries the host page's campaign attribution into the player, and the events the player sends back out.

The universal embed

Every video's Share modal offers one embed: a responsive 16:9 wrapper capped at 780px around the chrome-free /embed/{shortId} surface, plus two scripts. The first copies the host page's query string, its ad cookies, its consent signal, its referrer and its URL onto the iframe source at insert time; the second injects the video's structured data into the host page for search and AI crawlers. Copy it from the Share modal; this is what it produces.

<!-- StreamAgent video -- works on any page builder -->
<div style="position:relative;width:100%;max-width:780px;aspect-ratio:16/9;margin:0 auto;">
  <!-- aspect-ratio needs a modern browser; for legacy support replace it with: height:0;padding-bottom:56.25%; -->
  <iframe id="sa-abc123"
    src="https://you.streamagent.io/embed/abc123"
    style="position:absolute;inset:0;width:100%;height:100%;border:0;"
    allow="autoplay; encrypted-media; picture-in-picture; fullscreen; clipboard-write"
    allowfullscreen
    loading="lazy"
    referrerpolicy="no-referrer-when-downgrade"
    title="Pricing walkthrough"></iframe>
</div>
<script>
  /* StreamAgent: carries this page's campaign attribution into the player. */
  (function () {
    var f = document.getElementById('sa-abc123');
    if (!f || f.getAttribute('data-sa-forwarded')) return;
    f.setAttribute('data-sa-forwarded', '1');
    var q = new URLSearchParams(window.location.search);
    function cookie(n) { var m = document.cookie.match(new RegExp('(^| )' + n + '=([^;]+)')); return m ? decodeURIComponent(m[2]) : ''; }
    var c = { fbp: cookie('_fbp'), fbc: cookie('_fbc'), ttp: cookie('_ttp'), ga_client_id: cookie('_ga').replace(/^GA\d+\.\d+\./, '') };
    for (var k in c) { if (c[k] && !q.has(k)) q.set(k, c[k]); }
    var consent = window.__streamAgentConsent || document.documentElement.getAttribute('data-sa-consent');
    if (consent && !q.has('ad_consent')) q.set('ad_consent', String(consent));
    if (document.referrer) q.set('sa_ref', document.referrer);
    q.set('sa_url', window.location.href.split('#')[0]);
    f.src = f.src + (f.src.indexOf('?') > -1 ? '&' : '?') + q.toString();
  })();
</script>
<script src="https://you.streamagent.io/embed/abc123/companion.js" async></script>
A video embed, as copied from the Share modal

Inside a customer's frame the player sets its own cookies as partitioned (SameSite=None; Secure; Partitioned), so a viewer who gives their email on one page of the host site is still known on its other pages, and never on any other site.

Routes and rotations

A route embeds from its own URL with ?embed=1, which renders the bare player rather than the share page. The wrapper takes the entry video's aspect ratio, so a vertical route gives a vertical embed. It carries the same forwarding script as a video, which marks the frame so a second script on the page never forwards twice.

<!-- Pricing walkthrough -- interactive video -- works on any page builder -->
<div style="position:relative;width:100%;aspect-ratio:16/9;">
  <!-- aspect-ratio needs a modern browser; for legacy support replace it with: height:0;padding-bottom:56.25%; -->
  <iframe id="sa-route-pricing-walkthrough"
    src="https://app.streamagent.io/r/pricing-walkthrough?embed=1"
    style="position:absolute;inset:0;width:100%;height:100%;border:0;"
    allow="autoplay; encrypted-media; picture-in-picture; fullscreen; clipboard-write"
    allowfullscreen
    loading="lazy"
    referrerpolicy="no-referrer-when-downgrade"
    title="Pricing walkthrough"></iframe>
</div>
<script>
  /* StreamAgent: carries this page's campaign attribution into the player. */
  (function () {
    var f = document.getElementById('sa-route-pricing-walkthrough');
    if (!f || f.getAttribute('data-sa-forwarded')) return;
    f.setAttribute('data-sa-forwarded', '1');
    var q = new URLSearchParams(window.location.search);
    function cookie(n) { var m = document.cookie.match(new RegExp('(^| )' + n + '=([^;]+)')); return m ? decodeURIComponent(m[2]) : ''; }
    var c = { fbp: cookie('_fbp'), fbc: cookie('_fbc'), ttp: cookie('_ttp'), ga_client_id: cookie('_ga').replace(/^GA\d+\.\d+\./, '') };
    for (var k in c) { if (c[k] && !q.has(k)) q.set(k, c[k]); }
    var consent = window.__streamAgentConsent || document.documentElement.getAttribute('data-sa-consent');
    if (consent && !q.has('ad_consent')) q.set('ad_consent', String(consent));
    if (document.referrer) q.set('sa_ref', document.referrer);
    q.set('sa_url', window.location.href.split('#')[0]);
    f.src = f.src + (f.src.indexOf('?') > -1 ? '&' : '?') + q.toString();
  })();
</script>
A route embed, as copied from the route's Embed action

A Smart Rotation embeds through /embed/r/{slug}. The server resolves the audience lane and redirects the frame to the winning video or route, carrying the forwarded parameters with it so lane rules that match on utm_source or utm_campaign see them.

<!-- StreamAgent -- works on any page builder -->
<div style="position:relative;width:100%;max-width:780px;aspect-ratio:16/9;margin:0 auto;">
  <!-- aspect-ratio needs a modern browser; for legacy support replace it with: height:0;padding-bottom:56.25%; -->
  <iframe id="sa-spring-offer"
    src="https://you.streamagent.io/embed/r/spring-offer"
    style="position:absolute;inset:0;width:100%;height:100%;border:0;"
    allow="autoplay; encrypted-media; picture-in-picture; fullscreen; clipboard-write"
    allowfullscreen
    loading="lazy"
    referrerpolicy="no-referrer-when-downgrade"></iframe>
</div>
<script>
  /* StreamAgent: carries this page's campaign attribution into the player. */
  (function () {
    var f = document.getElementById('sa-spring-offer');
    if (!f || f.getAttribute('data-sa-forwarded')) return;
    f.setAttribute('data-sa-forwarded', '1');
    var q = new URLSearchParams(window.location.search);
    function cookie(n) { var m = document.cookie.match(new RegExp('(^| )' + n + '=([^;]+)')); return m ? decodeURIComponent(m[2]) : ''; }
    var c = { fbp: cookie('_fbp'), fbc: cookie('_fbc'), ttp: cookie('_ttp'), ga_client_id: cookie('_ga').replace(/^GA\d+\.\d+\./, '') };
    for (var k in c) { if (c[k] && !q.has(k)) q.set(k, c[k]); }
    var consent = window.__streamAgentConsent || document.documentElement.getAttribute('data-sa-consent');
    if (consent && !q.has('ad_consent')) q.set('ad_consent', String(consent));
    if (document.referrer) q.set('sa_ref', document.referrer);
    q.set('sa_url', window.location.href.split('#')[0]);
    f.src = f.src + (f.src.indexOf('?') > -1 ? '&' : '?') + q.toString();
  })();
</script>
A rotation embed

What the forwarding script carries

Added to the iframe URLValue
The host page's query stringEverything after ? on the host URL: utm_*, ad click ids (fbclid, gclid, ttclid, li_fat_id), lid warm links and sa_id personalised-link tokens.
fbp, fbc, ttp, ga_client_idThe host page's _fbp, _fbc, _ttp and _ga cookies. A cross-site frame can never read these itself, and _fbp is the viewer's browser id on the ad platform, the strongest match signal there is. A value already on the host URL wins.
ad_consentThe host page's consent signal (window.__streamAgentConsent or <html data-sa-consent>), so a consent manager on the host site governs the events the player shares with ad platforms.
sa_refThe host page's referrer, so the player records where the visitor came from, not the host page itself.
sa_urlThe host page's URL, recorded as the page the video was on.

Inside the frame the player keeps these for the session, so a lead captured three minutes in, or on the third step of a route, still carries the campaign and the browser id that brought the visitor.

Attribution by query string

If you build your own iframe, put the ids you have on the src yourself. A bare fbclid becomes an fbc click id on its own. The player reads these keys and nothing else:

fbp
fbc
gclid
ttclid
ttp
ga_client_id
li_fat_id
fbclid
ad_consent
<iframe src="https://you.streamagent.io/embed/abc123?gclid=Cj0K…&fbclid=IwAR…"></iframe>

Attribution by postMessage

Cookies such as _fbp and _fbc live on your domain, not on the URL. Send them into the frame after it loads with a message of type streamagent:attribution. Only the keys above are read; anything else is ignored. Values sent this way take priority over query parameters.

<iframe id="sa-abc123" src="https://you.streamagent.io/embed/abc123"></iframe>
<script>
  var frame = document.getElementById('sa-abc123');
  var cookie = function (name) {
    var m = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
    return m ? m[2] : '';
  };
  frame.addEventListener('load', function () {
    frame.contentWindow.postMessage({
      type: 'streamagent:attribution',
      attribution: {
        fbp: cookie('_fbp'),
        fbc: cookie('_fbc'),
        gclid: new URLSearchParams(location.search).get('gclid') || ''
      }
    }, '*');
  });
  window.addEventListener('message', function (e) {
    if (e.data && e.data.type === 'streamagent:attribution-ack') { /* received: true */ }
  });
</script>
Send cookie-based ids into the frame
  • Send as early as you can after the frame loads; the player waits briefly for a message before settling on the URL values.
  • The frame replies with { type: 'streamagent:attribution-ack', received: true } once it has applied the values.
  • Empty strings and unknown keys are dropped. Attribution is used for analytics, lead records and ad-platform events only.

The loader, for React and AI-built sites

A React site (a Next.js app, or anything Lovable, v0, Bolt or Replit builds) renders pasted HTML through innerHTML, which never runs the scripts inside it: the universal embed plays, but forwards nothing and injects nothing. Use the loader there. One script tag for the whole site, in index.html or the root layout, and a placeholder wherever a video goes. The placeholder's attributes are valid JSX as written.

<!-- Once per site: in index.html, or the root layout -->
<script src="https://you.streamagent.io/embed/sdk.js" async></script>

<!-- Wherever the video goes -->
<div data-sa-video="abc123" data-sa-title="Pricing walkthrough" data-sa-max-width="780px"></div>
The loader paste, as copied from the Share modal (React option)

On an AI site builder (Lovable, v0, Bolt, Replit) nobody edits a file: you tell the builder what to do. The Share modal's builder option therefore hands you a prompt with the same two lines inside it, and one rule the builder needs, since left alone it will swap the placeholder for a raw iframe and lose the forwarding. Send it to the builder's chat as-is.

Add my StreamAgent video to the page.

1. Put this script tag once in index.html, inside <head> (or in the root layout):
<script src="https://you.streamagent.io/embed/sdk.js" async></script>

2. Put this placeholder exactly where the video should appear. Its attributes are valid JSX; keep them as written:
<div data-sa-video="abc123" data-sa-title="Pricing walkthrough" data-sa-max-width="780px"></div>

Do not replace the placeholder with an iframe, do not use dangerouslySetInnerHTML, and do not add any other video code. The script turns the placeholder into the player on its own.
The prompt, as copied from the Share modal (Lovable, v0, Bolt or Replit option)

When the builder is connected to StreamAgent itself (through the MCP server or the REST API), nobody has to copy anything: its agent asks for the embed code with get_embed_snippet (GET /api/v1/embed) and gets the same script tag, placeholder and prompt back for any video, route or rotation. A video that opens a live route comes back as the route, so the page captures leads.

The loader is served from /embed/sdk.js on your public host and builds the same frame the snippet pastes, pointed at that host. For each placeholder it forwards your page's query string, ad cookies, consent signal, referrer and URL (the same keys as the forwarding script), loads the video's companion script, and mounts placeholders added later by a re-render, once each.

PlaceholderOpens
<div data-sa-video="{shortId}">A video.
<div data-sa-route="{slug}">A route, as the bare player.
<div data-sa-rotation="{slug}">A Smart Rotation.
<streamagent-video video="{shortId}">The same as a custom element; route and rotation attributes work too.
Optional attributeEffect
data-sa-titleThe accessible name of the frame.
data-sa-aspectThe frame's aspect ratio as W:H; 16:9 when absent.
data-sa-max-widthCaps the frame's width and centres it, for example 780px.
data-sa-loadingeager for a hero above the fold; lazy when absent.
data-sa-hostA different public host for the player; the loader's own host when absent.

window.StreamAgent exposes mount(element), scan() and on(event, handler); the events are below.

Events out to your page

Inside a frame the player posts a small message to the page it sits on for each milestone, so your own tag manager or pixel can hear it. The message type is streamagent:event; the loader also raises a streamagent:event DOM event on the placeholder (it bubbles) and pushes { event: 'streamagent_<name>', streamagent: detail } to window.dataLayer when one exists.

play
pause
progress
complete
start
gate
cta
goal
lead
Event names
FieldValue
eventOne of the names above.
sourceThe internal event the name came from, such as watch_depth_75.
video_idThe video playing.
route_idThe route, when there is one.
element_idThe gate or button, for lead, gate and cta.
percentFor progress and complete: how far the viewer got.
<script>
  window.addEventListener('streamagent:event', function (e) {
    if (e.detail.event === 'lead') { /* a lead was captured in e.detail.video_id */ }
  });
  // Or, without the loader:
  window.addEventListener('message', function (e) {
    if (e.data && e.data.type === 'streamagent:event') { /* e.data.event, e.data.video_id */ }
  });
</script>
Listening on the host page

oEmbed: paste a link, get the player

Every watch page and route page advertises an oEmbed provider, so a builder that discovers oEmbed (WordPress, Ghost, Notion, Squarespace's Embed block, Medium, Confluence) turns a pasted link into the player with no code block, which also sidesteps the plan tiers that gate custom code. The endpoint is /api/oembed on the same host as the link.

GET https://you.streamagent.io/api/oembed?url=https%3A%2F%2Fyou.streamagent.io%2Fwatch%2Fabc123&format=json
{
  "version": "1.0",
  "type": "video",
  "provider_name": "StreamAgent",
  "title": "Pricing walkthrough",
  "html": "<div style=\"…\"><iframe src=\"https://you.streamagent.io/embed/abc123\" …></iframe></div>",
  "width": 780,
  "height": 439,
  "thumbnail_url": "https://you.streamagent.io/api/thumbnail/abc123"
}
The response
  • Links accepted: /watch/{shortId}, /embed/{shortId}, /r/{slug} and /embed/r/{slug}, on the app host, your subdomain or your verified domain.
  • maxwidth and maxheight size the player; format must be json.
  • The html is the iframe alone; consumers strip scripts, and the frame still records the page it sits on.

The companion script

/embed/{shortId}/companion.js is bundled into every video embed. On load it injects the video's structured data into the host page so crawlers that never follow an iframe still see the video and its transcript. It is served with no caching and returns an empty script the moment a video is made private or non-indexable, so turning discoverability off takes effect on the next page load.

Paste notes by platform

The same notes the Share modal shows under "Where are you embedding?".

PlatformNote
ClickFunnelsTwo ways to do this -- pick what matches your skill level. Easy path (recommended for most users): Drag in a Video element. Switch the source to "Custom Embed," then paste the iframe code. Save. Works the same on ClickFunnels 2.0 and Classic. Power path: Drag in a Custom JS/HTML element instead. Click the element, hit "Open Code Editor," paste the full embed (iframe + script). Set Code Type to "3rd Party Embed Code." This is the right choice if you want the attribution script firing.
GoHighLevelIn the funnel/website editor, drag in a Custom JS / HTML element (also labeled "Code" on some plans). Paste the iframe code. Save and publish. For surveys or two-step funnels, paste into the page's "Custom HTML" section under Settings → Tracking if you need it on every step.
WebflowAdd an Embed element (the </> block). Paste the code. Publish. Attribution (UTMs, click IDs, referrer) flows through automatically.
Lovable, v0, Bolt or Replit (AI builder)You paste a prompt, not code. Copy the text below and send it to your builder's chat as-is. It tells the builder to add one script tag for your site and a placeholder where the video goes, and not to swap in an iframe (builders do that on their own, and it loses your attribution). The script then builds the player, carries UTMs, click ids, ad cookies and consent from your page into it, adds the video's search data, and raises streamagent:event on the page for every play, milestone, completion and captured lead.
React or Next.js (hand-coded)A React site never runs the scripts inside pasted HTML, so the universal embed plays but forwards nothing. Use the loader shown here instead: put the one script tag in index.html (or your root layout), then drop the placeholder wherever the video goes; its attributes are valid JSX as written. The loader builds the player, forwards UTMs, click ids, ad cookies and consent from your page, adds the video's structured data, and raises streamagent:event on the element (and a dataLayer push) for every play, milestone, completion and captured lead.
WordPressEasiest: paste the video's direct link on its own line in the editor and WordPress renders the player itself (oEmbed). Or add a Custom HTML block (Gutenberg) or switch to the Text tab (Classic) and paste the code. Plugin coming soon for one-click installs.
SquarespaceHeads up: Requires Core plan or higher: the Basic plan does not render Code blocks. On Core or above, add a Code block (not Embed), paste the whole embed, and make sure the Display Source toggle is OFF (when ON, Squarespace shows the raw code as text instead of rendering it). If your plan strips scripts, paste the iframe alone: the player still records the page it sits on and where the visitor came from.
WixIn Wix Editor or Wix Studio, add an Embed a Widget element (under Add → Embed Code). Paste the whole embed. Wix sandboxes the widget inside its own iframe, so the forwarding script sees Wix's wrapper rather than your page: the player still works and still records your page, but UTMs, click ids and ad cookies only reach it from Custom Code (Settings → Advanced → Custom Code) on a paid plan, or from your own ad-platform parameters on the embed URL.
CarrdDrag in an Embed element. Paste. Carrd auto-sizes the wrapper, so the aspect-ratio div above handles mobile correctly.
FramerInsert an Embed component and paste the code; Framer respects the aspect ratio out of the box. In a code component (React), use the loader from the React option instead: pasted scripts never run there.
NotionHeads up: Notion doesn't accept raw iframe HTML. Use the direct link below -- paste the link, hit Enter, choose "Embed" from the menu. Notion finds the player through oEmbed and embeds it natively.
Kit (formerly ConvertKit)In your landing page editor, add an HTML block (Kit, formerly ConvertKit). Paste. If you're embedding in an email broadcast or sequence, use the direct link below -- email clients strip iframes.
BeehiivOn a Beehiiv landing page, add an HTML Embed block. For email posts, use the direct link below -- most email clients strip iframes and all of them strip scripts.
KajabiEdit your page → add a Custom Code element. Paste the iframe code. Kajabi blocks scripts on most pages, so iframe-only.
TeachableIn a lesson, use the Code block. On marketing pages, use the Custom HTML element. Iframe is the safe bet.
ThinkificAdd an HTML / Liquid block to your lesson or page. Paste the iframe code.
GhostPaste the video's direct link on its own line and Ghost renders the player itself (oEmbed). Or type /html to insert an HTML card and paste the code; Ghost supports both iframes and scripts on most plans.
Custom HTML pagePaste anywhere in your HTML. The iframe is self-contained -- no other setup needed. UTMs, click IDs, your page's ad cookies and its referrer are forwarded into the video automatically.
Email body (Klaviyo, etc)Heads up: Email clients strip both iframes and scripts. Use the direct link below -- it opens a full share page with the player. You can wrap the link around a thumbnail image for a "click-to-play" feel.