Embed API Reference

Updated: June 2, 2026

The Perspective Embed SDK lets you drop interviews into any web page with a script tag and a data attribute. For more control, use the JavaScript API to create and manage embed instances programmatically.

For the user-facing setup guide (choosing embed types, previewing, copying snippets from the dashboard), see Embed Interviews on Your Website or App.

Quick Start

Add the SDK script and a trigger element to your page:

html
<div data-perspective-widget="RESEARCH_ID"></div><script src="https://getperspective.ai/v1/perspective.js"></script>

Replace RESEARCH_ID with the ID of your perspective (visible in the dashboard URL or returned by the Create Perspective API).

The script loads asynchronously and initializes all trigger elements on the page automatically.

Embed Types

Each embed type uses a different data attribute on the trigger element:

TypeAttributeElementDescription
Widgetdata-perspective-widget<div>Inline embed that renders directly in the page flow.
Popupdata-perspective-popup<button>Modal overlay centered on screen.
Sliderdata-perspective-slider<button>Side panel that slides in from the edge.
Floatdata-perspective-float<div>Floating chat bubble anchored to the corner.
Fullpagedata-perspective-fullpage<div>Full-viewport interview experience.

Button-based types (Popup, Slider) require a <button> element. The user clicks it to open the interview.

Examples

Inline widget:

html
<div data-perspective-widget="RESEARCH_ID"></div>

Popup triggered by a button:

html
<button data-perspective-popup="RESEARCH_ID">  Share your feedback</button>

Slider triggered by a button:

html
<button data-perspective-slider="RESEARCH_ID">  Open interview</button>

Floating chat bubble:

html
<div data-perspective-float="RESEARCH_ID"></div>

URL Parameters

Pass parameters via the data-perspective-params attribute as comma-separated key=value pairs:

html
<div  data-perspective-widget="RESEARCH_ID"  data-perspective-params="source=homepage,user_id=123,plan=pro"></div>

Built-In Parameters

ParameterTypeDescription
emailstringPre-fill participant email.
namestringPre-fill participant name.
returnUrlURLRedirect URL after interview completion.
voice"0"Set to "0" to disable voice mode (text-only).
scroll"0"Set to "0" to disable scroll mode.
hideProgress"true"Hide the progress bar.
hideGreeting"true"Hide the welcome greeting at the start.
hideBranding"true"Hide the branding header (logo and company name).
enableFullScreen"true" / "false"Show or hide the fullscreen button. Defaults to "true".

Any unrecognized parameters are treated as custom key-value pairs. They are passed through to webhooks and data exports, making them useful for attribution and tracking (e.g., campaign=summer2024).

The SDK also forwards search parameters from the parent page URL into the embed iframe, except for SDK-owned parameters such as embed, embed_type, theme, and brand.* color keys. Parameters passed explicitly through data-perspective-params or the JavaScript params option override forwarded parent-page values. For more on how this becomes participant metadata, see Track Participant Context.

Theming

Color Mode

Force a color mode with data-perspective-theme:

html
<div  data-perspective-widget="RESEARCH_ID"  data-perspective-theme="dark"></div>

Accepted values: dark, light, system (default, follows OS preference).

Brand Colors

Override brand colors with data-perspective-brand (light mode) and data-perspective-brand-dark (dark mode):

html
<div  data-perspective-widget="RESEARCH_ID"  data-perspective-brand="primary=#7c3aed,bg=#ffffff"  data-perspective-brand-dark="primary=#a78bfa,bg=#1a1a1a"></div>

Available color tokens:

TokenDescription
primaryPrimary accent color — applied to buttons, the progress bar, links, the voice mic, and focus rings.
bgBackground behind the interview card. Only visible when no background scene is set; otherwise the scene covers it.

Other tokens (such as secondary and text) are not applied and are ignored.

Widget Appearance

The inline Widget renders as a centered, framed card by default — bordered, rounded, with a soft shadow and a themed background — so it looks finished without any custom CSS. The widget adapts to how you size its container:

  • Drop it in without sizing and it renders as a centered card (default max width 480px, min height 640px).
  • Size the container yourself — an explicit height, height: 100%, or a flex/grid cell — and it fills that space edge to edge with no framing.

Override the auto-detection and individual card styles with data-perspective-frame, a comma-separated list of key=value pairs:

html
<div  data-perspective-widget="RESEARCH_ID"  data-perspective-frame="layout=card,maxWidth=720px,radius=4px,shadow=none,bg=#ffffff"></div>
KeyDescriptionDefault
layoutcard to always frame, fill to always fill edge to edge. Omit to auto-detect from the container's sizing.auto
maxWidthCard max width, e.g. 720px or none. Card layout only.480px
minHeightMinimum height, e.g. 720px. Applies in both layouts.640px card / 500px fill
radiusCard border-radius, e.g. 4px.theme radius
borderCard border shorthand, e.g. 1px solid #ddd or none.themed 1px border
shadowCard box-shadow, e.g. none.soft shadow
bgCard background, e.g. #ffffff.theme background

Every key except layout maps to a --perspective-widget-* CSS custom property (--perspective-widget-max-width, --perspective-widget-min-height, --perspective-widget-radius, --perspective-widget-border, --perspective-widget-shadow, --perspective-widget-bg), so you can also set these from your own stylesheet. layout has no CSS variable — it only controls the card-vs-fill decision. Inline data-perspective-frame values take precedence over stylesheet variables, which take precedence over the built-in defaults.

With the JavaScript API or NPM SDK, pass these as a frame object. The field names match the attribute keys above, except the background field is background (the shorter bg is only an alias accepted inside the data-perspective-frame string):

javascript
Perspective.createWidget(container, {  researchId: "RESEARCH_ID",  frame: { layout: "card", maxWidth: "720px", shadow: "none", background: "#ffffff" },});

In React, pass the frame prop to <Widget> with the same fields:

tsx
<Widget researchId="RESEARCH_ID" frame={{ layout: "card", maxWidth: "720px" }} />

Frame options apply to the inline Widget only. Popup, Slider, Float, and Fullpage embeds manage their own layout.

Advanced Attributes

Use these attributes when you need more control over SDK behavior:

AttributeApplies ToDescription
data-perspective-frameWidgetControls the inline widget's card vs. fill layout and card styling (max width, min height, border, radius, shadow, background). See Widget Appearance.
data-perspective-no-stylePopup, Slider, FloatPrevents the SDK from applying default button styling to the trigger or launcher.
data-perspective-disable-closePopup, SliderHides close controls and disables overlay click / Escape close behavior.
data-perspective-slider-modeSliderSet to push when the slider should take its own page column instead of overlaying the page.
data-perspective-launcher-iconFloatSets the launcher icon to default, avatar, or an image URL.
data-perspective-launcher-styleFloatSemicolon-separated CSS declarations for the float launcher, such as width:64px;height:64px;border-radius:50%.
data-perspective-launcher-classFloatAdds CSS classes to the float launcher while keeping SDK classes.
data-perspective-teaserFloatSet to "false" to disable the welcome teaser (bubble, chime, and notification dot). See Welcome Teaser.
data-perspective-teaser-delayFloatMilliseconds after mount before the teaser bubble appears. Defaults to 3000.
data-perspective-teaser-soundFloatSet to "false" to mute the teaser chime while keeping the bubble.
data-perspective-disable-jsonld-attributionAll SDK embedsSkips JSON-LD structured data injection. Other SDK attribution signals still remain.
data-perspective-chatFloatLegacy alias for data-perspective-float. Prefer data-perspective-float for new embeds.

Slider Modes

Sliders support two display modes:

  • Overlay - the default. The slider floats above the page with a backdrop, and outside clicks can close it.
  • Push - the slider occupies its own column and shifts the page content aside. The rest of the page stays interactive, and outside clicks do not close the slider.

Push mode falls back to overlay on narrow screens where there is not enough room to keep the page usable.

For script embeds:

html
<button  data-perspective-slider="RESEARCH_ID"  data-perspective-slider-mode="push">  Ask a question</button><script src="https://getperspective.ai/v1/perspective.js"></script>

For the JavaScript API:

javascript
Perspective.openSlider({  researchId: "RESEARCH_ID",  sliderMode: "push",});

For React:

tsx
const { open } = useSlider({  researchId: "RESEARCH_ID",  sliderMode: "push",});

Auto-Open (Popup Only)

Trigger a popup automatically without a user click:

html
<div  data-perspective-popup="RESEARCH_ID"  data-perspective-auto-open="timeout:5000"  data-perspective-show-once="session"  style="display:none"></div>

Auto-open triggers:

  • timeout:N -- Opens the popup after N milliseconds.
  • exit-intent -- Opens when the user moves their cursor toward the browser chrome (desktop only).

Show-once options:

  • session -- Show once per browser session.
  • visitor -- Show once per visitor (persisted in localStorage).
  • false -- Show every time.

Welcome Teaser (Float Only)

Float embeds can show a welcome teaser — a message bubble that appears above the launcher button after a short delay, along with a chime sound and a notification dot. The teaser text comes from the interview's welcomeMessage.

Control the teaser with a teaser object through the JavaScript API or NPM SDK:

javascript
Perspective.createFloatBubble({  researchId: "RESEARCH_ID",  welcomeMessage: "Have a question? I'm here to help.",  teaser: {    enabled: true, // false disables the whole welcome sequence (bubble, chime, dot)    delay: 5000, // ms after mount before the teaser appears (default: 3000)    sound: false, // mute the chime (default: true)  },});
  • enabled: false turns off the teaser bubble, the chime sound, and the notification dot entirely.
  • delay sets when the teaser bubble appears, in milliseconds.
  • sound: false mutes the chime while keeping the teaser bubble.

For script embeds, set the teaser data attributes on the trigger element:

html
<!-- Delay the teaser to 5s and mute the chime --><div  data-perspective-float="RESEARCH_ID"  data-perspective-teaser-delay="5000"  data-perspective-teaser-sound="false"></div>
<!-- Disable the welcome sequence entirely --><div  data-perspective-float="RESEARCH_ID"  data-perspective-teaser="false"></div>

In React, pass the teaser prop to <FloatBubble> (or the useFloatBubble hook):

tsx
<FloatBubble  researchId="RESEARCH_ID"  welcomeMessage="Have a question? I'm here to help."  teaser={{ delay: 10000, sound: false }}/>

JavaScript API

For programmatic control, use the global Perspective object:

javascript
// Create an inline widgetconst container = document.querySelector("#perspective-widget");const widget = Perspective.createWidget(container, {  researchId: "RESEARCH_ID",  params: { source: "app", user_id: "123" },  onReady: () => console.log("Loaded"),  onSubmit: () => console.log("Completed"),  onNavigate: (url) => window.location.href = url,  onClose: () => console.log("Closed"),  onError: (err) => console.error(err),});
// Open a popupconst popup = Perspective.openPopup({  researchId: "RESEARCH_ID",  onClose: () => console.log("Closed"),});
// Open a sliderconst slider = Perspective.openSlider({  researchId: "RESEARCH_ID",});
// Create a floating chat bubbleconst float = Perspective.createFloatBubble({  researchId: "RESEARCH_ID",});
// Create a full-viewport embedconst fullpage = Perspective.createFullpage({  researchId: "RESEARCH_ID",});
// Clean up without changing persisted open/closed statewidget.unmount();
// Explicitly close and persist closed state for restorable embedspopup.destroy();

All creation functions return a handle with unmount(), destroy(), and update() methods. Use unmount() for framework cleanup or route changes. Use destroy() when you intentionally want to close the embed; popup, slider, and float embeds can persist that closed state. Float handles also expose open(), close(), toggle(), and isOpen.

The browser global also exposes Perspective.mount(containerOrSelector, config) for selector-based mounting, Perspective.init(config) for non-widget embed types, Perspective.destroy(researchId), Perspective.destroyAll(), Perspective.autoInit(), Perspective.configure(config), and Perspective.getConfig().

Callbacks

CallbackArgumentsDescription
onReady--Embed has loaded and is ready for interaction.
onSubmitdata: { researchId: string }Participant completed the interview.
onNavigateurl: stringEmbed requests a page navigation. If not provided, defaults to window.location.href (full page reload).
onClose--User closed the embed (popup, slider, or float).
onErrorerror: ErrorAn error occurred during the embed lifecycle.
onAuthdata: { researchId: string; token: string }Embed auth completed and returned a token for custom storage.

NPM JavaScript SDK

For TypeScript or bundler-based apps that do not need React components, install @perspective-ai/sdk:

bash
npm install @perspective-ai/sdk
ts
import {  createWidget,  openPopup,  openSlider,  createFloatBubble,  createFullpage,  configure,  fetchEmbedConfig,} from "@perspective-ai/sdk";
configure({ host: "https://getperspective.ai" });
const container = document.querySelector<HTMLElement>("#perspective-widget");const widget = createWidget(container, {  researchId: "RESEARCH_ID",  params: { source: "app", user_id: "123" },});
const popup = openPopup({ researchId: "RESEARCH_ID" });const slider = openSlider({ researchId: "RESEARCH_ID" });const float = createFloatBubble({ researchId: "RESEARCH_ID" });const fullpage = createFullpage({ researchId: "RESEARCH_ID" });
const config = await fetchEmbedConfig("RESEARCH_ID");
widget.unmount();popup.destroy();float.open();float.close();

createWidget accepts an HTMLElement | null. If you want selector-based mounting, use the browser global Perspective.mount("#selector", config) or resolve the element before calling the NPM function.

React SDK

For React and Next.js apps, use @perspective-ai/sdk-react instead of the script tag. It provides typed components and hooks with full lifecycle control.

Installation

bash
npm install @perspective-ai/sdk-react

Widget (Inline Embed)

Renders the interview directly inside a container element:

tsx
import { Widget } from "@perspective-ai/sdk-react";
function FeedbackWidget() {  return (    <Widget      researchId="RESEARCH_ID"      params={{ source: "app", user_id: "123" }}      onReady={() => console.log("Loaded")}      onSubmit={() => console.log("Completed")}    />  );}

The Widget component accepts all standard div props (className, style, etc.) for sizing and layout, plus a frame prop to control its card appearance — see Widget Appearance.

Use the usePopup hook for modal overlays:

tsx
import { usePopup } from "@perspective-ai/sdk-react";
function FeedbackButton() {  const { open, isOpen } = usePopup({    researchId: "RESEARCH_ID",    onSubmit: () => console.log("Completed"),  });
  return <button onClick={open}>Give Feedback</button>;}

Slider

Use the useSlider hook for a side panel that slides in from the edge:

tsx
import { useSlider } from "@perspective-ai/sdk-react";
function SliderTrigger() {  const { open } = useSlider({ researchId: "RESEARCH_ID" });  return <button onClick={open}>Open Survey</button>;}

Float Bubble

A floating chat bubble anchored to the corner of the page:

tsx
import { FloatBubble } from "@perspective-ai/sdk-react";
function App() {  return <FloatBubble researchId="RESEARCH_ID" onSubmit={handleSubmit} />;}

Pass a welcomeMessage to show a teaser bubble above the launcher, and a teaser prop to control its timing and chime — see Welcome Teaser.

Fullpage

Takes over the entire viewport:

tsx
import { Fullpage } from "@perspective-ai/sdk-react";
function InterviewPage() {  return <Fullpage researchId="RESEARCH_ID" />;}

Auto-Open

Trigger a popup automatically after a delay or on exit intent:

tsx
import { useAutoOpen } from "@perspective-ai/sdk-react";
function AutoSurvey() {  useAutoOpen({    researchId: "RESEARCH_ID",    trigger: { type: "timeout", delay: 5000 },    showOnce: "session",  });  return null;}

React Callbacks

All components and hooks accept the same callback props as the JavaScript API: onReady, onSubmit, onNavigate, onClose, and onError. See the Callbacks table above for details.

React helpers also include useFloatBubble, useEmbedConfig, useThemeSync, and DiscoveryMetadata for headless float control, config fetching, theme synchronization, and server-rendered discovery metadata.

Direct Iframe Embed

If you cannot use the SDK (e.g., in environments that restrict third-party scripts), embed the interview directly in an iframe:

html
<iframe  id="perspective-iframe"  src="https://getperspective.ai/interview/RESEARCH_ID?email=user@example.com&returnUrl=https://example.com/thanks"  style="width: 100%; height: 600px; border: none;"  allow="microphone"></iframe>

When using a direct iframe, you must add a postMessage listener to handle navigation and lifecycle events:

javascript
var iframe = document.getElementById("perspective-iframe");var allowedOrigin = new URL(iframe.src).origin;
window.addEventListener("message", (event) => {  if (event.origin !== allowedOrigin) return;  if (event.source !== iframe.contentWindow) return;  if (!event.data?.type?.startsWith("perspective:")) return;
  switch (event.data.type) {    case "perspective:ready":      // Embed loaded and ready      break;    case "perspective:submit":      // Interview completed      break;    case "perspective:redirect":      // Navigate to the URL provided by the embed      window.location.href = event.data.url;      break;    case "perspective:close":      // User closed the embed      break;    case "perspective:resize":      // Adjust iframe height: event.data.height      iframe.style.height = event.data.height + "px";      break;    case "perspective:error":      // Handle error: event.data.error      break;  }});

PostMessage Events

EventDataDescription
perspective:ready--Embed loaded and ready.
perspective:submit--Interview completed.
perspective:redirect{ url }Embed wants to navigate. You must handle this.
perspective:close--User closed the embed.
perspective:resize{ height }Embed content height changed.
perspective:error{ error }An error occurred.

Microphone access in nested iframes

For a Perspective iframe you embed directly, allow="microphone" is all voice mode needs.

It gets more involved when that iframe sits inside another iframe (common with page builders like Wix or Squarespace, or your own wrapper): Permissions Policy only delegates the microphone into a frame when every ancestor iframe allows it. Because the Perspective iframe is a different origin than the wrapper, allow="microphone" is not sufficient on the wrapper — the shorthand grants the microphone only to the wrapper's own origin. Set the wrapper to allow="microphone *" (or allow="microphone https://getperspective.ai" to scope it). The Perspective iframe keeps its own allow="microphone", so once the wrapper delegates across origins, voice mode works.

Best Practices

  • Load the script once: If you have multiple embeds on the same page, include the SDK script tag only once. It discovers all trigger elements automatically.
  • Use the SDK over iframes: The SDK handles responsive sizing, navigation, theming, and lifecycle events. Direct iframes require you to manage all of this yourself.
  • Pass user identity via params: If your users are already authenticated, pass email and name so participants skip the identification step.
  • Prefer onNavigate over default behavior: In single-page apps, handle onNavigate to use your router instead of a full page reload.