Smart Agent Teams

Errors

The error envelope, HTTP status mapping and every error code the SAT API returns.

When a request fails, the API answers with an HTTP status and a JSON envelope with a stable machine-readable code. Every error uses the envelope, including validation errors, unknown paths, rate limiting and unexpected server errors. The only non-envelope failure is a 503 from /health, which returns the health report with status: "unhealthy". Branch on code. The message is for people and may change. This page lists the envelope and every code in the supported API.

The envelope

{
  "error": {
    "code": "RUN_LEASE_LOST",
    "message": "Run is cancelled",
    "details": {}
  }
}
FieldTypeMeaning
codestringStable identifier in upper snake case. See Error codes
messagestringA sentence for people. Do not parse it
detailsobjectExtra context, often empty. For example {"email": "..."} with USER_EXISTS, or {"errors": [...]} with a validation error

@sat/api-client turns the envelope into an ApiError with status, code and message; if a body has no envelope (for example an error page from a proxy in front of the API), code is HTTP_ERROR and message is the HTTP status text.

Framework errors

Errors raised by the web framework itself, before or around your endpoint, use the same envelope with these codes:

StatusCodeWhen
422VALIDATION_ERRORThe request does not match the schema: a missing field, a wrong type, a value out of range, an invalid UUID in the path
401AUTH_REQUIREDNo Authorization header, or a scheme other than Bearer. Message Not authenticated
404NOT_FOUNDNo route matches the path. Message Not Found
405METHOD_NOT_ALLOWEDThe path exists, the method does not. Message Method Not Allowed
429RATE_LIMITEDA rate limit was hit. Message Too many requests: 5 per 1 minute (the limit that was exceeded)
500INTERNAL_ERRORAn unhandled exception. Message An unexpected error occurred. Unless ENVIRONMENT is exactly production, details.error carries the exception text

A schema validation error looks like this:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "runner_id: Field required",
    "details": {
      "errors": [
        {"loc": ["body", "runner_id"], "message": "Field required", "type": "missing"}
      ]
    }
  }
}

message describes the first problem as field path: message, with the field path joined by dots and without the leading body, query or path. details.errors lists every problem, each with loc (where the bad value is: body, query or path, then the field), message and type (the validator's error type, such as missing or uuid_parsing).

Rate-limit headers (X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After) are added to a 429 only when the limiter's headers are turned on with RATELIMIT_HEADERS_ENABLED=true. They are off by default, and no SAT deployment turns them on.

HTTP status codes

StatusMeaning in SAT
200OK, with a body
201Created: registration, a company, agent, task, comment, goal, project, routine, wake, routine trigger or API key
202Accepted: password reset email requested
204No body: a delete, a revoked key, or a claim with nothing to run
400The request is valid but not allowed in the current state, for example INVALID_STATE, RUN_FINISHED, ALREADY_DECIDED or TASK_CYCLE
401No credential (AUTH_REQUIRED), or the credential is invalid, expired or revoked (INVALID_TOKEN)
403Authenticated, but not allowed: FORBIDDEN (admin required) or SESSION_REQUIRED
404Not found, including any company you are not a member of and anything inside it
405Wrong method for the path (METHOD_NOT_ALLOWED)
409Conflict: USER_EXISTS, API_KEY_EXISTS, RUN_LEASE_LOST
422Validation failed (VALIDATION_ERROR)
429Rate limited (RATE_LIMITED)
500Unexpected server error (INTERNAL_ERROR)
503/health only: the database is unreachable or migrations failed

404, not 403, for non-members

Every company endpoint first checks that you are a member of the company in the path. If you are not, the answer is 404 NOT_FOUND with the message Company not found, exactly as if the company did not exist. Resources inside a company work the same way: a task, agent, run, approval, goal, project or routine id that belongs to another company answers 404. This keeps company ids and resource ids from being probed.

403 means you are a member but lack a role: only owner and admin can update a company (FORBIDDEN).

Error codes

Company resources

CodeStatusWhen
NOT_FOUND404The company (or you are not a member), or the task, agent, run, approval, goal, project, routine or API key does not exist in it. The message names the entity: Task not found, Assignee not found, Parent goal not found
FORBIDDEN403PATCH /companies/{company_id} by a member who is not owner or admin
INVALID_STATE400Pausing a terminated or pending_approval agent; resuming an agent that is not paused; terminating an agent that is already terminated; waking a dormant agent (paused, pending_approval, terminated); triggering a routine whose agent is dormant; approving an approval whose agent is terminated (reject it instead); PATCH /runs/{run_id} with status: "queued"
ORG_CYCLE400Setting an agent's reports_to_id to itself or to one of its reports
GOAL_CYCLE400Nesting a goal under itself or its descendants
GOOGLE_DISABLED404POST /api/login/google while GOOGLE_OAUTH_CLIENT_ID is not set
GOOGLE_SIGN_IN_FAILED401The Google ID token was rejected: bad signature, wrong audience or issuer, expired, or the email is not verified
TASK_CYCLE400PATCH /tasks/{task_ref} with a parent_id that is one of the task's own subtasks, at any depth. Setting parent_id to the task itself is ignored, not an error
ALREADY_DECIDED400Approving or rejecting an approval that is not pending
RUN_FINISHED400Updating or cancelling a run that is succeeded, failed or cancelled
RUN_LEASE_LOST409A heartbeat for a run that is not running, or with another runner_id; a PATCH /runs/{run_id} with a runner_id other than the claiming runner's
RUN_NOT_RUNNING400run_id on a task create, task update or comment, when that run is not running
INVALID_MONTH400GET /costs with a month such as 2026-13 or 2026-00: the YYYY-MM shape is right but the month is not 01 to 12. A value that is not YYYY-MM at all answers 422 VALIDATION_ERROR
VALIDATION_ERROR422GET /dashboard with days other than 7, 14 or 30, besides the schema checks every endpoint makes

Authentication and the current user

CodeStatusWhen
AUTH_REQUIRED401No Authorization header, or a scheme other than Bearer (message Not authenticated)
INVALID_TOKEN401An access token or API key that is invalid, expired or revoked, an API key created before the user's last password reset, or a refresh token used as a bearer credential
INACTIVE_USER400 or 401The authenticated user is deactivated (401 on the endpoints that require a session). In practice a deactivated user gets INVALID_TOKEN, because token and key checks already reject inactive users
USER_EXISTS409Registering an email that already has an account. details.email holds it
REGISTRATION_FAILED400Registration failed for another reason
INVALID_CREDENTIALS401Sign-in with an unknown email or a wrong password
ACCOUNT_LOCKED400Sign-in while the account is locked after five failed attempts, even with the right password
ACCOUNT_INACTIVE400Sign-in to a deactivated account
INVALID_REFRESH_TOKEN401Refresh with a token that is expired, revoked (sign-out, password reset), malformed or not a refresh token
INVALID_RESET_TOKEN400A password reset link that is invalid, already used or expired
VALIDATION_ERROR422PATCH /api/me with a blank name; PATCH /api/me/preferences with invalid preferences. For preferences, details.errors lists the problems in the validator's own shape (type, loc, msg, input), not the loc, message, type shape of schema errors
INTERNAL_ERROR500An unexpected failure inside a registration, sign-in, refresh, sign-out or reset handler
SESSION_REQUIRED403POST /api/logout called with an API key. Sign out with a session access token

API keys

CodeStatusWhen
SESSION_REQUIRED403Creating or revoking a key with an API key instead of a session access token. This applies to /api/me/api-keys and to the legacy /api/users/{username}/api-keys routes
API_KEY_EXISTS409An active key (not revoked, not expired) already has this name
API_KEY_LIMIT400The user already has 20 active keys. Revoked and expired keys do not count
NOT_FOUND404Revoking a key that does not exist, is already revoked or expired, or belongs to another user

Any endpoint

CodeStatusWhen
VALIDATION_ERROR422The request does not match the schema. See Framework errors
AUTH_REQUIRED401No bearer credential
NOT_FOUND404No route matches the path
METHOD_NOT_ALLOWED405The method is not allowed on this path
RATE_LIMITED429A rate limit was hit
INTERNAL_ERROR500An unhandled exception. Unless ENVIRONMENT is exactly production, details.error carries the exception text
HTTP_ERRORanyAnother framework error raised with a plain message instead of a code

Handling errors

  • Retry network errors, 429, 500 and 503 with backoff. Do not retry other 4xx answers unchanged.
  • On 401, refresh the session once and retry; see Refresh on 401. With an API key, a 401 means the key is revoked, expired or was created before the owner's last password reset.
  • On 404 for a company, check that the credential's owner is a member. The API cannot tell you which.
  • Runners treat RUN_LEASE_LOST, RUN_FINISHED and RUN_NOT_RUNNING as "stop this run now". See Build your own runner.
  • The CLI maps errors to exit codes: 3 for 401 and 403, 4 for 404, 5 for conflicts and validation errors such as ORG_CYCLE or RUN_FINISHED. See CLI.

On this page