Semaphor

iFrame

Learn how to use the iframe to embed the self-service analytics experience in your app.

Overview

Zero-Dependency Embeds with iFrame

When you embed Semaphor using an iFrame, you don’t need to bundle any analytics libraries, UI components, or dependencies into your application. Everything: visualization libraries, state management, styling, and interactive features lives fully within the Semaphor frame.

This gives you two major advantages:

  1. No extra dependencies in your codebase Your app stays lightweight. You’re not shipping chart libraries, SDKs, or custom components just to display analytics. Semaphor handles all of that inside the frame.

  2. Analytics stays isolated from your product code There’s no risk of CSS collisions, version conflicts, or package bloat. Semaphor runs in its own sandbox, so your product and your analytics never step on each other.

1. Obtain the Auth Token

Before rendering an iframe, you need to retrieve an Auth Token. This token ensures secure access to your embed session.

const PROJECT_ID = 'your-project-id';
const PROJECT_SECRET = 'your-project-secret';
const END_USER_ID = 'tenant-user-id';
const SEMANTIC_DOMAIN_ACCESS = {
  mode: 'include',
  domains: ['your-semantic-domain-id'],
} as const;
const INITIAL_DASHBOARD = 'your-dashboard-id';

const tokenRequest = {
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  endUserId: END_USER_ID,
  semanticDomainAccess: SEMANTIC_DOMAIN_ACCESS,
  initialDashboardId: INITIAL_DASHBOARD,
};

const TOKEN_URL = 'https://semaphor.cloud/api/v1/token';

const response = await fetch(TOKEN_URL, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(tokenRequest),
});

const authToken = await response.json();

Do not expose projectSecret in client-side code in production. Use a backend service to fetch the token securely.

See the Token API reference for all available options including security policies, user provisioning, and multi-tenancy.

2. Build the Embed URL

Use the token to generate the iframe URL. You can control the experience with query parameters.

const url = `https://semaphor.cloud/embed/${authToken?.accessToken}?showAssistant=true&showControls=true&currentTheme=light`; 

3. Render the Iframe

Render the iframe inside a responsive container.

<iframe src={url} width="100%" height="100%" /> 
src/App.tsx
import { useEffect, useState } from 'react';

const PROJECT_ID = 'your-project-id';
const PROJECT_SECRET = 'your-project-secret';
const END_USER_ID = 'tenant-user-id';
const SEMANTIC_DOMAIN_ACCESS = {
  mode: 'include',
  domains: ['your-semantic-domain-id'],
} as const;
const INITIAL_DASHBOARD = 'your-dashboard-id';

const TOKEN_URL = 'https://semaphor.cloud/api/v1/token';

type AuthToken = {
  accessToken: string;
};

function App() {
  const [authToken, setAuthToken] = useState<AuthToken>();

  const tokenRequest = {
    projectId: PROJECT_ID,
    projectSecret: PROJECT_SECRET,
    endUserId: END_USER_ID,
    semanticDomainAccess: SEMANTIC_DOMAIN_ACCESS,
    initialDashboardId: INITIAL_DASHBOARD,
    tokenExpiry: 60 * 30, // 30 minutes
    // sls: 'corp_a', // This will route the user to the corp_a schema
  };

  useEffect(() => {
    async function fetchToken() {
      try {
        const response = await fetch(TOKEN_URL, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          body: JSON.stringify(tokenRequest),
        });
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const token = await response.json();
        if (token?.accessToken) {
          setAuthToken(token);
        }
      } catch (error) {
        console.error('There was an error!', error);
      }
    }
    fetchToken();
  }, []);

  const url = `https://semaphor.cloud/embed/${authToken?.accessToken}?showAssistant=true&showControls=true&currentTheme=light`; 

  if (!authToken) {
    return <div>Loading...</div>;
  }

  return (
    <div style={{ width: '100%', height: '100vh' }}>
      <iframe src={url} width="100%" height="100%" />
    </div>
  );
}

export default App;

Warn before leaving with unsaved changes

When someone edits an embedded dashboard, the iframe can tell your page whether it has unsaved changes. Use this to confirm before your own Back button navigates away.

1. Add parentOrigin to the token request

Add your page's origin to the token request you already make on your backend. Semaphor sends messages only to this exact origin.

const tokenRequest = {
  projectId: PROJECT_ID,
  projectSecret: PROJECT_SECRET,
  endUserId: END_USER_ID,
  semanticDomainAccess: SEMANTIC_DOMAIN_ACCESS,
  initialDashboardId: INITIAL_DASHBOARD,
  parentOrigin: 'https://app.example.com', 
};

If you omit parentOrigin, the iframe works as before and sends no messages.

2. Listen for the message

Whenever the dirty state changes, the iframe posts a message to your page:

{
  source: 'semaphor-embed',
  type: 'dashboard:dirty-state',
  version: 1,
  dashboardId: 'your-dashboard-id',
  payload: { dirty: true },
}

Register the listener before the iframe loads, and accept only messages that come from the Semaphor origin and from your iframe.

import { useEffect, useRef, useState } from 'react';

const SEMAPHOR_ORIGIN = 'https://semaphor.cloud';

function EmbeddedDashboard({ url }: { url: string }) {
  const iframeRef = useRef<HTMLIFrameElement>(null);
  const [dirty, setDirty] = useState(false);

  useEffect(() => {
    function onMessage(event: MessageEvent) {
      if (event.origin !== SEMAPHOR_ORIGIN) return;
      if (event.source !== iframeRef.current?.contentWindow) return;
      if (event.data?.type !== 'dashboard:dirty-state') return;
      setDirty(event.data.payload.dirty);
    }
    window.addEventListener('message', onMessage);
    return () => window.removeEventListener('message', onMessage);
  }, []);

  function handleBack() {
    if (dirty && !window.confirm('You have unsaved changes. Leave anyway?')) {
      return;
    }
    window.history.back();
  }

  return (
    <>
      <button onClick={handleBack}>Back</button>
      <iframe ref={iframeRef} src={url} width="100%" height="100%" />
    </>
  );
}
<button id="back">Back</button>
<iframe id="dashboard" width="100%" height="100%"></iframe>

<script>
  const SEMAPHOR_ORIGIN = 'https://semaphor.cloud';
  const iframe = document.getElementById('dashboard');
  let dirty = false;

  window.addEventListener('message', (event) => {
    if (event.origin !== SEMAPHOR_ORIGIN) return;
    if (event.source !== iframe.contentWindow) return;
    if (event.data?.type !== 'dashboard:dirty-state') return;
    dirty = event.data.payload.dirty;
  });

  document.getElementById('back').addEventListener('click', () => {
    if (dirty && !window.confirm('You have unsaved changes. Leave anyway?')) {
      return;
    }
    window.history.back();
  });

  // Set the URL after the listener is registered (see "Build the Embed URL").
  iframe.src = url;
</script>

The iframe sends one message per transition:

EventMessage
Editable dashboard finishes loadingdirty: false
First unsaved changedirty: true
Save, discard, or undo back to the saved statedirty: false

Further edits while already dirty send nothing, and a failed save leaves the dashboard dirty. Nothing can be unsaved before the first message arrives, so starting with dirty = false is safe. If you embed more than one dashboard, use dashboardId to tell the messages apart.

parentOrigin must be an exact origin such as https://app.example.com: scheme, host, and port only, with no path, query string, or wildcard. HTTPS is required except for localhost. Always check event.origin and event.source before reading a message, as shown above.


URL Parameters

  • showAssistant: Enable the AI assistant panel in the embed (true/false)
  • showControls: Show header controls for the embed (true/false)
  • currentTheme: Set theme for the embed (light, dark, or system)

Append these as query parameters to the embed URL as needed.

Use signed token config settings when you need to hide individual dashboard controls for a specific customer or session. For example, you can independently hide Dashboard Hub navigation, dashboard sharing, and Manage Groups while leaving Export and Report preferences available. See Feature Visibility.

Key Considerations

  • Security: Always generate tokens server-side; never expose secrets in client code.
  • Sizing: Wrap the iframe in a container with explicit width/height for proper layout.
  • Token expiry: Use tokenExpiry in your token request to control session duration.
  • Dashboard chrome: Use token config settings for per-token visibility of Hub, Share, and Manage Groups controls.

Single Visual Embedding

Instead of embedding a full dashboard, you can embed a single visual -- a specific chart, KPI, or table -- from any dashboard. This is useful when you want to display an individual metric or visualization inline within your application.

Build the Visual URL

The URL includes both the dashboard ID and the visual (card) ID:

const DASHBOARD_ID = 'your-dashboard-id';
const VISUAL_ID = 'your-visual-id';

const url = `https://semaphor.cloud/view/dashboard/${DASHBOARD_ID}/visual/${VISUAL_ID}?token=${authToken?.accessToken}&theme=light`; 

You can find the visual ID by opening the dashboard in Semaphor, clicking on the card, and copying the ID from the card settings.

Render the Visual

Use the same iframe pattern as dashboard embedding:

<div style={{ width: '400px', height: '300px' }}>
  <iframe src={url} width="100%" height="100%" style={{ border: 'none' }} /> // [!code highlight]
</div>

Visual URL Parameters

ParameterTypeDescription
tokenstringAuth token (same token used for dashboard embedding)
themelight | darkVisual theme
filterValuesJSON arrayDashboard-level filter values to apply
controlValuesJSON objectControl values to apply
selectedSheetIdstringSheet containing the visual (required if the visual is not on the first sheet)
cardTitlestringOverride the page title

On this page