Embed Widget Reference
Embed the Koji interview widget with an iframe: URL parameters, koji:* postMessage events, React integration, and prefilled respondents.
Embed Widget Reference
The fastest way to put a Koji interview in front of users is the embed widget: one iframe tag, no API integration required. The widget runs the whole conversation and reports its lifecycle to your page through postMessage events.
Quick start
<iframe
src="https://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60"
width="100%"
height="700"
frameborder="0"
allow="microphone"
style="border: none; border-radius: 12px;">
</iframe>
Include allow="microphone" on the iframe so voice interviews can access the microphone.
The embed URL
https://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60
The URL is https://www.koji.so/embed/{project_id} plus optional query parameters. The page detects embed context automatically: site chrome is hidden and the koji:* postMessage events are enabled whenever the interview runs in an iframe. A legacy embed=1 parameter is still accepted and has no effect.
Query parameters
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
api_key | string | No | - | Project API key. Required for prefilled respondents (external_id lookup) and origin-restricted keys |
theme | `'dark' | 'light'` | No | dark |
rid | string | No | - | Respondent external_id to prefill (alias: external_id). Requires api_key |
postMessage events
The widget posts three events to the parent window:
koji:ready
The widget finished loading its configuration and is ready to show the landing screen.
{
"type": "koji:ready",
"projectId": "3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60",
"projectName": "Churn Interview Study"
}
koji:interview_started
The interview becomes active in the iframe (the AI greeting counts as the first message). Also fires once when an in-progress interview resumes after a reload, including reloads in the brief window after completion while analysis is still persisting.
{
"type": "koji:interview_started",
"conversationId": "9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c",
"mode": "text"
}
koji:interview_completed
The interview finishes (AI-detected or respondent-driven). Does not re-fire when a completed interview is reloaded.
{
"type": "koji:interview_completed",
"conversationId": "9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c",
"messageCount": 18
}
Lifecycle subtleties worth knowing:
koji:interview_startedfires again when an in-progress interview resumes after a page reload.koji:interview_completeddoes not re-fire when a completed interview is reloaded. You get it once, at the moment of completion.- In the short window right after completion, while analysis is still persisting, a reload can emit one more
koji:interview_started. If you track state, treatcompletedas final for a givenconversationIdand ignore laterstartedevents for it.
Listening for events
Events are posted to the parent window. Always verify event.origin against the Koji origin before trusting the payload.
window.addEventListener('message', (event) => {
// Always verify the sender before trusting the payload
if (event.origin !== 'https://www.koji.so') return
switch (event.data?.type) {
case 'koji:ready':
console.log('Widget loaded', event.data.projectId)
break
case 'koji:interview_started':
console.log('Interview started', event.data.conversationId, event.data.mode)
break
case 'koji:interview_completed':
console.log('Interview completed', event.data.messageCount, 'messages')
// e.g. thank the user, close the modal, notify your backend
break
}
})
React integration
import { useEffect } from 'react'
export function KojiInterview({ onStart, onComplete }) {
useEffect(() => {
function handleMessage(event) {
if (event.origin !== 'https://www.koji.so') return
if (event.data?.type === 'koji:interview_started') onStart?.(event.data)
if (event.data?.type === 'koji:interview_completed') onComplete?.(event.data)
}
window.addEventListener('message', handleMessage)
return () => window.removeEventListener('message', handleMessage)
}, [onStart, onComplete])
return (
<iframe
src="https://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60"
width="100%"
height="700"
allow="microphone"
style={{ border: 'none', borderRadius: 12 }}
/>
)
}
Prefilled respondents
To open the widget as a known user, pass their external_id via the rid parameter (alias: external_id) together with your project API key:
https://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60?api_key=pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K&rid=user_8271
The api_key parameter is required for the prefill lookup: the bootstrap endpoint returns prefilledRespondent only when the request is authenticated with a valid project API key. The prefill payload contains id, display_name, external_id, and intake_data only.
Because this key is visible in the page, create a dedicated embed key with an origin allowlist and only the permissions the widget flow needs. See API Key Permissions.
Theming
The theme parameter switches the widget between dark (the default) and light:
https://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60?theme=light
Under the hood: the bootstrap endpoint
On load, the iframe calls a public configuration endpoint:
GET https://www.koji.so/api/v1/embed/{project_id}
Public configuration endpoint the embeddable widget calls on load. You rarely call this directly. The iframe does. - Project name, intake config, and capacity state are public for any published study.
prefilledRespondent(matched via theexternal_idquery param) is returned only when the request is authenticated with a valid project API key; it containsid,display_name,external_id, andintake_dataonly.
FAQ
Do I need an API key to embed?
No. A published study embeds with the plain iframe URL. The api_key parameter is only needed for prefilled respondents and for keys that restrict origins.
Why did koji:interview_started fire twice?
It fires once when the interview becomes active, and once more if an in-progress interview resumes after a reload. Deduplicate on conversationId if you only want the first occurrence.
Can my server hear the completed event?
postMessage only reaches the parent page in the browser. Forward the event to your backend yourself, or poll the read endpoint from a server job. Both patterns are covered in Webhook Setup.
Does the embed support voice interviews?
Yes, as long as the iframe includes allow="microphone" so the widget can access the microphone.
Related Articles
API Authentication
How Koji Headless API authentication works: pk_live_ API keys, permissions, session tokens, and origin allowlists.
Cancel-Flow Exit Interviews: AI Moderation That Saves Churning Users
Replace your single-textarea cancellation survey with an AI-moderated exit interview that probes the real reason people leave and triggers save offers in real time.
Headless API Overview
Manage interviews programmatically with the Koji REST API — start, message, and complete interviews from your own code.
In-App AI Surveys: Embedded Customer Research Inside Your Product
Embed adaptive AI interviews directly into your product to capture in-the-moment customer feedback. A complete guide to in-app AI surveys, triggers, and implementation with Koji.
In-Product Research Recruiting: Recruit Customer Interview Participants From Inside Your App
Stop paying recruiting panels for participants you already have. Learn how to recruit research participants directly from your product using embedded prompts, in-app banners, email triggers, and personalized AI interview links. Faster, cheaper, and more representative than external panels — with zero scheduling friction.
Structured Questions in AI Interviews
Mix quantitative data collection — scales, ratings, multiple choice, ranking — with AI-powered conversational follow-up in a single interview.
User Research API: Embed AI Interviews into Any Product or Workflow
How to use Koji's User Research API to run AI-moderated interviews from your own backend. Covers REST endpoints, the embed widget, webhooks, authentication, rate limits, and headless interview patterns.
Using the Embed Widget
Add a Koji interview to your website using an embeddable iframe with configuration options and event listeners.