{"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-20T23:47:12.045Z"},"content":[{"type":"documentation","id":"9b42f662-976d-40ec-b4b4-496f40cb2a29","slug":"webhook-setup","title":"Webhook Setup","url":"https://www.koji.so/docs/webhook-setup","summary":"Honest guide to completion notifications in Koji: native outbound webhooks do not exist yet. Documents the three working patterns available today: polling GET /interviews/{id} from a server job, forwarding the koji:interview_completed embed event to your backend, and schedule-driven automation platforms.","content":"# Webhook Setup\n\nLet's be direct up front: **Koji does not have native outbound webhooks yet.** There is no place to register a webhook URL, no signing secret, and no webhook event payloads. Native webhooks are on the roadmap. Until they ship, this guide covers the three patterns that get you the same outcome today, reliably.\n\n---\n\n## What you can build today\n\n| Pattern | Where it runs | Best for |\n|---|---|---|\n| Poll the read endpoint | Your server | Reliable completion detection and analysis retrieval |\n| Forward the embed completed event | The respondent's browser | Instant in-page reactions when you use the embed widget |\n| Automation platforms | Zapier, n8n, Make | No-code pipelines on a schedule |\n\n---\n\n## Option 1: Poll the read endpoint\n\nThe read endpoint returns the interview's status, transcript, stats, and (once ready) the AI analysis:\n\n```bash\ncurl https://www.koji.so/api/v1/interviews/9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c \\\n  -H \"Authorization: Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K\"\n```\n\nReturns the full transcript, respondent info, stats, and (once ready) the AI analysis. Server-to-server friendly. No session token required.\n\nThe full loop has four steps:\n\n1. When you call `POST /interviews/start`, store the returned `interview_id` (plus your own `external_id`) in a pending table.\n2. A scheduled job fetches each pending id every minute.\n3. When `status` is `completed` and `analysis` is present, run your handler and remove the id from the table.\n4. Expire ids after a reasonable window so the poller does not grow unbounded.\n\nStep 2 and 3 in code:\n\n```javascript\n// Runs on your server every minute (cron job, worker, or scheduled function).\n// pendingIds holds interview ids you stored when you called the start endpoint.\nconst API_KEY = 'pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K'\n\nasync function checkPending(pendingIds) {\n  for (const id of pendingIds) {\n    const res = await fetch(`https://www.koji.so/api/v1/interviews/${id}`, {\n      headers: { 'Authorization': `Bearer ${API_KEY}` },\n    })\n    if (!res.ok) continue\n\n    const interview = await res.json()\n    if (interview.status === 'completed' && interview.analysis) {\n      await handleCompleted(interview) // your \"webhook handler\"\n      // Remove id from your pending store so it is not processed twice\n    }\n  }\n}\n```\n\nTwo details make this robust:\n\n- `analysis` is populated only after the interview is completed and the background analysis has finished; otherwise it is `null`. Waiting for `status === 'completed' && analysis` gives you the fully processed result in one pass.\n- The key needs the `interview:read` permission and no session token, so a single server-side key covers the whole worker. See [API Key Permissions](/docs/api-permissions).\n\nMind your rate limit: each key allows 60 requests per minute by default, so a poller checking large batches should spread requests or batch by minute. See [Rate Limits and CORS](/docs/rate-limits-and-cors).\n\n---\n\n## Option 2: Forward the embed completed event\n\nIf your interviews run in the embed widget, the browser already knows the moment an interview finishes. The widget posts this event to the parent page:\n\n```json\n{\n  \"type\": \"koji:interview_completed\",\n  \"conversationId\": \"9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c\",\n  \"messageCount\": 18\n}\n```\n\nThe interview finishes (AI-detected or respondent-driven). Does not re-fire when a completed interview is reloaded. Forward it to your backend and you have a webhook-shaped signal:\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  if (event.data?.type !== 'koji:interview_completed') return\n\n  // Forward the completion signal to your own backend\n  fetch('/api/koji/interview-completed', {\n    method: 'POST',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify(event.data),\n  })\n})\n```\n\nTreat this as a fast path, not the source of truth: it only fires while the respondent's tab is open, and it does not re-fire if a completed interview is reloaded. Pair it with Option 1 polling for interviews whose event never arrived. Full event reference in [Embed Widget Reference](/docs/embed-widget-reference).\n\n---\n\n## Option 3: Automation platforms\n\nZapier, n8n, and Make can all drive the REST API on a schedule with their generic HTTP modules:\n\n1. A schedule trigger runs every few minutes.\n2. An HTTP step calls `GET https://www.koji.so/api/v1/interviews/{id}` for each pending id with your `Authorization: Bearer` header.\n3. A filter passes only interviews where `status` is `completed` and `analysis` is present.\n4. Downstream steps route the result: Slack message, CRM update, spreadsheet row.\n\nThis is Option 1 without writing code. The same rate limit and permission notes apply.\n\n---\n\n## Native webhooks: coming soon\n\nOutbound webhooks are planned. When they ship, they will replace the polling loop, not the rest of your pipeline: keep your completion handling in one function (like `handleCompleted` above) and the switch will be a one-line change from poller to webhook receiver.\n\n---\n\n## FAQ\n\n### Where do I register my webhook URL?\n\nNowhere yet. Koji has no native outbound webhooks today. Use one of the three patterns above.\n\n### How often should I poll?\n\nEvery minute is a good default. With the default limit of 60 requests per minute per key, that is up to 60 pending interviews checked per cycle without special handling.\n\n### Is the embed event enough on its own?\n\nNo. It only fires in an open browser tab and never re-fires for already-completed interviews. Use it for instant UX reactions and keep polling as the source of truth.\n\n### Will I need to rebuild when native webhooks ship?\n\nNot if you isolate completion handling in one function. Swap the trigger from your poller to the webhook receiver and keep everything downstream unchanged.\n\n### Does polling give me the transcript too, or just the analysis?\n\nEverything. The read endpoint returns the full transcript, respondent info (including your `external_id`), session stats, and the analysis in a single response, so one fetch feeds every downstream destination.","category":"API Reference","lastModified":"2026-09-20T18:39:50.766019+00:00","metaTitle":"Webhook Setup | Koji Headless API","metaDescription":"Koji does not ship native webhooks yet. Get completion signals today with read-endpoint polling, the embed completed event, or automation platforms.","keywords":["webhooks","webhook setup","real-time notifications","interview events","webhook signature"],"aiSummary":"Honest guide to completion notifications in Koji: native outbound webhooks do not exist yet. Documents the three working patterns available today: polling GET /interviews/{id} from a server job, forwarding the koji:interview_completed embed event to your backend, and schedule-driven automation platforms.","aiPrerequisites":["api-authentication","completing-interviews-via-api"],"aiLearningOutcomes":["Know that Koji has no native webhooks yet and avoid dead-end setups","Build a polling worker on the read endpoint","Forward the embed completed event to your backend","Drive the REST API from Zapier, n8n, or Make on a schedule"],"aiDifficulty":"intermediate","aiEstimatedTime":"8 min"}],"pagination":{"total":1,"returned":1,"offset":0}}