{"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-23T14:06:38.389Z"},"content":[{"type":"documentation","id":"326ce594-92b4-48ba-a543-8a9f7a506611","slug":"completing-interviews-via-api","title":"Completing Interviews via API","url":"https://www.koji.so/docs/completing-interviews-via-api","summary":"Reference for the complete endpoint of the Koji Headless API. Marks the interview finished, returns session stats, and triggers AI analysis in the background. Shows how to poll the read endpoint until the analysis lands.","content":"# Completing Interviews via API\n\nCompleting an interview does two things: it marks the session finished, and it kicks off AI analysis in the background. Call it when the AI signals the interview is done, or when the respondent ends the conversation themselves.\n\n---\n\n## The endpoint\n\n```\nPOST https://www.koji.so/api/v1/interviews/{interview_id}/complete\n```\n\nMarks the interview as completed and triggers AI analysis in the background.\n\n### Headers\n\n| Header | Value | Required |\n|---|---|---|\n| `Authorization` | `Bearer pk_live_...` | Yes: Your project API key |\n| `X-Session-Token` | `<session_token from start>` | Yes: The session token returned by the start endpoint. Proves the request belongs to this respondent session |\n| `Content-Type` | `application/json` | Yes: JSON request body |\n\nLike the message endpoint, complete requires both the API key (with `interview:complete` permission) and the respondent's session token.\n\n---\n\n## Request body\n\n```json\n{\n  \"reason\": \"natural\"\n}\n```\n\n- `reason` is optional (default `'natural'`). Use it to record why the interview ended, e.g. `'natural'`, `'user_ended'`, `'timeout'`.\n\n---\n\n## Response\n\n```json\n{\n  \"status\": \"completed\",\n  \"interview_id\": \"9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c\",\n  \"completed_at\": \"2026-07-14T12:34:56.000Z\",\n  \"stats\": {\n    \"message_count\": 18,\n    \"user_messages\": 9,\n    \"duration_seconds\": 432\n  },\n  \"analysis\": null,\n  \"analysis_pending\": true\n}\n```\n\nThe `stats` block summarizes the session:\n\n- `message_count`: total messages in the transcript.\n- `user_messages`: how many came from the respondent.\n- `duration_seconds`: time from start to completion.\n\n- Analysis runs asynchronously. `analysis` is `null` and `analysis_pending` is `true` until it finishes. Poll the GET endpoint to retrieve it.\n\n---\n\n## Analysis is asynchronous\n\n`analysis` is `null` and `analysis_pending` is `true` in the completion response, always. The AI analysis runs in the background after completion. To retrieve it, poll `GET /interviews/{id}` until `analysis` is non-null:\n\n```javascript\n// Mark the interview complete\nawait fetch(`https://www.koji.so/api/v1/interviews/${interviewId}/complete`, {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K',\n    'X-Session-Token': sessionToken,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ reason: 'natural' }),\n})\n\n// Analysis runs in the background: poll the read endpoint until it lands\nasync function waitForAnalysis(interviewId) {\n  for (let attempt = 0; attempt < 20; attempt++) {\n    const res = await fetch(`https://www.koji.so/api/v1/interviews/${interviewId}`, {\n      headers: { 'Authorization': 'Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K' },\n    })\n    const data = await res.json()\n    if (data.analysis) return data.analysis\n    await new Promise(r => setTimeout(r, 15000))\n  }\n  return null\n}\n```\n\nThe read endpoint requires the `interview:read` permission and no session token, which makes it a natural fit for a server-side worker. See [API Key Permissions](/docs/api-permissions).\n\n---\n\n## Try it with curl\n\n```bash\ncurl -X POST https://www.koji.so/api/v1/interviews/9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c/complete \\\n  -H \"Authorization: Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K\" \\\n  -H \"X-Session-Token: 5b1f0c7d-9e42-4a68-b3c1-d7f28a904e5b\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{ \"reason\": \"natural\" }'\n```\n\nThen fetch the results once analysis has had time to run:\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\n- `analysis` is populated only after the interview is completed and the background analysis has finished; otherwise `null`.\n- `stats.duration_seconds` is `null` until the respondent has both started and completed timestamps.\n\n---\n\n## When to call complete\n\n- The `done` frame from the message endpoint has `interview_complete: true`. This is the AI-driven natural ending. See [Sending Messages via API](/docs/sending-messages-via-api).\n- The respondent ends the conversation early. Record it with `\"reason\": \"user_ended\"`.\n- Your client times the session out. Record it with `\"reason\": \"timeout\"`.\n\nThe `reason` value is free-form and defaults to `'natural'`.\n\n---\n\n## Errors\n\n| Status | `error` | When | Extra fields |\n|---|---|---|---|\n| 401 | `Missing or invalid authorization header` | No `Authorization: Bearer` header sent |: |\n| 401 | `Invalid API key` | The key does not exist, is malformed, is inactive, or has expired |: |\n| 403 | `API key does not have interview:complete permission` | The key was created without the interview:complete scope |: |\n| 403 | `Origin not allowed` | The browser's Origin header does not match the key's allowed origins |: |\n| 403 | `Headless API access is not enabled for this account.` | The study owner's account cannot use the headless API right now. The API and MCP connector are available on every plan, including a free account, so this is account state, not a plan gate |: |\n| 429 | `Rate limit exceeded` | The key exceeded its per-minute request limit | `retry_after` |\n| 500 | `Internal server error` | Unexpected server failure. Safe to retry with backoff |: |\n| 401 | `Missing X-Session-Token header` | The session token header was not sent |: |\n| 401 | `Invalid session token` | The token does not match this interview's respondent |: |\n| 404 | `Interview not found` | Unknown interview id (or the interview was deleted) |: |\n| 403 | `Interview does not belong to this project` | The interview belongs to a different study than the API key |: |\n\n---\n\n## FAQ\n\n### How do I get the analysis?\n\nPoll `GET /interviews/{id}` after completing. `analysis` stays `null` until the background job finishes, then the full analysis object appears in the read response.\n\n### What does analysis_pending mean?\n\nIt signals that analysis was triggered and has not landed yet. In the completion response it is always `true`, because the analysis job starts asynchronously at completion time.\n\n### Which permission does polling need?\n\nThe read endpoint requires `interview:read` on the API key. The session token is not needed for reads.\n\n### What if I never call complete?\n\nThe interview stays active and no analysis is triggered. Completion is what finalizes the session and starts the analysis job, so make sure one of your code paths always calls it.","category":"API Reference","lastModified":"2026-09-20T18:39:38.298691+00:00","metaTitle":"Completing Interviews via API | Koji Headless API","metaDescription":"Reference for the Koji complete endpoint: the reason field, session stats in the response, asynchronous analysis, and polling for results.","keywords":["complete interview","api complete","interview analysis","session token","post complete"],"aiSummary":"Reference for the complete endpoint of the Koji Headless API. Marks the interview finished, returns session stats, and triggers AI analysis in the background. Shows how to poll the read endpoint until the analysis lands.","aiPrerequisites":["starting-interviews-via-api","api-authentication"],"aiLearningOutcomes":["Complete an interview with the reason field","Read the stats block in the completion response","Understand why analysis is null and analysis_pending is true","Poll the read endpoint until analysis is available"],"aiDifficulty":"intermediate","aiEstimatedTime":"8 min"}],"pagination":{"total":1,"returned":1,"offset":0}}