Overview
Two response shapes
Every error from the API is JSON with a top-level detailfield. There are two shapes you’ll encounter. Application errors — anything we raise on purpose — return a single string:
{"detail": "Invalid API key"}FastAPI validation errors — raised before your request reaches our handler — return an array, one entry per failed field, with msg, type, and a loc path to the offending input:
{
"detail": [
{
"msg": "field required",
"type": "value_error.missing",
"loc": ["body", "tone"]
}
]
}Branch on typeof detail === “string” in your client to handle both cleanly.
Status codes
What each code means
We use a small, conventional subset of HTTP status codes. Nothing surprising.
| Status | Meaning |
|---|---|
| 400 | Bad Request. Malformed input, an unsupported file type, or a required field missing from a multipart upload. |
| 401 | Unauthorized. Auth credential missing or invalid. Check your X-API-Key header. |
| 403 | Forbidden. Your credential is valid, but you’re not allowed to touch this resource. |
| 404 | Not Found. The resource doesn’t exist, or doesn’t belong to you. |
| 409 | Conflict. A resource with that identifier already exists — most often a duplicate voice-profile name. |
| 422 | Validation Error. Your request body failed FastAPI input validation. Returns the array shape with a loc pointer to the bad field. |
| 429 | Rate Limit. Includes a Retry-After header in seconds. Respect it. |
| 500 | Server Error. We get a Sentry alert. If you see one, we’d still like to hear about it from your side. |
Recovery
Common errors and what to do
A handful of error responses cover most of what you’ll see in practice. Here’s how to handle each.
401 — Invalid API key
{"detail": "Invalid API key"}Check that the X-API-Key header is present and that you’re sending the full key, including the ink_live_ prefix. Keys are environment-scoped — a key from a different workspace will look valid but fail here.
422 — Field validation
{
"detail": [
{
"msg": "field required",
"type": "value_error.missing",
"loc": ["body", "humanness_level"]
}
]
}Read detail[0].loc to find the offending field. The first element tells you where it came from (body, query, header) and the rest is the path into your payload.
429 — Rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 27
{"detail": "Rate limit exceeded"}Wait the number of seconds in Retry-After, then retry the same request. If the header is missing for any reason, fall back to exponential back-off starting at one second.
400 — Unsupported file type
{"detail": "Unsupported file type. Allowed: docx, pdf, txt, md"}Convert your source to one of the allowed extensions before uploading. We sniff content-type and reject anything else early, before the worker picks it up.
422 — Invalid enum value
{
"detail": [
{
"msg": "Invalid tone. Allowed: balanced, casual, formal, academic",
"type": "value_error.enum",
"loc": ["body", "tone"]
}
]
}Pick one of the values from the msgstring. We list the allowed set inline so you don’t have to cross-reference the spec to fix a typo.
404 — Document not found
{"detail": "Document not found"}Either the job ID is wrong, or the job belongs to a different workspace. Document IDs are owner-scoped — there’s no global 404 vs. 403 distinction we expose.
500s
When the server breaks
A 500 means something on our side went wrong in a way we didn’t anticipate. Sentry will almost certainly have flagged it before you finish reading the response, but we genuinely want to hear about it from your perspective too — your context is usually more useful than a stack trace.
If you hit a 500, email hello@inksong.appwith the rough time, the endpoint you called, and a request ID if you have one. We’ll trace it and write back.