---
title: Errors
path: core-concepts/errors
status: published
---

# Errors

ScaiVault error responses are structured and stable. Your code branches on `error.code`, not on HTTP status alone or string-matching the `message`.

## Error envelope

Every error response:

```json
{
  "error": {
    "code": "secret_not_found",
    "message": "Secret 'environments/production/missing' not found",
    "details": {
      "path": "environments/production/missing"
    },
    "request_id": "req_abc123"
  }
}
```

- **`code`** — machine-readable code from a stable vocabulary. Branch on this.
- **`message`** — human-readable description. Display or log as-is.
- **`details`** — optional, code-specific context (the offending path, the missing field, the failed condition).
- **`request_id`** — matches `X-Request-ID`. Include in any support ticket.

The HTTP status matches the error class (`4xx` for client, `5xx` for server), but specific handling should use `code`.

## Retry classification

| Code | Retry? | Notes |
|------|--------|-------|
| `authentication_required` | No | Add or refresh the `Authorization` header. |
| `token_expired` | No | Refresh via OAuth client-credentials or refresh flow. |
| `access_denied` | No | Caller lacks permission for this action. Don't retry blindly. |
| `mfa_required` | No | Re-auth with MFA before retrying. |
| `ip_not_allowed` | No | Caller IP blocked by policy condition. |
| `invalid_request` | No | Fix the request body. |
| `validation_error` | No | `details` lists the failing fields. |
| `invalid_path` | No | Path format violates rules. |
| `secret_not_found` | No | Path doesn't exist. |
| `version_not_found` | No | Version doesn't exist or has aged out. |
| `secret_exists` | No | Soft-deleted path in retention. |
| `version_conflict` | Rarely | Concurrent write; fetch current state and retry. |
| `policy_in_use` | No | Detach bindings first. |
| `rate_limited` | Yes | Honor `Retry-After`. |
| `service_unavailable` | Yes | Transient — back off. |
| `internal_error` | Once | Retry once with backoff; then escalate. |

Anything not listed above is generally non-retryable.

## Full error catalog

### Authentication / authorization

| HTTP | Code | Meaning |
|------|------|---------|
| 401 | `authentication_required` | Missing or malformed `Authorization` header. |
| 401 | `token_expired` | The token's `exp` has passed. |
| 401 | `token_invalid` | Signature didn't validate. |
| 401 | `token_revoked` | Token was explicitly revoked in ScaiKey. |
| 403 | `access_denied` | No policy rule permits this action for this identity. |
| 403 | `insufficient_scope` | The token's scopes are too narrow. |
| 403 | `mfa_required` | Action requires MFA; the token is not MFA-backed. |
| 403 | `ip_not_allowed` | Caller IP is outside the rule's `ip_ranges`. |
| 403 | `time_window_violation` | Caller is outside the rule's `time_window`. |
| 403 | `tenant_access_denied` | Explicit tenant access attempted without partner admin. |

### Validation

| HTTP | Code | Meaning |
|------|------|---------|
| 400 | `invalid_request` | Malformed JSON or top-level shape. |
| 400 | `invalid_path` | Path violates the path format rules. |
| 400 | `invalid_secret_type` | Unknown `secret_type`. |
| 400 | `invalid_duration` | Duration string (`90d`, etc.) didn't parse. |
| 400 | `invalid_cursor` | Pagination cursor is expired or malformed. |
| 422 | `validation_error` | Field-level validation failed. `details.fields` lists problems. |
| 400 | `invalid_policy` | Policy rules or bindings invalid. |
| 400 | `invalid_glob` | Path pattern isn't a valid glob. |

### Not found

| HTTP | Code | Meaning |
|------|------|---------|
| 404 | `secret_not_found` | No secret at that path. |
| 404 | `version_not_found` | Version doesn't exist or aged out. |
| 404 | `policy_not_found` | |
| 404 | `binding_not_found` | |
| 404 | `webhook_not_found` | |
| 404 | `subscription_not_found` | |
| 404 | `rotation_policy_not_found` | |
| 404 | `ca_not_found` | |
| 404 | `certificate_not_found` | |
| 404 | `csr_not_found` | |
| 404 | `trust_anchor_not_found` | |
| 404 | `engine_not_found` | |
| 404 | `role_not_found` | |
| 404 | `lease_not_found` | |
| 404 | `tenant_not_found` | |
| 404 | `identity_not_found` | |
| 404 | `federation_backend_not_found` | |

### Conflict

| HTTP | Code | Meaning |
|------|------|---------|
| 409 | `secret_exists` | Path currently soft-deleted in retention window. |
| 409 | `version_conflict` | Concurrent modification; refetch and retry. |
| 409 | `policy_in_use` | Policy has active bindings; remove them first or use `force=true`. |
| 409 | `engine_in_use` | Engine has active leases. |
| 409 | `ca_has_certificates` | CA has issued certificates; revoke them first. |
| 409 | `name_conflict` | Another resource with that name exists. |
| 410 | `secret_expired` | `expires_at` has passed. |

### Rate limit / availability

| HTTP | Code | Meaning |
|------|------|---------|
| 429 | `rate_limited` | Too many requests. `Retry-After` tells you when. |
| 429 | `quota_exceeded` | Hard quota reached for the tenant; not just a rate limit. |
| 503 | `service_unavailable` | Transient dependency (DB, Redis, KMS) issue. |
| 503 | `feature_disabled` | Feature gated off at deploy. |

### Server errors

| HTTP | Code | Meaning |
|------|------|---------|
| 500 | `internal_error` | Unexpected server error. Include `request_id` in a support ticket. |
| 500 | `encryption_error` | KMS interaction failed. |
| 500 | `storage_error` | Database or object storage error. |
| 502 | `backend_error` | Federated backend or ACME upstream failed. |
| 504 | `backend_timeout` | Federated backend didn't respond in time. |

## Validation error details

`422 validation_error` responses include `details.fields`:

```json
{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "details": {
      "fields": [
        {"field": "rules[0].path_pattern", "error": "required"},
        {"field": "rules[0].permissions", "error": "must include at least one permission"}
      ]
    }
  }
}
```

Show these to the caller verbatim.

## Rate limit headers

On any response (success or `429`):

```
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 842
X-RateLimit-Reset: 1714478400
```

On `429`:

```
Retry-After: 45
```

Sleep `Retry-After` seconds before retrying. SDKs handle this automatically.

## Request IDs

Every response includes `X-Request-ID`. You can also send your own `X-Request-ID` header on the request — ScaiVault echoes it back. Use this for distributed tracing.

When filing a support ticket, always include the `request_id` from the failing response.

## What's next

- [Error Codes Reference](../reference/error-codes) — complete list with HTTP statuses.
- [Rate Limiting](../advanced/rate-limiting) — caps by endpoint category.
