---
title: "Embed API Reference"
nav_title: "Embed API"
description: "Embed Perspective interviews programmatically using the SDK, iframe, or JavaScript API."
tags: ["embed conversation", "website widget", "JavaScript SDK", "React SDK", "inline interview", "npm"]
date: "2026-06-02"
nav_order: 5
nav_display: true
---

# Embed API Reference

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](/docs/guide/collect/embed-interviews).

## 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](/docs/build/api-overview#create-a-perspective)).

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:

| Type | Attribute | Element | Description |
|------|-----------|---------|-------------|
| **Widget** | `data-perspective-widget` | `<div>` | Inline embed that renders directly in the page flow. |
| **Popup** | `data-perspective-popup` | `<button>` | Modal overlay centered on screen. |
| **Slider** | `data-perspective-slider` | `<button>` | Side panel that slides in from the edge. |
| **Float** | `data-perspective-float` | `<div>` | Floating chat bubble anchored to the corner. |
| **Fullpage** | `data-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

| Parameter | Type | Description |
|-----------|------|-------------|
| `email` | string | Pre-fill participant email. |
| `name` | string | Pre-fill participant name. |
| `returnUrl` | URL | Redirect 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](/docs/guide/collect/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:

| Token | Description |
|-------|-------------|
| `primary` | Primary accent color (buttons, links). |
| `secondary` | Secondary accent color. |
| `bg` | Background color override. |
| `text` | Text color override. |

## Advanced Attributes

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

| Attribute | Applies To | Description |
|-----------|------------|-------------|
| `data-perspective-no-style` | Popup, Slider, Float | Prevents the SDK from applying default button styling to the trigger or launcher. |
| `data-perspective-disable-close` | Popup, Slider | Hides close controls and disables overlay click / Escape close behavior. |
| `data-perspective-slider-mode` | Slider | Set to `push` when the slider should take its own page column instead of overlaying the page. |
| `data-perspective-launcher-icon` | Float | Sets the launcher icon to `default`, `avatar`, or an image URL. |
| `data-perspective-launcher-style` | Float | Semicolon-separated CSS declarations for the float launcher, such as `width:64px;height:64px;border-radius:50%`. |
| `data-perspective-launcher-class` | Float | Adds CSS classes to the float launcher while keeping SDK classes. |
| `data-perspective-disable-jsonld-attribution` | All SDK embeds | Skips JSON-LD structured data injection. Other SDK attribution signals still remain. |
| `data-perspective-chat` | Float | Legacy 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.

## JavaScript API

For programmatic control, use the global `Perspective` object:

```javascript
// Create an inline widget
const 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 popup
const popup = Perspective.openPopup({
  researchId: "RESEARCH_ID",
  onClose: () => console.log("Closed"),
});

// Open a slider
const slider = Perspective.openSlider({
  researchId: "RESEARCH_ID",
});

// Create a floating chat bubble
const float = Perspective.createFloatBubble({
  researchId: "RESEARCH_ID",
});

// Create a full-viewport embed
const fullpage = Perspective.createFullpage({
  researchId: "RESEARCH_ID",
});

// Clean up without changing persisted open/closed state
widget.unmount();

// Explicitly close and persist closed state for restorable embeds
popup.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

| Callback | Arguments | Description |
|----------|-----------|-------------|
| `onReady` | -- | Embed has loaded and is ready for interaction. |
| `onSubmit` | `data: { researchId: string }` | Participant completed the interview. |
| `onNavigate` | `url: string` | Embed requests a page navigation. If not provided, defaults to `window.location.href` (full page reload). |
| `onClose` | -- | User closed the embed (popup, slider, or float). |
| `onError` | `error: Error` | An error occurred during the embed lifecycle. |
| `onAuth` | `data: { 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.

### Popup

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} />;
}
```

### 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](#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

| Event | Data | Description |
|-------|------|-------------|
| `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. |

Add `allow="microphone"` to the iframe if the interview uses voice mode.

## 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.
