Behaviour
Errors
Every error from the API, REST and MCP alike, is the same envelope. Branch on code, which never changes; show or log message, which may.
The envelope
{
"error": {
"code": "insufficient_credits",
"message": "This run costs N credits and the account has M. Top up to continue.",
"details": {
"needed": "N",
"balance": "M",
"topUpUrl": "https://kinoseed.app/pricing"
}
}
}details is there when the client can act on it: the balance and a link to the plans, the seconds to wait, the fields that failed validation. A request refused with an error was never charged, and a run that fails after it started is refunded on the job (see credits.refunded).
Codes
| Code | Status | When | What to do | Retry |
|---|---|---|---|---|
| unauthorized | 401 | No Authorization header, a token that is not a key, a revoked key, or a key whose account is gone. details.reason says missing, invalid or revoked. | Send Authorization: Bearer ks_live_... with a live key from the account page. | no |
| forbidden_scope | 403 | The key is valid but was created without the scope this operation needs. | Create a key with that scope (a key's scopes are fixed when it is created) and revoke the old one. | no |
| insufficient_credits | 402 | The balance does not cover the quoted price. Nothing was charged. details has needed, balance and topUpUrl. | Start a plan at topUpUrl, then send the same request again. | yes |
| rate_limited | 429 | Over a request or generation budget for this key, this account or this address. | Wait for the Retry-After header (also details.retryAfterSeconds) and retry. | yes |
| invalid_request | 400 | The body or query failed validation, the JSON did not parse, or an upload was too large (413) or of an unsupported type (415). details.issues lists the fields. | Fix the request; sending it again unchanged gets the same answer. | no |
| unknown_model | 400 | model is not an id in the catalog for this kind of run. | Pick an id from GET /models (filter with ?kind=image or ?kind=video). | no |
| unsafe_url | 400 | An input URL is not public https: a private address, localhost, an IP literal, a root-relative path or plain http. | Upload the file with POST /files and pass the URL it returns, or use a public https URL. | no |
| not_found | 404 | No job (or webhook endpoint) with this id belongs to the account behind the key. | Check the id. Jobs and endpoints of other accounts are never visible, so this is also the answer for them. | no |
| limit_reached | 409 | The account already has as many of something as it may have: 5 webhook endpoints. details.limit says how many. | Delete one you no longer use (DELETE /webhooks/{id}), then create the new one. | no |
| idempotency_in_flight | 409 | Another request with the same Idempotency-Key is still being processed. | Wait a moment and send it again: you will get the first request's response. | yes |
| idempotency_mismatch | 422 | This Idempotency-Key was used in the last 24 hours with a different body. | Use a new key for a new request. | no |
| too_many_running | 429 | The account already has the maximum number of runs queued or running through the API. | Wait for one to finish (poll GET /jobs/{id}), then start the next. Retry-After suggests when. | yes |
| provider_unavailable | 503 | A service behind the API (storage, or the model provider) is not answering right now. | Retry with backoff. | yes |
| internal | 500 | Something failed on our side. It was recorded and we are alerted. | Retry with backoff; with an Idempotency-Key the retry cannot start a second run. | yes |
Retrying safely
Retry the codes marked yes with exponential backoff, honouring Retry-After when it is set. Send an Idempotency-Key on every POST and a retry after a timeout returns the first response instead of starting a second run. A 5xx without an envelope (a proxy timing out) is safe to retry the same way.