{"site":{"name":"Koji","description":"AI-native customer research platform that helps teams conduct, analyze, and synthesize customer interviews at scale.","url":"https://www.koji.so","contentTypes":["blog","documentation"],"lastUpdated":"2026-09-20T15:57:24.200Z"},"content":[{"type":"documentation","id":"658b5225-6556-47c2-bded-7c5659bea401","slug":"embed-widget-reference","title":"Embed Widget Reference","url":"https://www.koji.so/docs/embed-widget-reference","summary":"Reference for embedding the Koji interview widget in any page: the iframe snippet, all query parameters, the three koji:* postMessage events with payloads, event listening with origin checks, a React wrapper, prefilled respondents via rid plus api_key, and theming.","content":"# Embed Widget Reference\n\nThe 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.\n\n---\n\n## Quick start\n\n```html\n<iframe\n  src=\"https://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60\"\n  width=\"100%\"\n  height=\"700\"\n  frameborder=\"0\"\n  allow=\"microphone\"\n  style=\"border: none; border-radius: 12px;\">\n</iframe>\n```\n\nInclude `allow=\"microphone\"` on the iframe so voice interviews can access the microphone.\n\n---\n\n## The embed URL\n\n```\nhttps://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60\n```\n\nThe 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.\n\n---\n\n## Query parameters\n\n| Param | Type | Required | Default | Description |\n|---|---|---|---|---|\n| `api_key` | `string` | No | - | Project API key. Required for prefilled respondents (external_id lookup) and origin-restricted keys |\n| `theme` | `'dark' | 'light'` | No | `dark` | Widget theme |\n| `rid` | `string` | No | - | Respondent external_id to prefill (alias: external_id). Requires api_key |\n\n---\n\n## postMessage events\n\nThe widget posts three events to the parent window:\n\n### `koji:ready`\n\nThe widget finished loading its configuration and is ready to show the landing screen.\n\n```json\n{\n  \"type\": \"koji:ready\",\n  \"projectId\": \"3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60\",\n  \"projectName\": \"Churn Interview Study\"\n}\n```\n\n### `koji:interview_started`\n\nThe 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.\n\n```json\n{\n  \"type\": \"koji:interview_started\",\n  \"conversationId\": \"9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c\",\n  \"mode\": \"text\"\n}\n```\n\n### `koji:interview_completed`\n\nThe interview finishes (AI-detected or respondent-driven). Does not re-fire when a completed interview is reloaded.\n\n```json\n{\n  \"type\": \"koji:interview_completed\",\n  \"conversationId\": \"9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c\",\n  \"messageCount\": 18\n}\n```\n\nLifecycle subtleties worth knowing:\n\n- `koji:interview_started` fires again when an in-progress interview resumes after a page reload.\n- `koji:interview_completed` does not re-fire when a completed interview is reloaded. You get it once, at the moment of completion.\n- 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.\n\n---\n\n## Listening for events\n\nEvents are posted to the parent window. Always verify `event.origin` against the Koji origin before trusting the payload.\n\n```javascript\nwindow.addEventListener('message', (event) => {\n  // Always verify the sender before trusting the payload\n  if (event.origin !== 'https://www.koji.so') return\n\n  switch (event.data?.type) {\n    case 'koji:ready':\n      console.log('Widget loaded', event.data.projectId)\n      break\n    case 'koji:interview_started':\n      console.log('Interview started', event.data.conversationId, event.data.mode)\n      break\n    case 'koji:interview_completed':\n      console.log('Interview completed', event.data.messageCount, 'messages')\n      // e.g. thank the user, close the modal, notify your backend\n      break\n  }\n})\n```\n\n---\n\n## React integration\n\n```jsx\nimport { useEffect } from 'react'\n\nexport function KojiInterview({ onStart, onComplete }) {\n  useEffect(() => {\n    function handleMessage(event) {\n      if (event.origin !== 'https://www.koji.so') return\n      if (event.data?.type === 'koji:interview_started') onStart?.(event.data)\n      if (event.data?.type === 'koji:interview_completed') onComplete?.(event.data)\n    }\n    window.addEventListener('message', handleMessage)\n    return () => window.removeEventListener('message', handleMessage)\n  }, [onStart, onComplete])\n\n  return (\n    <iframe\n      src=\"https://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60\"\n      width=\"100%\"\n      height=\"700\"\n      allow=\"microphone\"\n      style={{ border: 'none', borderRadius: 12 }}\n    />\n  )\n}\n```\n\n---\n\n## Prefilled respondents\n\nTo open the widget as a known user, pass their `external_id` via the `rid` parameter (alias: `external_id`) together with your project API key:\n\n```\nhttps://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60?api_key=pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K&rid=user_8271\n```\n\nThe `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.\n\nBecause 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](/docs/api-permissions).\n\n---\n\n## Theming\n\nThe `theme` parameter switches the widget between `dark` (the default) and `light`:\n\n```\nhttps://www.koji.so/embed/3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60?theme=light\n```\n\n---\n\n## Under the hood: the bootstrap endpoint\n\nOn load, the iframe calls a public configuration endpoint:\n\n```\nGET https://www.koji.so/api/v1/embed/{project_id}\n```\n\nPublic 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.\n- `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.\n\n---\n\n## FAQ\n\n### Do I need an API key to embed?\n\nNo. 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.\n\n### Why did koji:interview_started fire twice?\n\nIt 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.\n\n### Can my server hear the completed event?\n\npostMessage 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](/docs/webhook-setup).\n\n### Does the embed support voice interviews?\n\nYes, as long as the iframe includes `allow=\"microphone\"` so the widget can access the microphone.","category":"API Reference","lastModified":"2026-08-19T16:08:23.017116+00:00","metaTitle":"Embed Widget Reference | Koji Headless API","metaDescription":"Complete reference for the Koji embed widget: iframe snippet, query parameters, koji:ready/started/completed events, theming, and prefilled respondents.","keywords":["embed widget","iframe","postmessage api","embed parameters","widget integration"],"aiSummary":"Reference for embedding the Koji interview widget in any page: the iframe snippet, all query parameters, the three koji:* postMessage events with payloads, event listening with origin checks, a React wrapper, prefilled respondents via rid plus api_key, and theming.","aiPrerequisites":["using-the-embed-widget"],"aiLearningOutcomes":["Embed the interview widget with a single iframe tag","Configure the widget through query parameters","Listen to koji:* postMessage events safely","Prefill respondents with rid and an api_key","Wrap the embed in a React component"],"aiDifficulty":"beginner","aiEstimatedTime":"9 min"}],"pagination":{"total":1,"returned":1,"offset":0}}