{"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-05-18T13:48:49.014Z"},"content":[{"type":"documentation","id":"f04a883e-a2b2-480d-9aa0-c9f4b47009d7","slug":"sending-messages-via-api","title":"Sending Messages via API","url":"https://www.koji.so/docs/sending-messages-via-api","summary":"Messages are exchanged via POST /api/v1/interviews/:id/message (singular). Requires interview:chat permission and X-Session-Token header. Responses stream via SSE. Supports structured question widgets during interviews. Voice mode uses WebSocket connections with real-time audio streaming.","content":"# Sending Messages via API\n\nOnce you have [started an interview](/docs/starting-interviews-via-api), your application needs to handle the back-and-forth conversation between the respondent and Koji's interviewer. This article explains how messages flow in API-started interviews and how to integrate the conversational experience into your own UI.\n\n---\n\n## How Message Flow Works\n\nAn API-started interview follows a structured conversational pattern:\n\n1. **You start the interview** via `POST /api/v1/interviews/start`. The response includes an `initial_message` — this is the first thing the interviewer says to the respondent.\n2. **The respondent replies.** Your application collects the respondent's input (text or voice) and sends it to Koji.\n3. **Koji processes the response** and streams the next interviewer message back via Server-Sent Events (SSE), including follow-up questions that adapt to what the respondent said.\n4. **The cycle repeats** until the interview reaches a natural conclusion or you explicitly [complete it](/docs/completing-interviews-via-api).\n\nThis exchange happens through the interview session, with messages flowing through Koji's conversation engine.\n\n---\n\n## Text-Based Message Flow\n\nFor text-mode interviews, messages are exchanged through the message endpoint:\n\n```\nPOST https://koji.so/api/v1/interviews/:interview_id/message\n```\n\n**Important:** The endpoint path uses singular `/message`, not `/messages`.\n\n### Headers\n\n| Header | Value | Required |\n|---|---|---|\n| `Authorization` | `Bearer your_api_key` | Yes |\n| `X-Session-Token` | `session_token_from_start` | Yes |\n| `Content-Type` | `application/json` | Yes |\n\nThe API key must have the `interview:chat` permission to call this endpoint.\n\n### Request Body\n\n```json\n{\n  \"content\": \"The respondent's message text goes here.\"\n}\n```\n\n### Streaming Response (SSE)\n\nThe message endpoint streams the interviewer's response using Server-Sent Events. Your client should handle the stream as it arrives:\n\n```javascript\nconst response = await fetch(\n  `https://koji.so/api/v1/interviews/${interviewId}/message`,\n  {\n    method: 'POST',\n    headers: {\n      'Authorization': `Bearer ${apiKey}`,\n      'X-Session-Token': sessionToken,\n      'Content-Type': 'application/json'\n    },\n    body: JSON.stringify({ content: respondentMessage })\n  }\n);\n\nconst reader = response.body.getReader();\nconst decoder = new TextDecoder();\n\nwhile (true) {\n  const { done, value } = await reader.read();\n  if (done) break;\n  \n  const chunk = decoder.decode(value);\n  // Process SSE chunks — each contains a portion\n  // of the interviewer's response\n  handleStreamChunk(chunk);\n}\n```\n\n### Structured Response Handling\n\nWhen the interview includes [structured questions](/docs/structured-questions-guide), the SSE stream may include structured question events. These indicate that the interviewer is presenting a widget (such as a scale slider, multiple choice selector, or ranking interface) to the respondent. Your UI should render the appropriate input widget and send the structured response back.\n\n---\n\n## Voice-Based Message Flow\n\nVoice interviews use a different mechanism. When you start a voice interview, the response includes `voice_credentials` with a WebSocket URL and authentication token.\n\n### Establishing the Voice Connection\n\n```javascript\nconst ws = new WebSocket(voice_credentials.server_url);\n\nws.onopen = () => {\n  ws.send(JSON.stringify({\n    type: 'auth',\n    token: voice_credentials.token\n  }));\n};\n```\n\nOnce connected and authenticated, audio streams bidirectionally over the WebSocket. The voice service handles speech-to-text, processes the response through Koji's conversation engine, and streams synthesized speech back.\n\n### Voice Events\n\nThe WebSocket sends JSON events alongside audio data:\n\n| Event Type | Description |\n|---|---|\n| `transcript` | Real-time transcription of the respondent's speech |\n| `interviewer_start` | The interviewer has begun speaking |\n| `interviewer_end` | The interviewer has finished speaking |\n| `interview_status` | Status update (active, completing, completed) |\n| `error` | An error occurred in the voice session |\n\nHandle these events to update your UI — for example, showing a transcript as the respondent speaks or displaying a visual indicator when the interviewer is responding.\n\n---\n\n## Retrieving the Conversation\n\nAt any point during or after the interview, you can retrieve the full conversation:\n\n```\nGET https://koji.so/api/v1/interviews/:interview_id\n```\n\n### Headers\n\n| Header | Value | Required |\n|---|---|---|\n| `Authorization` | `Bearer your_api_key` | Yes |\n\nThe API key must have the `interview:read` permission. The response includes the full transcript, each message with its sender, timestamp, and content. After the interview is completed, it also includes analysis results, quality scores, and structured answers. See [Completing Interviews via API](/docs/completing-interviews-via-api) for more on the analysis payload.\n\n---\n\n## Building Your Own Chat UI\n\nWhen integrating the message flow into your application, here is a recommended approach:\n\n1. **Display the initial_message** from the start response as the first chat bubble.\n2. **Collect respondent input** via a text field or voice recording interface.\n3. **Send the message** to the `/message` endpoint.\n4. **Stream the response** — process SSE chunks to display the interviewer's reply progressively.\n5. **Handle structured questions** — if the stream includes a structured question event, render the appropriate widget.\n6. **Check interview status** — if it indicates the interview is winding down, prepare to call the [complete endpoint](/docs/completing-interviews-via-api).\n\nKeep the experience conversational. Avoid overwhelming respondents with too much UI chrome. The interview should feel like a natural conversation, not a form.\n\n---\n\n## Message Content Guidelines\n\nWhen sending respondent messages to the API:\n\n- **Send the raw text.** Do not pre-process, summarize, or modify what the respondent typed.\n- **Preserve formatting.** If the respondent uses line breaks, keep them.\n- **Do not inject instructions.** Sending hidden prompts or instructions alongside the respondent's message violates the terms of service and produces unreliable results.\n- **Handle empty messages.** Validate on your end that the message is not empty before sending.\n\n---\n\n## Conversation Length\n\nKoji's interview engine manages conversation length based on your research brief configuration. The interviewer naturally wraps up the conversation when it has covered the topics defined in the brief. You can also end the interview at any time by calling the [complete endpoint](/docs/completing-interviews-via-api).\n\nTypical interviews last 5 to 15 minutes for text and 8 to 20 minutes for voice, but this varies based on the research brief complexity and respondent engagement.\n\n---\n\n## Error Handling\n\n| Status Code | Error | Meaning |\n|---|---|---|\n| 400 | `invalid_message` | Message content is empty or malformed |\n| 401 | `unauthorized` | Invalid API key or session token |\n| 403 | `forbidden` | Key lacks `interview:chat` permission |\n| 404 | `not_found` | Interview does not exist |\n| 409 | `conflict` | Interview is already completed |\n| 429 | `rate_limited` | Too many messages in a short period |\n\nImplement retry logic with exponential backoff for transient errors like 429 and 5xx responses.\n\n---\n\n## Next Steps\n\n- [Start an interview to get your first session](/docs/starting-interviews-via-api)\n- [Complete the interview and retrieve analysis](/docs/completing-interviews-via-api)\n- [Learn about structured questions](/docs/structured-questions-guide)\n- [Explore the headless API overview](/docs/headless-api-overview)","category":"API Reference","lastModified":"2026-04-25T19:14:08.521275+00:00","metaTitle":"Sending Messages via API — Koji Docs","metaDescription":"Understand how messages flow between your app and Koji during API-started interviews, including text and voice modes.","keywords":["api messages","message flow","chat api","voice websocket","interview transcript"],"aiSummary":"Messages are exchanged via POST /api/v1/interviews/:id/message (singular). Requires interview:chat permission and X-Session-Token header. Responses stream via SSE. Supports structured question widgets during interviews. Voice mode uses WebSocket connections with real-time audio streaming.","aiPrerequisites":["starting-interviews-via-api"],"aiLearningOutcomes":["Send and receive messages through the API","Handle voice WebSocket connections","Retrieve transcripts during active interviews","Build a custom chat UI for interviews"],"aiDifficulty":"intermediate","aiEstimatedTime":"8 min read"}],"pagination":{"total":1,"returned":1,"offset":0}}