Error codes
Error codes
Stable dot-namespaced identifiers returned in the RFC 9457 Problem envelope's `code` field. Branch on these in your SDK exception handlers.
Every error response from the Sonny Labs API is an RFC 9457
application/problem+json envelope with a stable, dot-namespaced
code field. The type URL on the envelope points back to one of
the pages below — branch on code in your SDK exception handlers,
not on title or detail.
| Code | HTTP | Summary |
|---|---|---|
auth.api_key.expired | 401 | The API key authenticated correctly but its expires_at is in the past. |
auth.forbidden | 403 | The principal is authenticated but is not allowed to perform this operation. |
auth.forbidden_scope | 403 | The credential is valid but does not carry the scope this operation requires. |
auth.no_org_context | 400 | The request authenticated but no org_id was bound to the session — typically a misconfigured caller. |
auth.scope.insufficient | 403 | Backend variant of auth.forbidden_scope emitted by the scope-enforcement filter. |
auth.scope.missing | 403 | Alias of auth.scope.insufficient retained for SDK dispatch compatibility. |
auth.token.invalid | 401 | A credential was supplied but failed signature, format, or expiry validation. |
auth.unauthenticated | 401 | The request reached an authenticated endpoint without a credential the server could verify. |
http.403 | 403 | Generic 403 emitted when a controller throws ResponseStatusException(FORBIDDEN) without a more specific code. |
idempotency.key_reuse_mismatch | 409 | The same Idempotency-Key was reused with a different request body. |
invites.already_member | 409 | The accepting user is already a member of the target organisation. |
invites.email_mismatch | 403 | The accepting user's email does not match the email the invite was issued for. |
invites.expired | 410 | The invite token is past its expires_at and is no longer valid. |
invites.not_found | 404 | The invite token is unknown to the server, or has been revoked. |
invites.principal_email_unavailable | 403 | The accepting principal's WorkOS profile did not return an email — required for the invite-email match check. |
onboarding.already_in_org | 409 | The signed-in user attempted to create a new org while already belonging to one. |
onboarding.unsupported_in_self_hosted | 501 | Org-creation is disabled in self-hosted deployments — the operator provisions orgs out of band. |
org.not_provisioned | 409 | The principal is authenticated but no auth.organizations row has been provisioned for their org yet. |
resource.not_found | 404 | The path matched no route or the requested entity does not exist (or is not visible to this principal). |
scans.invalid_param | 400 | A query parameter on the scans endpoints failed validation. |
scans.not_found | 404 | The scan id did not resolve in the principal's org. |
service.not-ready | 503 | The backend received a request before its readiness probe transitioned to ready. |
tenant.quota_exceeded | 429 | The organisation has exceeded a billing or fair-use quota for the requested operation. |
upstream.unavailable | 502, 503 | A downstream dependency the dashboard or backend relies on is unreachable. |
users.invalid_cursor | 400 | The opaque pagination cursor on /v1/users could not be decoded. |
users.invalid_param | 400 | A query parameter on the users endpoints failed validation. |
validation.body_invalid | 400 | Request body parsed as JSON but failed bean-validation rules. |
validation.body_unparseable | 400 | Request body could not be parsed as JSON at all. |
validation.error | 400 | Generic 400 surfaced when a request fails ad-hoc validation that is not bucketed into a more specific code. |
validation.invalid_json | 400 | Dashboard BFF variant emitted when a v1 proxy request body is not valid JSON. |
validation.parameter_invalid | 400 | A query or path parameter failed bean-validation rules. |
validation.parameter_missing | 400 | A required query or path parameter was not supplied. |
validation.parameter_type_mismatch | 400 | A query or path parameter could not be converted to the expected type. |
Namespaces
auth.*— credential rejected, scope missing, session expired.validation.*— request body / parameter failed validation.resource.*— path or entity not found in the principal's org.idempotency.*—Idempotency-Keycollision.scans.*— scan-specific 4xx (controller-local validation).upstream.*— downstream dependency unreachable; safe to retry.tenant.*— billing / quota exhaustion.invites.*,onboarding.*,org.*,users.*— auth-domain 4xx not covered by the broadauth.*namespace.service.*— readiness / liveness probe failures during boot.http.*— fallback synthesised when a controller throwsResponseStatusExceptionwithout a typed code.
SDK exception mapping
The SDKs dispatch on code first, then fall back to HTTP status:
| Namespace | Python SDK | TypeScript SDK |
|---|---|---|
auth.* | AuthenticationError | AuthenticationError |
validation.* | ValidationError | ValidationError |
idempotency.* | IdempotencyConflictError | IdempotencyConflictError |
| 403 (any) | ScopeMissingError | ScopeMissingError |
| 404 (any) | NotFoundError | NotFoundError |
| 429 (any) | RateLimitError | RateLimitError |
| 5xx (any) | ServerError | ServerError |
See the Python SDK quickstart and TypeScript SDK quickstart for the full dispatch tables.