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)#
| Name | Type | Description |
|---|---|---|
container* | string | HTMLElement | CSS selector or DOM element to mount the interview iframe into. Throws if the selector matches nothing. |
options.invite* | string | Invite token for the interview session. |
options.baseUrl | string | Origin of the InterviewRelay web app that serves the embed. Also used to validate incoming events β always set this explicitly. |
options.voice | string | Override the interview voice (OpenAI Realtime voice id, e.g. "sage"). |
options.theme | object | Theme 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
}
});
| Name | Type | Description |
|---|---|---|
theme.primary | string | Primary accent color (hex). |
theme.background | string | Widget background color (hex). |
theme.text | string | Primary 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
originmatches your configuredbaseUrland which come from its own iframe. Messages from other origins or frames are ignored, sointerview:*events cannot be spoofed by third parties. - Outgoing: the SDK passes your page's origin (
parentOrigin) to the embed, and the embed targets itspostMessagecalls 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 throwsContainer 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
getUserMediain 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
microphonefor the embed origin.
Events not arriving#
- Set
baseUrlto 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.onfor all instances).
Memory leaks in SPAs#
- Always call
embed.destroy()before removing the container (React:useEffectcleanup, Vue:onBeforeUnmount).
For a complete server-side integration (webhooks on completion, transcript export), see Webhooks and the API Reference.