{"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-20T17:45:18.617Z"},"content":[{"type":"documentation","id":"41938bf1-ba71-4644-b995-975f1a26a56e","slug":"starting-interviews-via-api","title":"Starting Interviews via API","url":"https://www.koji.so/docs/starting-interviews-via-api","summary":"Reference for the start endpoint of the Koji Headless API. Covers the fully optional request body, the 201 response with session_token and initial_message, linking interviews to your CRM via external_id and metadata, voice mode credentials, rate-limit headers, and every error.","content":"# Starting Interviews via API\n\nThe start endpoint is the first call every headless integration makes. It creates a new interview session and hands back everything your client needs for the rest of the conversation: the interview id, the per-session token, and the AI's opening question.\n\n---\n\n## The endpoint\n\n```\nPOST https://www.koji.so/api/v1/interviews/start\n```\n\nCreates a new interview session and returns the credentials your client needs for the rest of the conversation. The study is derived from the API key. You never send a project id.\n\n### Headers\n\n| Header | Value | Required |\n|---|---|---|\n| `Authorization` | `Bearer pk_live_...` | Yes: Your project API key |\n| `Content-Type` | `application/json` | Yes: JSON request body |\n\n---\n\n## Request body\n\nEvery field is optional. This is the full shape:\n\n```json\n{\n  \"respondent\": {\n    \"external_id\": \"user_8271\",\n    \"display_name\": \"Jamie\",\n    \"metadata\": {\n      \"plan\": \"pro\",\n      \"signup_date\": \"2026-01-15\"\n    }\n  },\n  \"mode\": \"text\",\n  \"locale\": \"en-US\"\n}\n```\n\n- All fields are optional. An empty body `{}` starts an anonymous text interview.\n- `respondent.external_id` links the interview to a user in your system (CRM correlation). You can find it again in the GET response.\n- `respondent.metadata` is free-form JSON stored on the respondent record.\n- `mode` is `'text'` (default) or `'voice'`. Voice mode returns `voice_credentials`.\n\n---\n\n## Response\n\nA successful call returns HTTP 201:\n\n```json\n{\n  \"interview_id\": \"9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c\",\n  \"respondent_id\": \"c81d5b3e-2f6a-49c0-b7d4-8e1a3c5f9b27\",\n  \"session_token\": \"5b1f0c7d-9e42-4a68-b3c1-d7f28a904e5b\",\n  \"status\": \"active\",\n  \"mode\": \"text\",\n  \"project\": {\n    \"id\": \"3f2c8a14-6b9d-4e07-a852-1c5f9d3b7e60\",\n    \"name\": \"Churn Interview Study\",\n    \"slug\": \"churn-interview-study\"\n  },\n  \"initial_message\": \"Hi! Thanks for taking the time to chat today. To start, could you tell me a bit about how you first came across the product?\"\n}\n```\n\n- Returns HTTP 201.\n- `session_token` is a bare UUID. Send it as `X-Session-Token` on the message and complete endpoints. Treat it as a per-interview secret.\n- `initial_message` is the AI's opening question. It can be absent if greeting generation fails. Render your own opener in that case.\n- In voice mode the response also includes `voice_credentials: { signed_url, agent_id }`. Connect to the ElevenLabs Conversational AI session via `signed_url` using their client SDK.\n\nStore `interview_id` and `session_token` for the rest of the session. The message and complete endpoints need both.\n\n---\n\n## Try it with curl\n\n```bash\ncurl -X POST https://www.koji.so/api/v1/interviews/start \\\n  -H \"Authorization: Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"respondent\": { \"external_id\": \"user_8271\", \"display_name\": \"Jamie\" },\n    \"mode\": \"text\"\n  }'\n```\n\n---\n\n## Start from JavaScript\n\n```javascript\nconst response = await fetch('https://www.koji.so/api/v1/interviews/start', {\n  method: 'POST',\n  headers: {\n    'Authorization': 'Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    respondent: { external_id: 'user_8271', display_name: 'Jamie' },\n    mode: 'text',\n  }),\n})\n\nconst interview = await response.json()\n// Keep these for the rest of the session:\nconst { interview_id, session_token, initial_message } = interview\n```\n\nFrom here, the conversation continues on the message endpoint. See [Sending Messages via API](/docs/sending-messages-via-api).\n\n---\n\n## Linking interviews to your CRM\n\nThe optional `respondent` object is how you connect interviews to users in your own system:\n\n- `external_id` is your identifier for the user (for example `user_8271`). It comes back as `respondent.external_id` in the GET response, so a completion worker can join interview results to the right CRM record.\n- `metadata` is free-form JSON stored on the respondent record. Use it for plan, cohort, signup date, or anything your analysis pipeline wants later.\n- `display_name` is the respondent's name. It comes back as `respondent.display_name` when you read the interview.\n\nStarting anonymous interviews is fine too: an empty body `{}` starts an anonymous text interview.\n\n---\n\n## Voice mode\n\nPass `\"mode\": \"voice\"` to start a voice interview. The response then also includes:\n\n```json\n\"voice_credentials\": {\n  \"signed_url\": \"...\",\n  \"agent_id\": \"...\"\n}\n```\n\nConnect to the ElevenLabs Conversational AI session via `signed_url` using their client SDK. Text mode is the default when `mode` is omitted.\n\n---\n\n## Rate limit headers\n\nSuccessful start responses carry the standard rate-limit headers:\n\n| Header | Description |\n|---|---|\n| `X-RateLimit-Limit` | Your key's per-minute limit |\n| `X-RateLimit-Remaining` | Requests left in the current window |\n| `X-RateLimit-Reset` | ISO timestamp when the window resets |\n\nRate-limit headers are included on successful responses from the start and get endpoints. The message endpoint streams SSE and omits them on success. Details in [Rate Limits and CORS](/docs/rate-limits-and-cors).\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:start permission` | The key was created without the interview:start 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. Rare: API access is included on every plan, so this appears only for restricted accounts |: |\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| 403 | `This study is no longer accepting responses` | The study is paused or closed | `closed` |\n| 403 | `unavailable` | The study cannot accept responses right now (owner account state) | `message`, `reason` |\n| 429 | `interview_limit_reached` | The study hit its response limit or the study owner has run out of credits | `message`, `current`, `limit` |\n\nOne row deserves special attention: `interview_limit_reached` is a quota error, not a rate limit. It means the study hit its response limit or the study owner has run out of credits, and retrying will not help. A plain `Rate limit exceeded` 429, by contrast, resolves itself after `retry_after` seconds.\n\n---\n\n## FAQ\n\n### Do I need to send a project id?\n\nNo. The study is derived from the API key. You never send a project id.\n\n### Can I start an interview with an empty body?\n\nYes. An empty body `{}` starts an anonymous text interview. All request fields are optional.\n\n### What if initial_message is missing?\n\nIt can be absent if greeting generation fails. Render your own opener in that case; the conversation works normally from the first message you send.\n\n### Can I reuse a session token for another interview?\n\nNo. The token is scoped to a single interview session. Each start call issues a fresh one.","category":"API Reference","lastModified":"2026-08-19T21:52:50.247309+00:00","metaTitle":"Starting Interviews via API | Koji Headless API","metaDescription":"Full reference for POST /interviews/start: optional respondent fields, session tokens, CRM linking with external_id, voice credentials, and errors.","keywords":["start interview api","post start","headless interview","api integration","koji api"],"aiSummary":"Reference for the start endpoint of the Koji Headless API. Covers the fully optional request body, the 201 response with session_token and initial_message, linking interviews to your CRM via external_id and metadata, voice mode credentials, rate-limit headers, and every error.","aiPrerequisites":["api-authentication"],"aiLearningOutcomes":["Start an interview with curl or fetch and store the returned credentials","Link interviews to CRM records with external_id and metadata","Handle voice mode and its ElevenLabs credentials","Read rate-limit headers and handle every start-endpoint error"],"aiDifficulty":"intermediate","aiEstimatedTime":"10 min"}],"pagination":{"total":1,"returned":1,"offset":0}}