Completing Interviews via API
Finalize interviews with POST /interviews/{id}/complete, then poll the read endpoint for the asynchronous AI analysis.
Completing Interviews via API
Completing 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.
The endpoint
POST https://www.koji.so/api/v1/interviews/{interview_id}/complete
Marks the interview as completed and triggers AI analysis in the background.
Headers
| Header | Value | Required |
|---|---|---|
Authorization | Bearer pk_live_... | Yes: Your project API key |
X-Session-Token | <session_token from start> | Yes: The session token returned by the start endpoint. Proves the request belongs to this respondent session |
Content-Type | application/json | Yes: JSON request body |
Like the message endpoint, complete requires both the API key (with interview:complete permission) and the respondent's session token.
Request body
{
"reason": "natural"
}
reasonis optional (default'natural'). Use it to record why the interview ended, e.g.'natural','user_ended','timeout'.
Response
{
"status": "completed",
"interview_id": "9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c",
"completed_at": "2026-07-14T12:34:56.000Z",
"stats": {
"message_count": 18,
"user_messages": 9,
"duration_seconds": 432
},
"analysis": null,
"analysis_pending": true
}
The stats block summarizes the session:
-
message_count: total messages in the transcript. -
user_messages: how many came from the respondent. -
duration_seconds: time from start to completion. -
Analysis runs asynchronously.
analysisisnullandanalysis_pendingistrueuntil it finishes. Poll the GET endpoint to retrieve it.
Analysis is asynchronous
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:
// Mark the interview complete
await fetch(`https://www.koji.so/api/v1/interviews/${interviewId}/complete`, {
method: 'POST',
headers: {
'Authorization': 'Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K',
'X-Session-Token': sessionToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({ reason: 'natural' }),
})
// Analysis runs in the background: poll the read endpoint until it lands
async function waitForAnalysis(interviewId) {
for (let attempt = 0; attempt < 20; attempt++) {
const res = await fetch(`https://www.koji.so/api/v1/interviews/${interviewId}`, {
headers: { 'Authorization': 'Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K' },
})
const data = await res.json()
if (data.analysis) return data.analysis
await new Promise(r => setTimeout(r, 15000))
}
return null
}
The 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.
Try it with curl
curl -X POST https://www.koji.so/api/v1/interviews/9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c/complete \
-H "Authorization: Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K" \
-H "X-Session-Token: 5b1f0c7d-9e42-4a68-b3c1-d7f28a904e5b" \
-H "Content-Type: application/json" \
-d '{ "reason": "natural" }'
Then fetch the results once analysis has had time to run:
curl https://www.koji.so/api/v1/interviews/9f4c1e2a-7b3d-4e8f-a1c5-2d6b8e0f4a7c \
-H "Authorization: Bearer pk_live_wJalrXUtnFEMI4K7MDENGbPxRfiCYz2K"
analysisis populated only after the interview is completed and the background analysis has finished; otherwisenull.stats.duration_secondsisnulluntil the respondent has both started and completed timestamps.
When to call complete
- The
doneframe from the message endpoint hasinterview_complete: true. This is the AI-driven natural ending. See Sending Messages via API. - The respondent ends the conversation early. Record it with
"reason": "user_ended". - Your client times the session out. Record it with
"reason": "timeout".
The reason value is free-form and defaults to 'natural'.
Errors
| Status | error | When | Extra fields |
|---|---|---|---|
| 401 | Missing or invalid authorization header | No Authorization: Bearer header sent | : |
| 401 | Invalid API key | The key does not exist, is malformed, is inactive, or has expired | : |
| 403 | API key does not have interview:complete permission | The key was created without the interview:complete scope | : |
| 403 | Origin not allowed | The browser's Origin header does not match the key's allowed origins | : |
| 403 | Headless API access requires an Interviews plan or higher. | The study owner's account cannot use the headless API. Rare: API access is included on all current plans, so this appears only for restricted accounts | : |
| 429 | Rate limit exceeded | The key exceeded its per-minute request limit | retry_after |
| 500 | Internal server error | Unexpected server failure. Safe to retry with backoff | : |
| 401 | Missing X-Session-Token header | The session token header was not sent | : |
| 401 | Invalid session token | The token does not match this interview's respondent | : |
| 404 | Interview not found | Unknown interview id (or the interview was deleted) | : |
| 403 | Interview does not belong to this project | The interview belongs to a different study than the API key | : |
FAQ
How do I get the analysis?
Poll GET /interviews/{id} after completing. analysis stays null until the background job finishes, then the full analysis object appears in the read response.
What does analysis_pending mean?
It 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.
Which permission does polling need?
The read endpoint requires interview:read on the API key. The session token is not needed for reads.
What if I never call complete?
The 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.
Related Articles
Sending Messages via API
Send respondent messages to POST /interviews/{id}/message and parse the Server-Sent Events reply stream frame by frame.
Understanding Quality Scores
Learn how Koji evaluates interview quality on a 0-5 scale and why it matters for your research and billing.
User Research API: Embed AI Interviews into Any Product or Workflow
How to use Koji's User Research API to run AI-moderated interviews from your own backend. Covers REST endpoints, the embed widget, webhooks, authentication, rate limits, and headless interview patterns.
Webhook Setup
Koji has no native outbound webhooks yet. Three working alternatives: polling the read endpoint, embed completion events, and automation platforms.
API Authentication
How Koji Headless API authentication works: pk_live_ API keys, permissions, session tokens, and origin allowlists.
Embed Widget Reference
Embed the Koji interview widget with an iframe: URL parameters, koji:* postMessage events, React integration, and prefilled respondents.