Back to docs
API Reference

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

ParamTypeRequiredDefaultDescription
api_keystringNo-Project API key. Required for prefilled respondents (external_id lookup) and origin-restricted keys
theme`'dark''light'`Nodark
ridstringNo-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_started fires again when an in-progress interview resumes after a page reload.
  • koji:interview_completed does 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, treat completed as final for a given conversationId and ignore later started events 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 the external_id query param) is returned only when the request is authenticated with a valid project API key; it contains id, display_name, external_id, and intake_data only.

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.