InterviewRelay

SDK Integration#

The InterviewRelay JavaScript SDK lets you embed AI-powered interview experiences directly into your website or web application. Participants can complete interviews without leaving your site.

The SDK renders the interview in a sandboxed iframe and bridges its lifecycle (started, progress, completed, error) back to your page via a secured postMessage channel.

Installation#

npm / yarn / pnpm#

# npm
npm install @ai-interview/sdk

# yarn
yarn add @ai-interview/sdk

# pnpm
pnpm add @ai-interview/sdk

Script tag#

A ready-made browser bundle ships with the package as dist/index.global.js. It exposes window.InterviewSDK:

<script src="https://unpkg.com/@ai-interview/sdk@1.2.0/dist/index.global.js"></script>

Pin a specific version in production (as shown above) instead of @latest, so an SDK update can never change your embed behavior unexpectedly.

Quick Start#

Get an interview running in a few lines of code:

<!DOCTYPE html>
<html>
<head>
  <title>Interview</title>
  <script src="https://unpkg.com/@ai-interview/sdk@1.2.0/dist/index.global.js"></script>
</head>
<body>
  <div id="interview-container"></div>

  <script>
    const embed = InterviewSDK.mount('#interview-container', {
      invite: 'YOUR_INVITE_TOKEN',
      baseUrl: 'https://app.yourdomain.com' // your InterviewRelay web app origin
    });

    embed.on('completed', (payload) => {
      console.log('Interview completed!', payload.sessionId);
    });

    embed.on('error', (payload) => {
      console.error('Interview error:', payload.message);
    });
  </script>
</body>
</html>

For the module version:

import InterviewSDK from '@ai-interview/sdk';

const embed = InterviewSDK.mount('#interview-container', {
  invite: 'YOUR_INVITE_TOKEN',
  baseUrl: 'https://app.yourdomain.com'
});

embed.on('completed', (payload) => {
  console.log('Interview completed!', payload.sessionId);
});

Invite tokens come from the API: create a campaign and send invites, generate a share link, or use the Quickstart endpoint to set everything up in one call.

Configuration#

InterviewSDK.mount(container, options)#

NameTypeDescription
container*string | HTMLElementCSS selector or DOM element to mount the interview iframe into. Throws if the selector matches nothing.
options.invite*stringInvite token for the interview session.
options.baseUrlstringOrigin of the InterviewRelay web app that serves the embed. Also used to validate incoming events β€” always set this explicitly.
options.voicestringOverride the interview voice (OpenAI Realtime voice id, e.g. "sage").
options.themeobjectTheme colors applied inside the embed (see below).

Returns an InterviewEmbed instance with on, off, and destroy methods.

Theme#

Three colors can be customized. They are passed to the embed as URL parameters and applied as CSS custom properties inside the iframe:

const embed = InterviewSDK.mount('#interview-container', {
  invite: 'YOUR_INVITE_TOKEN',
  baseUrl: 'https://app.yourdomain.com',
  theme: {
    primary: '#146EF5',     // accent color (buttons, highlights)
    background: '#0f172a',  // widget background
    text: '#f1f5f9'         // primary text color
  }
});
NameTypeDescription
theme.primarystringPrimary accent color (hex).
theme.backgroundstringWidget background color (hex).
theme.textstringPrimary text color (hex).

Locale

The interview language is determined by the invite (set when the invite or share link is created) β€” it cannot be overridden client-side. This guarantees participants see consent text in the language the study was designed for.

Events#

Register handlers with embed.on(event, handler); remove them with embed.off(event, handler).

embed.on('start', () => console.log('Interview started'));
embed.on('progress', ({ status }) => console.log('Status:', status));
embed.on('completed', ({ sessionId }) => console.log('Done:', sessionId));
embed.on('error', ({ message }) => console.error('Error:', message));

Event Reference#

| Event | Payload | Description | |-------|---------|-------------| | start | {} | Participant started the interview | | progress | { status: "in_progress" } | The interview advanced | | pause | {} | Interview paused | | resume | {} | Interview resumed | | completed | { sessionId: string } | Interview completed | | error | { message: string } | An error occurred (invalid/expired invite, session ended, runtime error) |

For rich completion data (duration, transcript, billing), use the completed event's sessionId with the API, or subscribe to the interview.completed webhook server-side.

Methods#

embed.on(event, handler) / embed.off(event, handler)#

Add or remove an event listener (see Events above).

embed.destroy()#

Unmount the iframe, remove the message listener, and clean up all handlers:

embed.destroy();

Always call destroy() when removing the interview widget from the page (e.g., in a SPA when navigating away). This detaches the global message listener and prevents leaks.

Static helpers#

InterviewSDK also tracks mounted instances:

InterviewSDK.mount('#a', { invite: '…' });   // returns the embed, also stored under '#a'
InterviewSDK.on('completed', handler);        // register on all current instances
InterviewSDK.off('completed', handler);       // remove from all current instances
InterviewSDK.destroy('#a');                   // destroy the instance mounted at '#a'
InterviewSDK.destroyAll();                    // destroy every instance

How It Works#

mount() creates an iframe pointing at the InterviewRelay embed page:

{baseUrl}/embed?invite={token}&voice={voice}&theme[primary]={hex}&theme[background]={hex}&theme[text]={hex}&parentOrigin={yourOrigin}
  • The iframe is created with allow="microphone; autoplay" β€” required for voice interviews.
  • The iframe fills the container's width and has a default height of 700px.
  • Lifecycle events flow back to your page via postMessage.

Security#

Message origin validation (SDK v1.2+)#

The event channel is locked down on both sides:

  • Incoming: the SDK only accepts messages whose origin matches your configured baseUrl and which come from its own iframe. Messages from other origins or frames are ignored, so interview:* events cannot be spoofed by third parties.
  • Outgoing: the SDK passes your page's origin (parentOrigin) to the embed, and the embed targets its postMessage calls at that origin specifically instead of "*".

Because baseUrl doubles as the trust anchor for incoming events, always set it explicitly to your InterviewRelay web app origin.

Token security#

  • Never put API keys in client-side code. Invite tokens are safe to use in the browser; API keys (ir_live_…) are not.
  • Invite tokens are single-participant: once the interview is completed, the token cannot be used to start a new one.
  • Share links (/join/…) generate a fresh invite per participant, with optional caps and expiry.

Content Security Policy (CSP)#

If your site uses CSP headers, allow the InterviewRelay app to be framed:

frame-src https://app.yourdomain.com;
script-src 'self' https://unpkg.com;   # only if you load the SDK via script tag

Microphone access from a cross-origin iframe also requires a Permissions-Policy that does not deny microphone for that origin.

Privacy#

The embed does not show a cookie banner and does not load analytics β€” consent inside the widget is limited to the interview recording consent that is part of the interview flow itself.

Framework Integration#

React#

import { useEffect, useRef } from 'react';
import InterviewSDK from '@ai-interview/sdk';

function InterviewWidget({ invite, onComplete }) {
  const containerRef = useRef(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const embed = InterviewSDK.mount(containerRef.current, {
      invite,
      baseUrl: 'https://app.yourdomain.com',
      theme: { primary: '#146EF5' }
    });

    embed.on('completed', (payload) => onComplete?.(payload));
    embed.on('error', (payload) => console.error('Interview error:', payload.message));

    return () => embed.destroy();
  }, [invite, onComplete]);

  return <div ref={containerRef} style={{ width: '100%' }} />;
}

export default InterviewWidget;

Vue#

<template>
  <div ref="containerRef" class="interview-container" />
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import InterviewSDK from '@ai-interview/sdk';

const props = defineProps({
  invite: { type: String, required: true }
});

const emit = defineEmits(['completed', 'error']);

const containerRef = ref(null);
let embed = null;

onMounted(() => {
  embed = InterviewSDK.mount(containerRef.value, {
    invite: props.invite,
    baseUrl: 'https://app.yourdomain.com'
  });
  embed.on('completed', (payload) => emit('completed', payload));
  embed.on('error', (payload) => emit('error', payload));
});

onBeforeUnmount(() => {
  embed?.destroy();
});
</script>

<style scoped>
.interview-container {
  width: 100%;
}
</style>

Generating invites server-side#

Create invites on your backend with an API key, then pass the token to the SDK:

// Server-side (Node.js) β€” one-call setup via Quickstart
const response = await fetch('https://api.yourdomain.com/api/quickstart', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ir_live_YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    title: 'Churn interview',
    locale: 'en',
    questions: [
      { id: 'q1', type: 'open', text: 'What made you cancel?' }
    ],
    distribution: { mode: 'share_link', maxRespondents: 100 }
  })
});

const { share_link } = await response.json();
// Let each participant join via share_link.url, or create per-participant
// invites with POST /api/campaigns/{id}/invites β€” see the API reference.

Sizing & Styling#

The iframe fills 100% of the container's width, has a default height of 700px, no border, and a 12px border radius. Control the widget's footprint through the container:

#interview-container {
  max-width: 720px;
  margin: 0 auto;
}

Colors inside the widget are controlled via the theme option. There are no overridable CSS classes inside the iframe β€” cross-origin isolation prevents host CSS from leaking in (by design).

Script Templates#

The SDK package also exports ready-made interview script templates you can use server-side when creating scripts via the API:

import { getTemplateLibrary, getTemplate, TEMPLATE_EXIT_INTERVIEW } from '@ai-interview/sdk';

const all = getTemplateLibrary();
// [{ script, metadata: { id, name, description, category, estimatedMinutes, icon } }, …]

const exitInterview = getTemplate('exit-interview');

Available templates: customer discovery, user research, exit interview, market validation, phone screen, and product feedback (NPS). Each includes a full script (persona, tone, 8–12 questions with probing logic) that you can adapt and publish via POST /api/scripts.

Browser Support#

The interview itself uses WebRTC and the Web Audio API inside the iframe:

| Browser | Minimum Version | Notes | |---------|----------------|-------| | Chrome / Edge | 80+ | Full support | | Firefox | 78+ | Full support | | Safari | 14+ | Full support | | iOS Safari | 14+ | Requires a user gesture before audio can start |

Microphone access requires a secure context: your host page must be served over HTTPS (or localhost during development). Internet Explorer is not supported.

Troubleshooting#

Widget not rendering#

  • Ensure the container element exists before calling mount() β€” the SDK throws Container element not found: <selector> otherwise.
  • Check the browser console for errors from the embed page.
  • Verify the invite token is valid: open {baseUrl}/embed?invite={token} directly in a browser tab.

Microphone not working#

  • The host page must be HTTPS (or localhost) β€” browsers block getUserMedia in insecure contexts.
  • Don't strip the iframe's allow="microphone; autoplay" attribute if you post-process the DOM.
  • Check that no Permissions-Policy header on your site denies microphone for the embed origin.

Events not arriving#

  • Set baseUrl to exactly the origin serving the embed (scheme + host + port). Since v1.2 events from any other origin are ignored.
  • Make sure you registered handlers on the embed instance that created the iframe (or use InterviewSDK.on for all instances).

Memory leaks in SPAs#

  • Always call embed.destroy() before removing the container (React: useEffect cleanup, Vue: onBeforeUnmount).

For a complete server-side integration (webhooks on completion, transcript export), see Webhooks and the API Reference.