API Docs

Errors explained

Every non-2xx response is JSON. Here are the shapes you’ll see, the status codes we use, and what to do about each one.

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.

StatusMeaning
400Bad Request. Malformed input, an unsupported file type, or a required field missing from a multipart upload.
401Unauthorized. Auth credential missing or invalid. Check your X-API-Key header.
403Forbidden. Your credential is valid, but you’re not allowed to touch this resource.
404Not Found. The resource doesn’t exist, or doesn’t belong to you.
409Conflict. A resource with that identifier already exists — most often a duplicate voice-profile name.
422Validation Error. Your request body failed FastAPI input validation. Returns the array shape with a loc pointer to the bad field.
429Rate Limit. Includes a Retry-After header in seconds. Respect it.
500Server 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.

Tell us

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.