Kova returns JSON error bodies for every status code except 500, which returns plain text. The HTTP status is the primary signal; the JSON body carries an error or detail field.
Do not assume error bodies are JSON. A 500 returns Content-Type: text/plain with the literal body Internal Server Error. Calling .json() on it will throw. Guard every error parse.
Status code summary
Successful and most failed responses carry an x-request-id header. Log it — it’s what support needs to trace a request.
x-request-id is returned on every response, including 500s. Always log it — it’s what support needs to trace a request.
401 Unauthorized
Two distinct shapes, depending on where the request was rejected.
Missing or malformed key — rejected at the gateway:
Returned when the x-api-key header is absent, or its value doesn’t begin with kova_sk_. Also returned if you send Authorization: Bearer <key> instead — Kova does not accept bearer tokens.
Well-formed but unknown key — rejected at the billing service:
The header parsed correctly but the key doesn’t exist or has been revoked.
Fix: Verify the header name and value, then regenerate on Dashboard → API keys. The header name is case-insensitive — x-api-key and X-API-KEY both work.
402 Payment Required
Cause: The account behind the key has run out of credit.
Fix: Top up at Dashboard → Billing. Requests resume as soon as credit lands — the key stays valid, so there’s nothing to regenerate.
422 Unprocessable Entity
detail is an array and may contain several errors at once. loc is the path to the offending field.
Common causes and exact messages
Two easy 422s to hit: passing bitrate in bits per second (128000) instead of kbps ("128k" or 128), and passing bitrate at all to wav, pcm, linear16, mulaw or alaw. See Text to speech.
Unknown voice
A voice that isn’t in the catalog returns 422 with a different body shape from the validation errors above — no detail array:
valid_voices is the live catalog, so this error is self-correcting — you can read the allowed set straight off the failure. The same shape is returned by POST /v1/tts, POST /v1/tts/stream, and the Vapi endpoint.
Because this body has error and not detail, code that assumes every 422 has a detail array will KeyError here. Branch on which key is present.
429 Too Many Requests
Cause: Your key already has 9 requests in flight. The 10th is rejected immediately — nothing queues server-side. HTTP requests and open WebSocket connections draw on the same budget.
Fix: Hold a client-side semaphore of 9 or fewer, and retry with backoff.
429 responses carry no Retry-After header. Choose your own schedule — start at 200 ms and back off exponentially with jitter, capped around 5 s.
See Rate limits for the full policy.
500 Internal Server Error
Plain text, Content-Type: text/plain; charset=utf-8, with an x-request-id header.
A genuine server fault. Transient — retry with backoff. The SDKs retry 5xx automatically.
Note the content type: 500 is the one status code that does not return JSON. Guard your error parsing so a .json() call doesn’t throw on top of the original failure.
For persistent 500s, contact support with the x-request-id.
WebSocket errors
The WebSocket transport does not use HTTP status codes after the handshake. Failures arrive as error frames:
Key differences from HTTP:
error frames are not fatal. The socket stays open and other contexts keep working.
- An unknown
voice_id produces a clean error frame naming every valid voice.
- A revoked key produces
{"error": "INVALID_API_KEY"} followed by WebSocket close code 1008 (policy violation).
- A malformed frame produces
{"error": "invalid frame: ..."} with the underlying validation message.
See WebSocket frames for the full frame reference.
How errors surface in the SDKs
The SDKs also export KovaTTSAuthError (401), KovaTTSRateLimitError (429), KovaTTSValidationError (422), KovaTTSServerError (5xx), KovaTTSConnectionError, and KovaTTSProtocolError. Catch the base KovaTTSError to handle everything; catch subclasses when you need to branch per code.