Common Error Codes
Every error the Koji Headless API returns: the error envelope, status codes, all error strings with fixes, and a debugging checklist.
Common Error Codes
Every failure in the Headless API is designed to be diagnosable from two things: the HTTP status and the error string in the body. This page collects all of them in one place, with fixes.
The error envelope
- Every error response is JSON with an
errorstring:{ "error": "..." }. - Some errors add fields:
retry_after(rate limit),messageandreason(start-endpoint availability errors),currentandlimit(quota errors),closed(paused studies). - There is no
codeordetailsfield. Match on the HTTP status and theerrorstring. - The message endpoint can also fail mid-stream: the final SSE frame is then
{"type":"error","error":"Stream processing failed"}with the HTTP status already committed as 200.
A typical error body:
{
"error": "Rate limit exceeded",
"retry_after": 42
}
Status code overview
| Status | Meaning |
|---|---|
| 200 | Success (message: SSE stream; complete/get: JSON) |
| 201 | Interview created (start) |
| 400 | Bad request. Missing content, inactive interview, or invalid fields |
| 401 | Authentication failed. Bad/missing API key or session token |
| 403 | Authenticated but not allowed. Missing permission, origin blocked, study closed, wrong project, or plan/entitlement |
| 404 | Interview or project not found |
| 429 | Rate limit exceeded or interview quota reached |
| 500 | Server error. Retry with backoff |
Every error string
This table consolidates every distinct error the API returns, across the start, message, complete, read, and embed bootstrap endpoints. The four per-permission 403s are collapsed into one row: the real string names the specific scope, for example API key does not have interview:read permission.
| Status | error | When it happens | How to fix it |
|---|---|---|---|
| 400 | Interview is not active | The interview was already completed or abandoned | The interview was already completed or abandoned. Start a new one. |
| 400 | Message content is required | content was missing or not a string | Send a JSON body with a content string field. |
| 401 | Missing or invalid authorization header | No Authorization: Bearer header sent | Send Authorization: Bearer pk_live_... on every request. |
| 401 | Invalid API key | The key does not exist, is malformed, is inactive, or has expired | Check the key starts with pk_live_, was copied in full, and has not been revoked or expired. Create a new key if in doubt. |
| 401 | Missing X-Session-Token header | The session token header was not sent | Send the session_token from the start response in the X-Session-Token header. |
| 401 | Invalid session token | The token does not match this interview's respondent | Use the exact session_token issued for this interview id. Tokens are not interchangeable between interviews. |
| 403 | API key does not have <permission> permission | The key was created without the permission this endpoint requires | Create a key that includes the scope the endpoint requires. See API Key Permissions. |
| 403 | Origin not allowed | The browser's Origin header does not match the key's allowed origins | Add your site's origin (with scheme) to the key's allowed origins, or use a wildcard entry. See API Authentication. |
| 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 | Check the study owner's plan and credit balance in the dashboard. |
| 403 | This study is no longer accepting responses | The study is paused or closed | The study is paused or closed. Reopen it from the dashboard. |
| 403 | unavailable | The study cannot accept responses right now (owner account state) | The study cannot accept responses right now. Inspect the message and reason fields for the specific cause. |
| 403 | Interview does not belong to this project | The interview belongs to a different study than the API key | The interview id and the API key point at different studies. Use the key for the study that owns the interview. |
| 403 | API key does not match project | The key belongs to a different study | Use the API key created for the same study as the embed project id. |
| 404 | Interview not found | Unknown interview id (or the interview was deleted) | Check the interview id. It must come from a start response for the same study. |
| 404 | Project not found | Unknown project id | Check the project id in the embed URL. |
| 429 | Rate limit exceeded | The key exceeded its per-minute request limit | Wait retry_after seconds, then retry. See Rate Limits and CORS. |
| 429 | interview_limit_reached | The study hit its response limit or the owner's interview quota | Raise the study's response limit or the owner's quota. Retrying will not help. |
| 500 | Internal server error | Unexpected server failure. Safe to retry with backoff | Retry with exponential backoff. If it persists, contact support. |
Per-endpoint error tables, including which extra fields accompany each error, live on the endpoint pages: Starting Interviews, Sending Messages, and Completing Interviews.
SSE stream errors
The message endpoint streams its reply as Server-Sent Events, which means a failure can happen after the HTTP status is already committed as 200. In that case the last frame of the stream is:
data: {"type":"error","error":"Stream processing failed"}
Your SSE parser must treat an error frame as terminal for that message. Do not rely on the HTTP status alone for the message endpoint; check frame types. See Sending Messages via API for a parser that handles this correctly.
Debugging checklist
- Key rejected? Confirm it starts with
pk_live_, is active (not revoked), and hasn't expired. Invalid session token? Send the exactsession_tokenfrom the start response in theX-Session-Tokenheader, for the same interview id.Origin not allowed? Your key restricts origins; add your site's origin (with scheme) or use a wildcard entry.Interview is not active? The interview was already completed. Start a new one.- Missing permission? The key was created with a narrowed permission set; create a key that includes the scope you need.
- 429? Check
retry_afterand theX-RateLimit-*headers; distinguish rate limits frominterview_limit_reachedquota errors.
FAQ
Is there an error code field I can switch on?
No. There is no code or details field. Match on the HTTP status plus the error string. The strings in the table above are the exact strings the handlers return.
I got a 429. Should I retry?
Depends on the string. Rate limit exceeded resolves after retry_after seconds. interview_limit_reached is a quota error and retrying will not help. See Rate Limits and CORS.
Why do I get 401 on message but 200 on start with the same key?
Start needs only the API key. Message additionally needs the X-Session-Token header with the exact token from the start response. A missing or mismatched token fails with 401 even though the key is valid.
The error says the interview belongs to a different project. What happened?
Interview ids are study-scoped, and so are API keys. You are calling with a key from one study and an interview id from another. Use the key for the study that owns the interview.
Related Articles
API Authentication
How Koji Headless API authentication works: pk_live_ API keys, permissions, session tokens, and origin allowlists.
How the Quality Gate Works
Understand Koji's quality gate — conversations scoring below 3/5 are completely free and don't consume credits, protecting your research budget.
Managing API Keys
Create, configure, and revoke project-level API keys for integrating with the Koji API.
Rate Limits and CORS
Per-key rate limits, X-RateLimit headers, 429 handling with backoff, quota errors, and how CORS works on the Koji API.
Browser Compatibility
Which browsers work with Koji and known limitations for voice and text interviews.
Interview Not Counting
Understand why some interviews do not count toward your usage and how the quality gate works.