# Errors

[Docs](https://www.rightnowai.co/docs) / [System Zero](https://www.rightnowai.co/docs/system-zero) / [Errors](https://www.rightnowai.co/docs/system-zero/errors)

Updated 2026-10-18

Use `error.code` to identify a refusal and `error.retryable` to decide whether to retry.

**cURL (decide.sh)**

```shell
# Set SYSTEM_ZERO_API_KEY to your API key before running this example.
# Generate once per logical decision; reuse this key when retrying.
idempotency_key="$(uuidgen 2>/dev/null \
  || cat /proc/sys/kernel/random/uuid 2>/dev/null \
  || openssl rand -hex 16 2>/dev/null \
  || python3 -c 'from uuid import uuid4; print(uuid4())' 2>/dev/null)"
cat > request.json <<'EOF'
{
  "model": "system-zero",
  "state": "The blender I ordered arrived with a cracked jar.",
  "questions": {
    "damaged": {
      "type": "boolean",
      "instructions": "Did the item arrive damaged?"
    }
  }
}
EOF
# Headerless error responses require Python 3 to read error.code.
# If x-should-retry is present, resend only when x-should-retry is true. If the
# header is absent, resend on 429, or on 409 and 5xx only with a retryable
# error.code. No answer also retries. Wait Retry-After plus up to 10 %,
# otherwise a random wait doubling from 1 s to 30 s. Stop if Retry-After is over
# 60 s. Also stop when a 429 has no Retry-After, or a 5xx has x-should-retry but
# no Retry-After.
random() { od -An -N4 -tu4 /dev/urandom | tr -d ' '; }
# Stop resending after 10 minutes.
start=$(date +%s) attempt=0
while :; do
  : "${SYSTEM_ZERO_API_KEY:?set it first}"
  : "${idempotency_key:?could not make one}"
  rm -f reply.json
  out=$(curl https://api.rightnowai.co/v1/system-zero/decide \
    --max-time 130 -sS -o reply.json -D - -w '%{http_code}' \
    -H "Authorization: Bearer $SYSTEM_ZERO_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $idempotency_key" \
    -d @request.json)
  code=$(printf '%s' "$out" | tail -c 3)
  retry_header=$(printf '%s\n' "$out" \
    | grep -i '^x-should-retry:' | tail -n 1)
  retry=$(printf '%s\n' "$retry_header" \
    | sed 's/^[^:]*:[[:space:]]*//;s/[[:space:]]*$//')
  after=$(printf '%s\n' "$out" | grep -i '^retry-after:' \
    | tail -n 1 | sed 's/^[^:]*:[[:space:]]*//;s/[[:space:]]*$//')
  case $after in *[!0-9]*|'') after= ;; esac
  if [ -z "$retry_header" ]; then
    case $code in
      000|429) ;;
      409|5??)
        error_code=$(python3 -c 'import json, sys
try:
    print(json.load(sys.stdin).get("error", {}).get("code", ""))
except (ValueError, AttributeError):
    pass' < reply.json 2>/dev/null) || break
        case $code:$error_code in
          409:idempotency_conflict) ;;
          409:upload_in_use) ;;
          409:session_busy) ;;
          5??:service_unavailable) ;;
          5??:system_zero_unavailable) ;;
          5??:billing_settlement_error) ;;
          5??:idempotency_unavailable) ;;
          5??:limiter_unavailable) ;;
          5??:internal_error) ;;
          5??:maintenance) ;;
          5??:revoke_notice_unavailable) ;;
          *) break ;;
        esac ;;
      *) break ;;
    esac
  else
    [ "$retry" = true ] || break
    case $code in 4??|5??) ;; *) break ;; esac
  fi
  if [ -n "$after" ]; then
    after=$(printf '%s' "$after" | sed 's/^0*//')
    after=${after:-0}
    [ "$after" -le 60 ] || break
    ms=$(( after * 1000 + $(random) % (after * 100 + 1) ))
  else
    [ "$code" != 429 ] || break
    if [ -n "$retry_header" ]; then
      case $code in 5??) break ;; esac
    fi
    cap=$(( attempt < 5 ? 1000 << attempt : 30000 ))
    ms=$(( $(random) % (cap + 1) ))
  fi
  [ $(( $(date +%s) - start + ms / 1000 )) -lt 600 ] || break
  sleep "$(( ms / 1000 )).$(printf '%03d' $(( ms % 1000 )))"
  [ $(( $(date +%s) - start )) -lt 600 ] || break
  attempt=$(( attempt + 1 ))
done
echo "HTTP $code"
cat reply.json 2>/dev/null
```

**PowerShell (decide.ps1)**

```powershell
# Set SYSTEM_ZERO_API_KEY to your API key before running this example.
if (-not $env:SYSTEM_ZERO_API_KEY) { throw "Set SYSTEM_ZERO_API_KEY first." }
# One attempt, no resend: running it again sends a new decision.
$body = @'
{
  "model": "system-zero",
  "state": "The blender I ordered arrived with a cracked jar.",
  "questions": {
    "damaged": {
      "type": "boolean",
      "instructions": "Did the item arrive damaged?"
    }
  }
}
'@
$request = @{
  Method      = "Post"
  Uri         = "https://api.rightnowai.co/v1/system-zero/decide"
  Headers     = @{
    Authorization     = "Bearer $env:SYSTEM_ZERO_API_KEY"
    "Idempotency-Key" = [guid]::NewGuid().ToString()
  }
  ContentType = "application/json; charset=utf-8"
  Body        = [Text.Encoding]::UTF8.GetBytes($body)
  TimeoutSec  = 130
}
Invoke-RestMethod @request | ConvertTo-Json -Depth 10
```

**Python (decide.py)**

```python
# Set SYSTEM_ZERO_API_KEY to your API key before running this example.
import os
import random
import time
import uuid

import requests

api_key = os.environ.get("SYSTEM_ZERO_API_KEY")
if not api_key: raise RuntimeError("Set SYSTEM_ZERO_API_KEY first.")

url = "https://api.rightnowai.co/v1/system-zero/decide"
# Generate once per logical decision; reuse this key when retrying.
idempotency_key = str(uuid.uuid4())
headers = {
    "Authorization": f"Bearer {api_key}",
    "Idempotency-Key": idempotency_key,
}
body = {
    "model": "system-zero",
    "state": "The blender I ordered arrived with a cracked jar.",
    "questions": {
        "damaged": {
            "type": "boolean",
            "instructions": "Did the item arrive damaged?"
        }
    }
}


# If x-should-retry is present, resend only when x-should-retry is true. If the
# header is absent, resend on 429, or on 409 and 5xx only with a retryable
# error.code. No answer also retries. Wait Retry-After plus up to 10 %,
# otherwise a random wait doubling from 1 s to 30 s. Stop if Retry-After is over
# 60 s. Also stop when a 429 has no Retry-After, or a 5xx has x-should-retry but
# no Retry-After.
def retry_wait(response, attempt):
    """Seconds to wait before resending, or None when the answer is final."""
    backoff = random.uniform(0, min(30, 2**attempt))
    if response is None:
        return backoff
    status = response.status_code
    retry = response.headers.get("x-should-retry")
    if retry is None:
        try:
            code = response.json().get("error", {}).get("code")
        except (ValueError, AttributeError):
            code = None
        conflicts = (
            "idempotency_conflict",
            "upload_in_use",
            "session_busy",
        )
        unavailable = (
            "service_unavailable",
            "system_zero_unavailable",
            "billing_settlement_error",
            "idempotency_unavailable",
            "limiter_unavailable",
            "internal_error",
            "maintenance",
            "revoke_notice_unavailable",
        )
        allowed = status == 429 or (status == 409 and code in conflicts)
        allowed = allowed or (500 <= status <= 599 and code in unavailable)
        if not allowed:
            return None
    elif retry != "true" or status < 400:
        return None
    after = response.headers.get("Retry-After", "")
    if not after.isdigit():
        if retry is not None and status >= 500:
            return None
        return None if status == 429 else backoff
    if int(after) > 60:
        return None
    return int(after) * (1 + random.random() / 10)


# Stop resending after 10 minutes.
deadline = time.monotonic() + 600
attempt = 0
while True:
    try:
        response = requests.post(url, headers=headers, json=body, timeout=130)
    except (requests.ConnectionError, requests.Timeout):
        response = None
    wait = retry_wait(response, attempt)
    if wait is None or time.monotonic() + wait >= deadline:
        break
    time.sleep(wait)
    if time.monotonic() >= deadline:
        break
    attempt += 1
if response is None:
    raise RuntimeError("No answer within 10 minutes.")
response.raise_for_status()
print(response.json()["answers"])
```

**TypeScript (decide.ts)**

```typescript
// Set SYSTEM_ZERO_API_KEY to your API key before running this example.
const apiKey = process.env.SYSTEM_ZERO_API_KEY;
if (!apiKey) {
  throw new Error("Set SYSTEM_ZERO_API_KEY first.");
}

const url = "https://api.rightnowai.co/v1/system-zero/decide";
// Generate once per logical decision; reuse this key when retrying.
const idempotencyKey = crypto.randomUUID();
const body = JSON.stringify({
  "model": "system-zero",
  "state": "The blender I ordered arrived with a cracked jar.",
  "questions": {
    "damaged": {
      "type": "boolean",
      "instructions": "Did the item arrive damaged?"
    }
  }
});

// If x-should-retry is present, resend only when x-should-retry is true. If the
// header is absent, resend on 429, or on 409 and 5xx only with a retryable
// error.code. No answer also retries. Wait Retry-After plus up to 10 %,
// otherwise a random wait doubling from 1 s to 30 s. Stop if Retry-After is
// over 60 s. Also stop when a 429 has no Retry-After, or a 5xx has
// x-should-retry but no Retry-After.
async function retryWait(response: Response | null, attempt: number) {
  const backoff = Math.random() * Math.min(30_000, 1_000 * 2 ** attempt);
  if (response === null) return backoff;
  const { status } = response;
  const retry = response.headers.get("x-should-retry");
  if (retry === null) {
    const payload = await response.clone().json().catch(() => null);
    const code = payload?.error?.code;
    const conflicts = [
      "idempotency_conflict",
      "upload_in_use",
      "session_busy",
    ];
    const unavailable = [
      "service_unavailable",
      "system_zero_unavailable",
      "billing_settlement_error",
      "idempotency_unavailable",
      "limiter_unavailable",
      "internal_error",
      "maintenance",
      "revoke_notice_unavailable",
    ];
    const allowed = status === 429
      || (status === 409 && conflicts.includes(code))
      || (status >= 500 && status <= 599 && unavailable.includes(code));
    if (allowed === false) return null;
  } else if (retry !== "true" || status < 400) {
    return null;
  }
  const after = response.headers.get("Retry-After") ?? "";
  if (!/^\d+$/.test(after)) {
    if (retry !== null && status >= 500) return null;
    return status === 429 ? null : backoff;
  }
  if (Number(after) > 60) return null;
  return Number(after) * 1_000 * (1 + Math.random() / 10);
}

// Stop resending after 10 minutes.
const deadline = Date.now() + 600_000;
let response: Response | null = null;
for (let attempt = 0; ; attempt++) {
  response = await fetch(url, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body,
    signal: AbortSignal.timeout(130_000),
  }).catch((error: Error) => {
    // No answer is a timeout or a failed connection; anything else is raised.
    if (error.name === "TimeoutError" || error.cause) return null;
    throw error;
  });
  const wait = await retryWait(response, attempt);
  if (wait === null || Date.now() + wait >= deadline) break;
  await response?.body?.cancel();
  await new Promise((resolve) => setTimeout(resolve, wait));
  if (Date.now() >= deadline) break;
}
if (!response) throw new Error("No answer within 10 minutes.");
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const { answers } = await response.json();
console.log(answers);
```

## Error body

A request refusal returns an HTTP status and an error envelope. This example shows the held-image refusal.

**error.json**

```json
{
  "error": {
    "message": "Images are not available yet. Send the request without the image attachment.",
    "type": "not_found_error",
    "param": null,
    "code": "image_not_available",
    "retryable": false
  },
  "request_id": "request-id-from-response"
}
```

- **`error.message`**: A readable description of the refusal.
- **`error.type`**: The error category.
- **`error.param`**: The affected field, or null.
- **`error.code`**: The code your application should handle.
- **`error.retryable`**: A boolean: true permits an automatic retry; false stops automatic retries.
- **`error.details`**: Optional machine-readable context, such as a cause or reset.
- **`request_id`**: The request identifier to include when contacting support.

An HTTP 200 can contain a refusal under an individual question in `answers`. Check the entries as well as the HTTP status.

## Error codes

Held marks an input or route this release does not serve. A held code does not make its feature available. Messages shown omit any field prefix.

**Error codes**

| Status | Code | Meaning | Retryable |
| --- | --- | --- | --- |
| 400 | `idempotency_key_reuse` | Use the original body with this key, or use a new key for changed work. | No automatic retry. |
| 400 | `invalid_idempotency_key` | Use a nonblank `Idempotency-Key` with valid ASCII characters within the accepted length. | No automatic retry. |
| 400 | `invalid_request` | Correct the JSON, field names, types or encoding before resending. | No automatic retry. |
| 400 | `invalid_trace_header` | Correct or remove `X-Client-Request-Id`; its value must be nonblank ASCII within the accepted length. | No automatic retry. |
| 401 | `invalid_api_key` | Authorization is missing, malformed or unknown; a Playground credential is not accepted as a public bearer key. Use a valid System Zero API key. | No automatic retry. |
| 403 | `account_suspended` | The account is suspended, blocked or waiting for access. Contact support. | No automatic retry. |
| 403 | `auth_error` | The API key is revoked or blocked. Replace the key; this response uses authentication\_error as its type. | No automatic retry. |
| 403 | `https_required` | Use the HTTPS base URL. | No automatic retry. |
| 404 | `audio_not_available` | Held. Audio is not available yet. Send the request without the audio attachment. Remove the audio attachment. | No automatic retry. |
| 404 | `committee_not_available` | Held. This route is not available. Use standard route. | No automatic retry. |
| 404 | `deliberate_not_available` | Held. This mode is not available. Use standard mode. | No automatic retry. |
| 404 | `document_controls_not_available` | Held. Documents with control characters are not available yet. Remove the control characters and send it again. Document attachments remain unavailable, including after control characters are removed. | No automatic retry. |
| 404 | `document_not_available` | Held. Documents are not available yet. Send the request without the document attachment. Send document contents as state text without an attachment. | No automatic retry. |
| 404 | `document_pictures_not_available` | Held. Pictures inside documents are not available yet. Send the document without pictures, or as text. Document attachments remain unavailable, including after pictures are removed. | No automatic retry. |
| 404 | `image_not_available` | Held. Images are not available yet. Send the request without the image attachment. Remove the image attachment. | No automatic retry. |
| 404 | `long_context_not_available` | Held. This route is not available. Use a shorter stateless text request; sessions remain held. | No automatic retry. |
| 404 | `long_recording_not_available` | Held. This input type is not supported. Remove the long recording. | No automatic retry. |
| 404 | `model_not_found` | The requested model is not offered. Choose the alias or an available pinned version. | No automatic retry. |
| 404 | `unknown_endpoint` | The route is unavailable. Check the base URL and path; this response does not mean the API key is invalid. | No automatic retry. |
| 404 | `video_not_available` | Held. This input type is not supported. Remove the video attachment. | No automatic retry. |
| 405 | `method_not_allowed` | Use a method from the Allow response header. | No automatic retry. |
| 409 | `idempotency_conflict` | The same key and body is still running. Wait and retry with that key. | Yes. Follow the response wait. |
| 409 | `idempotency_replay_unavailable` | The request completed, but the stored answer is unavailable. details.original\_status records the original status. A new key starts new work; this is not a replay of the old answer. | No automatic retry. |
| 413 | `payload_too_large` | Reduce the whole request body. | No automatic retry. |
| 413 | `questions_too_large` | Send fewer or shorter questions. | No automatic retry. |
| 413 | `state_too_large` | Shorten the state or reduce nesting. | No automatic retry. |
| 413 | `state_too_large_unguarded` | Shorten or split the state. | No automatic retry. |
| 422 | `injection_suspected` | Remove embedded instructions from the input and send changed input. | No automatic retry. |
| 422 | `input_quarantined` | Type: `invalid_request_error`. This exact input caused repeated serving failures, so it is refused for up to 24 hours. Change the input or contact support. Change the input or contact support; do not loop on the same input. | No automatic retry. |
| 422 | `invalid_criteria` | Use the supported criteria shape and the served text range. | No automatic retry. |
| 422 | `question_too_large` | Shorten the affected question. | No automatic retry. |
| 422 | `request_refused` | Type: `invalid_request_error`. System Zero refused this request. Change it before sending it again. Change the request before sending it again. | No automatic retry. |
| 422 | `workspace_quarantined` | Type: `invalid_request_error`. This account's requests are paused for up to an hour after repeated serving failures. Contact support. Contact support about the account pause. | No automatic retry. |
| 429 | `capacity_exhausted` | Wait for `Retry-After`; retry only with bounded client backoff. | Yes. Follow the response wait. |
| 429 | `concurrency_limit_exceeded` | The account already has its allowed work in flight. Follow `Retry-After` and reduce concurrency; details include max\_concurrent. | Yes. Follow the response wait. |
| 429 | `daily_dispatch_limit` | Type: `rate_limit_error`. This account reached today's request limit. It resets at 00:00 UTC. Wait until the UTC reset named by the response. | Yes. Follow the response wait. |
| 429 | `monthly_cap_reached` | The workspace reached its monthly decision cap. Follow resets\_at and `Retry-After`; details include monthly\_request\_cap and used. | Yes. Follow the response wait. |
| 429 | `playground_daily_limit` | The Playground reached its daily limit. Wait for resets\_at; details include daily\_limit. | Yes. Follow the response wait. |
| 429 | `rate_limit_exceeded` | Wait for `Retry-After` and pace requests to the returned limits. | Yes. Follow the response wait. |
| 429 | `workspace_limit_exceeded` | Wait for `Retry-After` and reduce the workspace's concurrent work or input load. | Yes. Follow the response wait. |
| 500 | `internal_error` | An unexpected failure was caught. Retry after the stated delay; contact support with the request ID if it persists. | Yes. Follow the response wait. |
| 503 | `idempotency_unavailable` | Replay protection cannot answer. Retry with the same `Idempotency-Key` after the wait. | Yes. Follow the response wait. |
| 503 | `limiter_unavailable` | The limiting service cannot answer. Retry after the stated delay. | Yes. Follow the response wait. |
| 503 | `maintenance` | The API is in maintenance. Wait for `Retry-After`. | Yes. Follow the response wait. |
| 503 | `service_unavailable` | Wait for `Retry-After`; keep the request ID if the failure persists. | Yes. Follow the response wait. |
| 503 | `system_zero_unavailable` | Wait for `Retry-After` and retry when retryable is true. | Yes. Follow the response wait. |
| 504 | `system_zero_timeout` | Shorten the work or inspect the outcome; do not automatically replay unchanged. | No automatic retry. |

While the API is unlisted, a call without a valid key for an active account receives `404 unknown_endpoint`. Check the key, workspace access, API host and route.

## Retries

`x-should-retry` matches `error.retryable`. Stop automatic retries when it is `false`, including `504 system_zero_timeout`.

When a response permits a retry, wait for `Retry-After-Ms` in milliseconds or `Retry-After` in seconds. Prefer `Retry-After-Ms` when both are present.

Keep the decision's `Idempotency-Key` and JSON body. An `idempotency_conflict` means the same work is still running; wait before retrying with that key.

A cold start is a model pool replacing its warm container after a crash, a recycle, a preemption or a deploy, or adding one under a burst. A cold start can outlast a client's retry window; follow the response flags and wait.

## Get help

For a refusal that persists, use [Contact](https://www.rightnowai.co/contact) and include the response's `X-Request-Id`.

## Next

[Rate limits](https://www.rightnowai.co/docs/system-zero/rate-limits)
