Errors
Every status code the decision API returns, with the exact response bodies, headers and retry guidance.
Error shapes
System One (/v1/systemone, /v1/models) uses TypeSafe's shape. Most errors have an error_type and a message:
{ "detail": { "error_type": "api_usage_error", "message": "Unknown model: nope" } }Validation errors (422) carry a list instead, one entry per problem.
/v1/decisions uses the OpenAI error shape and never returns 422:
{ "error": { "message": "...", "type": "invalid_request_error", "param": "questions", "code": null } }Every response, success or error, carries an x-typesafe-request-id header (x-request-id on /v1/decisions; /v1/models sends both). Quote it when you ask for help. Every error body is Content-Type: application/json.
Reading the bodies
The status codes and causes below are fixed. The message text is meant for people and may be reworded, so match on the status code and error_type, not on the message. The 422 bodies follow FastAPI exactly, including key order.
Summary
| Code | Meaning | What to do |
|---|---|---|
| 400 | Bad request | Fix the request. Do not retry. |
| 401 | Missing or invalid key | Check the key. Do not retry. |
| 403 | Organisation suspended, call from an IP outside the key's allowlist, key's access level doesn't cover the route, or model not on the key's list | Write to support@sthanika.ai, call from an allowed address, or use a key with the right access. Do not retry. |
| 404 / 405 | Unknown path, unknown custom model, or wrong method | Fix the URL, model or method. |
| 413 | Body or image over a size limit | Send less. |
| 415 | Unsupported Content-Encoding or Content-Type | Use gzip, zstd or plain JSON. |
| 422 | Request does not match the schema (System One only) | Fix the question. The body lists each problem. |
| 429 | Rate limit, daily limit, or no credit left | Wait for retry-after if present. Without it, a retry will not help soon. |
| 500 | Internal error | Retry. If it persists, send the request ID to support@sthanika.ai. |
| 503 | Model temporarily unavailable or switched off, or overloaded (/v1/decisions) | Wait for retry-after if present, then retry. |
| 504 | The model did not answer in time | Retry. |
| 529 | Overloaded (System One) | Wait for retry-after, then retry. |
400 Bad request
error_type is max_tokens_exceeded, api_usage_error or invalid_request on System One. /v1/decisions also uses wrong_model_type (generative parameters such as max_tokens or temperature) and unsupported_parameter (stream: true).
State plus the longest question is too long:
{
"detail": {
"error_type": "max_tokens_exceeded",
"message": "State plus the longest question is 40001 tokens, which exceeds the maximum context length of 32768 tokens per question."
}
}State plus all questions is too long:
{
"detail": {
"error_type": "max_tokens_exceeded",
"message": "Request is 70000 tokens, which exceeds the maximum context length of 65536 tokens per request."
}
}Unknown model:
{ "detail": { "error_type": "api_usage_error", "message": "Unknown model: nope" } }Images or box without the header (a model that does not accept images gets a 400 that says so):
{
"detail": {
"error_type": "api_usage_error",
"message": "Images are not turned on. Send the header TypeSafe-Danger-Zone-Images: true to use assets or box questions."
}
}Request-rule violations all use invalid_request with a message that names the rule:
| Cause | Message |
|---|---|
| Byte-order mark | Request body must be UTF-8 JSON without a byte order mark. |
| Invalid UTF-8 | Request body must be valid UTF-8. |
NaN, Infinity or overflow | Request body must contain only finite numbers (no NaN or Infinity). |
| Nesting over 64 in a field | `state` is nested more than 64 levels deep. (the field name varies) |
| Whole body nested over 512 levels | Request body is nested more than 512 levels deep. |
| Malformed compressed body | Request body could not be decompressed with Content-Encoding zstd. |
The 400 example for /v1/decisions (same causes, OpenAI shape, param names the field):
{
"error": {
"message": "Unknown model: nope",
"type": "invalid_request_error",
"param": "model",
"code": "model_not_found"
}
}A per-question context overflow on /v1/decisions does not fail the request: that question comes back as a refusal and the others are answered. See /v1/decisions.
401 Unauthorized
Missing, empty or non-Bearer Authorization header:
{
"detail": {
"error_type": "authentication_error",
"message": "Must supply an API key! Check your request and try again."
}
}Unknown, expired or revoked key:
{
"detail": {
"error_type": "authentication_error",
"message": "Cannot authenticate with the server. Please check your API key and try again."
}
}On /v1/decisions the same causes return the OpenAI shape with "type": "invalid_request_error" and "code": "invalid_api_key".
403 Forbidden
A key with an IP allowlist, used from another address:
{
"detail": {
"error_type": "permission_error",
"message": "This API key is not allowed to make this request."
}
}A key whose access level doesn't cover the route (a Read only key making a decision):
{
"detail": {
"error_type": "permission_error",
"message": "This API key is not allowed to make decisions."
}
}On /v1/models the message is This API key is not allowed to list models. On /v1/decisions the OpenAI shape has "type": "permission_error" and "code": "insufficient_scope".
A model that isn't on the key's model list:
{
"detail": {
"error_type": "permission_error",
"message": "This API key cannot use model sieve-auto."
}
}On /v1/decisions its code is model_not_allowed and param is "model".
A suspended organisation (every route, every key):
{
"detail": {
"error_type": "permission_error",
"message": "This organization is suspended. Contact support@sthanika.ai."
}
}The suspended-organisation response also carries x-should-retry: false, so OpenAI SDKs do not retry it. On /v1/decisions its code is organization_disabled.
404 and 405
A custom model name (adp_... or ft:...) that does not exist, or belongs to another organisation, returns 404 with error_type not_found_error and the message Model not found: <name>.
Any unknown path returns 404 and a known path with the wrong method returns 405:
{ "detail": "Not Found" }{ "detail": "Method Not Allowed" }413 Request too large
{
"detail": {
"error_type": "request_too_large",
"message": "Request body is larger than 32 MiB (before or after decompression)."
}
}The same status is used for an oversize image. See Limits.
415 Unsupported media type
{
"detail": {
"error_type": "unsupported_media_type",
"message": "Unsupported Content-Encoding: \"br\". Use gzip or zstd."
}
}Other messages: Only one Content-Encoding layer is supported., zstd window larger than 8 MiB is not supported., Compressed request body without a Content-Encoding header. and Unsupported Content-Type "text/plain". Use application/json or multipart/form-data.
422 Validation error
System One only. The body is a list in FastAPI's shape. Each entry has type, loc, msg, input and, for some errors, ctx, in that order. loc starts with "body". For a question, loc includes the question type, for example ["body", "questions", "urgency", "score", "criteria"].
A missing field. input is the enclosing object:
{
"detail": [
{
"type": "missing",
"loc": ["body", "state"],
"msg": "Field required",
"input": { "model": "sieve-latest", "questions": { "billing": { "type": "noul", "instructions": "Is this message about billing?" } } }
}
]
}A score with no levels:
{
"detail": [
{
"type": "too_short",
"loc": ["body", "questions", "urgency", "score", "criteria"],
"msg": "List should have at least 1 item after validation, not 0",
"input": [],
"ctx": { "field_type": "List", "min_length": 1, "actual_length": 0 }
}
]
}An unknown question type:
{
"detail": [
{
"type": "union_tag_invalid",
"loc": ["body", "questions", "q"],
"msg": "Input tag 'bool' found using 'type' does not match any of the expected tags: 'noul', 'choice', 'score', 'box'",
"input": { "type": "bool", "instructions": "x" },
"ctx": { "discriminator": "'type'", "tag": "bool", "expected_tags": "'noul', 'choice', 'score', 'box'" }
}
]
}All the messages:
| Rule | type | msg |
|---|---|---|
| Required field missing | missing | Field required |
| Not a string | string_type | Input should be a valid string |
| Not an object | dict_type | Input should be a valid dictionary |
| Not a list | list_type | Input should be a valid list |
Body, question or noul criteria is not an object | model_attributes_type | Input should be a valid dictionary or object to extract fields from |
questions, assets or choice criteria empty | too_short | Dictionary should have at least 1 item after validation, not 0 |
| Score criteria empty | too_short | List should have at least 1 item after validation, not 0 |
| More than 255 choice options | too_long | Dictionary should have at most 255 items after validation, not 256 |
| More than 10 score levels | too_long | List should have at most 10 items after validation, not 11 |
No type on a question | union_tag_not_found | Unable to extract tag using discriminator 'type' |
Unknown type (the tag is case-sensitive) | union_tag_invalid | Input tag '...' found using 'type' does not match any of the expected tags: 'noul', 'choice', 'score', 'box' |
| Wrong asset format | literal_error | Input should be 'png', 'jpg' or 'jpeg' |
| Wrong asset type | literal_error | Input should be 'image' |
Where a field accepts a string, object or list, a wrong value yields three entries, one per accepted shape, with loc ending in str, dict[str,any] and list[any].
429 Too many requests
HTTP/1.1 429 Too Many Requests
retry-after: 2
x-typesafe-request-id: ...{
"detail": {
"error_type": "rate_limit_error",
"message": "Rate limit exceeded for requests. Retry after the time in the retry-after header."
}
}The message names what ran out: requests or input tokens. retry-after is at most 30 seconds. On /v1/decisions the body is {"error": {"type": "rate_limit_error", "code": "rate_limit_exceeded", ...}}.
Other 429s:
| Cause | error_type (System One) / code (/v1/decisions) | retry-after |
|---|---|---|
| Daily input-token limit for the organisation or key and model | rate_limit_error / daily_token_limit_exceeded | Only if the reset at 00:00 UTC is within 30 seconds |
| New organisation ramping up faster than allowed | rate_limit_error / acceleration_limit_exceeded | Same |
| Prepaid balance too low, or wallet frozen | insufficient_quota / insufficient_quota | None. Add credit first. |
| The key's monthly budget is used up | insufficient_quota / key_budget_exceeded | None. The X-Sieve-Budget-Resets-At header says when the month resets; an admin can raise or remove the budget. |
| The organisation's monthly budget (all keys) is used up | insufficient_quota / project_budget_exceeded | None. Same header; an admin can change it in Settings → Organisation. |
Rate limit headers
Responses can carry x-ratelimit-limit-<unit>, x-ratelimit-remaining-<unit> and x-ratelimit-reset-<unit>, where the unit is requests or input-tokens. The reset value is a duration such as 1s or 250ms. The headers appear only when a limit is set for your organisation.
500, 503 and 504
{ "detail": { "error_type": "internal_error", "message": "Internal server error." } }{ "detail": { "error_type": "service_unavailable", "message": "The model is temporarily unavailable. Please retry shortly." } }{ "detail": { "error_type": "timeout_error", "message": "The model did not answer in time. Please retry." } }A 503 can also mean a model or the service was switched off by an operator. The error_type is service_unavailable and the message names the model, for example Model sieve-31b is temporarily disabled. Please try again later or use another model. These carry x-should-retry: false and no retry-after. When the balance check cannot run, the request is also refused with 503 rather than run unpaid.
529 Overloaded
System One answers an overloaded service with 529 and a retry-after header (integer seconds) plus retry-after-ms:
HTTP/1.1 529
retry-after: 1{
"detail": {
"error_type": "overloaded",
"message": "The service is temporarily overloaded. Please retry shortly."
}
}On /v1/decisions the same condition is 503 with retry-after, because OpenAI clients do not use 529:
{
"error": {
"message": "The service is temporarily overloaded. Please retry shortly.",
"type": "service_unavailable_error",
"param": null,
"code": "server_is_overloaded"
}
}Retries
- What the SDKs do. Both TypeSafe SDKs retry 408, 429 and 5xx (so 503 and 529 too) and connection errors, twice, with backoff from 0.5 s doubling to 5 s. They honour
retry-after-msfirst, thenretry-after. The Python SDK stops when the total time would pass 30 s; the JavaScript SDK ignores aretry-afterover 60 s. The default timeout per attempt is 10 s. - OpenAI SDK (for
/v1/decisions) retries 408, 409, 429 and 5xx twice by default and does not wait for aretry-afterover 120 s. - A 4xx other than 408 or 429 is never retried.
- Idempotency. Send an
Idempotency-Keyheader on calls you may retry. Requests with the sameIdempotency-Keyfrom the same API key, to the same route, within 60 seconds of a successful response get that response again, with the headerSieve-Idempotent-Replayed: true. A retry that arrives while the first call is still running waits for it. Without the header, an identical body from the same key within 60 seconds is treated as the same request, so a retry is not billed twice. Only successful responses are replayed; errors are not. - Server time limits: connect 2 s, total 30 s.
Next steps
Last updated