Back to docs
API Reference

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

HeaderValueRequired
AuthorizationBearer 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-Typeapplication/jsonYes: 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"
}
  • reason is 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. analysis is null and analysis_pending is true until 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"
  • analysis is populated only after the interview is completed and the background analysis has finished; otherwise null.
  • stats.duration_seconds is null until the respondent has both started and completed timestamps.

When to call complete

  • The done frame from the message endpoint has interview_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

StatuserrorWhenExtra fields
401Missing or invalid authorization headerNo Authorization: Bearer header sent:
401Invalid API keyThe key does not exist, is malformed, is inactive, or has expired:
403API key does not have interview:complete permissionThe key was created without the interview:complete scope:
403Origin not allowedThe browser's Origin header does not match the key's allowed origins:
403Headless 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:
429Rate limit exceededThe key exceeded its per-minute request limitretry_after
500Internal server errorUnexpected server failure. Safe to retry with backoff:
401Missing X-Session-Token headerThe session token header was not sent:
401Invalid session tokenThe token does not match this interview's respondent:
404Interview not foundUnknown interview id (or the interview was deleted):
403Interview does not belong to this projectThe 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.