Error Handling
The Mantis API returns structured error responses following a consistent format. This guide covers every error code the API returns, their meanings, and how to resolve them.
Error Response Format
Section titled “Error Response Format”All errors follow this JSON structure:
{ "error": { "code": "ERROR_CODE", "message": "Human-readable error description", "auth_action": "refresh_and_retry", "details": { // Optional: Additional context (only for specific errors) } }}Fields:
code- Machine-readable error identifier (constant)message- Human-readable description (may vary)auth_action- Present on401,403and503: what a client should do nextdetails- Optional additional context
auth_action
Section titled “auth_action”Switch on this rather than on code. It exists so a generic client can act without
knowing the code vocabulary, and the mapping is exhaustive server-side.
| Value | Meaning | Client behaviour |
|---|---|---|
refresh_and_retry | The access token expired | Refresh, replay the request once |
reauthenticate | Credentials revoked/expired, or 2FA needed | Send the user through sign-in again |
terminal | Forbidden, or the token is invalid | Do not retry; surface the error |
retry_later | The service is unavailable | Back off and retry |
HTTP Status Codes
Section titled “HTTP Status Codes”| Status Code | Meaning | Usage |
|---|---|---|
400 | Bad Request | Validation errors, malformed requests |
401 | Unauthorized | Missing or invalid authentication |
403 | Forbidden | Insufficient permissions, access denied |
404 | Not Found | Resource doesn’t exist |
408 | Request Timeout | An upstream operation exceeded its time budget |
409 | Conflict | Duplicate resource, constraint violation |
429 | Too Many Requests | Rate limit exceeded |
500 | Internal Server Error | Unexpected server-side error |
503 | Service Unavailable | External service (database, queue) unavailable |
Authentication Errors (401)
Section titled “Authentication Errors (401)”UNAUTHORIZED
Section titled “UNAUTHORIZED”HTTP Status: 401
Description: General authentication failure.
Example:
{ "error": { "code": "UNAUTHORIZED", "message": "Authentication required" }}Causes:
- No
Authorizationheader provided - Empty authorization header
Resolution: Include the Authorization: Bearer <token> header in your request.
INVALID_CREDENTIALS
Section titled “INVALID_CREDENTIALS”HTTP Status: 401
Description: Invalid email/password combination or API key.
Example:
{ "error": { "code": "INVALID_CREDENTIALS", "message": "Invalid credentials" }}Causes:
- Wrong email or password
- Account is inactive or suspended
- API key is revoked or expired
Resolution:
- Verify credentials are correct
- Check account status with administrator
- Create a new API key if expired
TOKEN_EXPIRED
Section titled “TOKEN_EXPIRED”HTTP Status: 401
Description: JWT access token has expired.
Example:
{ "error": { "code": "TOKEN_EXPIRED", "message": "Token expired" }}Resolution:
- Call
POST /auth/refreshto get a new access token - Implement automatic token refresh in your client
INVALID_TOKEN
Section titled “INVALID_TOKEN”HTTP Status: 401
Description: Token is malformed or signature is invalid.
Example:
{ "error": { "code": "INVALID_TOKEN", "message": "Invalid token: Invalid user ID in token" }}Causes:
- Malformed JWT structure
- Invalid signature (wrong secret key)
- Token was modified after signing
- Token audience/issuer mismatch
Resolution:
- Login again to get a fresh token
- Verify you’re connecting to the correct Mantis server
MISSING_AUTH_HEADER
Section titled “MISSING_AUTH_HEADER”HTTP Status: 401
Description: The Authorization header is missing.
Example:
{ "error": { "code": "MISSING_AUTH_HEADER", "message": "Missing authorization header" }}Resolution: Add the header:
Authorization: Bearer <your-token>INVALID_AUTH_HEADER
Section titled “INVALID_AUTH_HEADER”HTTP Status: 401
Description: The Authorization header format is incorrect.
Example:
{ "error": { "code": "INVALID_AUTH_HEADER", "message": "Invalid authorization header format" }}Causes:
- Missing “Bearer” prefix
- Extra spaces or special characters
- Incorrect encoding
Valid format:
Authorization: Bearer eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImN1cnJlbnQifQ...TWO_FACTOR_REQUIRED
Section titled “TWO_FACTOR_REQUIRED”HTTP Status: 403
Description: Pending 2FA token used on a protected endpoint.
Example:
{ "error": { "code": "TWO_FACTOR_REQUIRED", "message": "Two-factor authentication required" }}Cause: You received a pending_token from login and tried to use it on a regular endpoint.
Resolution: Complete the 2FA flow by calling POST /auth/2fa/verify first.
Other 401 codes
Section titled “Other 401 codes”These share the same envelope and status as the codes above:
| Code | Meaning | Resolution |
|---|---|---|
TOKEN_REVOKED | The access token was revoked — every request after a logout returns this | Log in again; do not retry with the same token |
REFRESH_TOKEN_EXPIRED | The refresh token has expired | Log in again |
PENDING_LOGIN_EXPIRED | The short-lived pending_token from login expired before 2FA was completed | Start the login flow again |
SSE_SESSION_NOT_FOUND | The SSE session id is unknown to the server | Request a fresh SSE token and reconnect |
SSE_SESSION_EXPIRED | The SSE session outlived its lifetime | Request a fresh SSE token and reconnect |
SSE_TOKEN_EXPIRED | The single-use SSE token expired before it was presented | Request a fresh SSE token and reconnect |
SSE_TOKEN_REPLAYED | An SSE token was presented twice — the ordinary reconnect failure, tokens are single-use | Request a new token for every reconnect rather than reusing one |
SSE_TOKEN_MISMATCH | The SSE token does not belong to the session it was presented for | Request a fresh SSE token and reconnect |
Authorization Errors (403)
Section titled “Authorization Errors (403)”FORBIDDEN
Section titled “FORBIDDEN”HTTP Status: 403
Description: Access denied with specific reason.
Example:
{ "error": { "code": "FORBIDDEN", "message": "Access denied: Account is suspended" }}Causes:
- Account status is not “active”
- Tenant-specific access restrictions
- Resource belongs to a different tenant
Resolution: Contact your administrator to check account status and permissions.
INSUFFICIENT_PERMISSIONS
Section titled “INSUFFICIENT_PERMISSIONS”HTTP Status: 403
Description: User lacks required role or permission.
Example:
{ "error": { "code": "INSUFFICIENT_PERMISSIONS", "message": "Insufficient permissions" }}Causes:
- Missing required role (e.g., “admin”, “operator”)
- Missing required permission (e.g., “deployments:create”)
- Tenant isolation prevents access
Resolution:
- Request the required role from your administrator
- Verify you’re in the correct tenant context
Validation Errors (400)
Section titled “Validation Errors (400)”VALIDATION_ERROR
Section titled “VALIDATION_ERROR”HTTP Status: 400
Description: Request data failed validation rules.
Example:
{ "error": { "code": "VALIDATION_ERROR", "message": "Validation failed: email: Invalid email format" }}Causes:
- Missing required fields
- Invalid field formats (email, UUID, etc.)
- Out-of-range values
- Field length constraints violated
Resolution: Review the validation message and fix the request body.
Common validation rules:
- Email must be valid format
- UUIDs must be properly formatted
- Names typically 1-255 characters
Pagination is the exception: page and per_page are clamped, not validated.
A page below 1 becomes 1 and per_page is clamped into 1-100 (MAX_PAGE_SIZE),
so an out-of-range value returns 200 with the clamped page rather than a
VALIDATION_ERROR.
INVALID_BODY
Section titled “INVALID_BODY”HTTP Status: 400
Description: Request body is not valid JSON.
Example:
{ "error": { "code": "INVALID_BODY", "message": "Invalid request body: expected value at line 1 column 1" }}Causes:
- Malformed JSON syntax
- Missing closing braces/brackets
- Wrong encoding
Resolution: Validate your JSON with a linter before sending.
MISSING_FIELD
Section titled “MISSING_FIELD”HTTP Status: 400
Description: Required field is missing from request.
Example:
{ "error": { "code": "MISSING_FIELD", "message": "Missing required field: solution_id" }}Resolution: Add the missing field to your request body.
Resource Errors (404)
Section titled “Resource Errors (404)”NOT_FOUND
Section titled “NOT_FOUND”HTTP Status: 404
Description: Requested resource doesn’t exist.
Example:
{ "error": { "code": "NOT_FOUND", "message": "Resource not found: Deployment not found" }}Causes:
- Invalid resource ID
- Resource was deleted
- Tenant isolation (resource exists but in different tenant)
- Resource never existed
Resolution:
- Verify the resource ID is correct
- Check if the resource was deleted
- Confirm you’re in the correct tenant
Conflict Errors (409)
Section titled “Conflict Errors (409)”CONFLICT
Section titled “CONFLICT”HTTP Status: 409
Description: Resource already exists or constraint violated.
Example:
{ "error": { "code": "CONFLICT", "message": "Resource already exists: User with email test@example.com already exists" }}Causes:
- Unique constraint violation (duplicate email, username, name)
- Deleting a row another row still references
- Writing to an entity owned by a config-as-code repository outside the reconciler
A foreign key violation on insert or update is 400 VALIDATION_ERROR, not 409 —
only the delete direction conflicts. A check-constraint violation is likewise 400,
except the managed-entity write guard, which is the 409 above.
Resolution:
- Use a different unique identifier
- Delete the existing resource first (if appropriate)
- Update the existing resource instead of creating a new one
DEPLOYMENT_FROZEN
Section titled “DEPLOYMENT_FROZEN”HTTP Status: 409
Description: Deployment is blocked by an active deployment freeze.
Example:
{ "error": { "code": "DEPLOYMENT_FROZEN", "message": "Deployment blocked by active freeze", "details": { "active_freezes": [ { "id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890", "name": "Production Freeze", "ends_at": "2024-12-26T23:59:59", "timezone": "America/New_York" } ], "can_override": false } }}Causes:
- Active freeze targeting the environment or tags
- Insufficient permissions to override freeze
Resolution:
- Wait until freeze ends
- Request override permission from administrator
- Use the
override_freeze: trueparameter (if you have override permission)
Rate Limiting (429)
Section titled “Rate Limiting (429)”Three things return 429, with different bodies, so handle them by status code
(429) rather than by parsing a specific JSON shape.
General API limiter
Section titled “General API limiter”HTTP Status: 429
Description: Per-client-IP limit exceeded (100 requests/second sustained, burst 200).
The body is plain text, not JSON:
Rate limit exceeded. Please try again later.Response headers:
Retry-After: 1X-RateLimit-Remaining: 0Authentication limiter (AUTH_RATE_LIMIT_EXCEEDED)
Section titled “Authentication limiter (AUTH_RATE_LIMIT_EXCEEDED)”HTTP Status: 429
Description: Too many authentication attempts (default 10 failed login attempts/minute per IP). Exceeding the limit triggers a temporary lockout.
The body is JSON:
{ "error": { "code": "AUTH_RATE_LIMIT_EXCEEDED", "message": "Too many failed login attempts. Please wait 900 seconds before trying again.", "retry_after_secs": 900 }}Response headers:
Retry-After: 900X-RateLimit-Remaining: 0Content-Type: application/jsonResolution:
- Honor the
Retry-Afterheader before retrying - Implement exponential backoff in your client
- Reduce request frequency
- Contact your administrator if you need higher general limits
TOO_MANY_REQUESTS
Section titled “TOO_MANY_REQUESTS”HTTP Status: 429
Description: A per-endpoint limit was hit — the encryption-key verification endpoint’s per-user limiter, or the failure-pattern recompute queue applying back-pressure when its worker channel is full. Neither IP limiter raises this.
The body is the standard error envelope:
{ "error": { "code": "TOO_MANY_REQUESTS", "message": "..." }}Resolution: Back off and retry; there is no Retry-After header on this one.
Git Storage Errors
Section titled “Git Storage Errors”Raised while syncing a Git-backed storage location.
| Code | HTTP Status | Meaning | Resolution |
|---|---|---|---|
GIT_AUTH_FAILED | 400 | The configured credentials were rejected by the remote | Re-check the credential attached to the repo |
GIT_INVALID_TARGET | 400 | The remote host could not be reached | Check the host, DNS and egress from Mandible |
GIT_REF_NOT_FOUND | 400 | The configured branch, tag or commit does not exist | Point the storage location at an existing ref |
GIT_SYNC_TIMEOUT | 408 | The clone or fetch exceeded its time budget | Retry; check remote reachability and repo size |
Database Errors (500)
Section titled “Database Errors (500)”DATABASE_ERROR
Section titled “DATABASE_ERROR”HTTP Status: 500
Description: Database operation failed.
Example:
{ "error": { "code": "DATABASE_ERROR", "message": "Internal server error" }}Causes:
- Database server is down
- Query timeout
- Database corruption (rare)
Resolution:
- Retry the request
- Contact administrator if persistent
DATABASE_CONNECTION_ERROR
Section titled “DATABASE_CONNECTION_ERROR”HTTP Status: 503
Description: Cannot establish database connection.
Example:
{ "error": { "code": "DATABASE_CONNECTION_ERROR", "message": "Service temporarily unavailable", "auth_action": "retry_later" }}Causes:
- Database server unreachable
- Connection pool exhausted
- Network issues
Resolution:
- Wait and retry
- Contact administrator for infrastructure issues
Service Errors (500/503)
Section titled “Service Errors (500/503)”INTERNAL_ERROR
Section titled “INTERNAL_ERROR”HTTP Status: 500
Description: Unexpected server error.
Example:
{ "error": { "code": "INTERNAL_ERROR", "message": "Internal server error" }}Resolution:
- Retry the request
- Report to administrator with request details
- Check server logs if you have access
SERVICE_UNAVAILABLE
Section titled “SERVICE_UNAVAILABLE”HTTP Status: 503
Description: External service is unavailable.
Example:
{ "error": { "code": "SERVICE_UNAVAILABLE", "message": "Service temporarily unavailable", "auth_action": "retry_later" }}Causes:
- Thorax (gRPC orchestration service) is down
- RabbitMQ message queue is unavailable
- Redis cache is down
- A database transaction conflicted with a concurrent one, or its connection closed
Resolution:
- Retry the request
- Check service health endpoints
- Contact administrator for infrastructure issues
EXTERNAL_SERVICE_ERROR
Section titled “EXTERNAL_SERVICE_ERROR”HTTP Status: 500
Description: External service returned an error.
Example:
{ "error": { "code": "EXTERNAL_SERVICE_ERROR", "message": "External service error" }}Resolution:
- Retry the request
- Check external service status
- Contact administrator if persistent
CONFIGURATION_ERROR
Section titled “CONFIGURATION_ERROR”HTTP Status: 500
Description: Server configuration is invalid.
Example:
{ "error": { "code": "CONFIGURATION_ERROR", "message": "Internal server error" }}Resolution: Contact your administrator immediately. This indicates a server misconfiguration.
Troubleshooting Tips
Section titled “Troubleshooting Tips”Debugging Authentication Issues
Section titled “Debugging Authentication Issues”-
Check token format:
Terminal window # Decode JWT (without verification) to inspect claimsecho "eyJhbGciOiJFUzI..." | cut -d'.' -f2 | base64 -d 2>/dev/null | jq -
Verify token expiration:
const token = 'eyJhbGci...';const payload = JSON.parse(atob(token.split('.')[1]));console.log('Expires at:', new Date(payload.exp * 1000)); -
Test with curl:
Terminal window curl -v https://mantis.example.com/api/v1/auth/me \-H "Authorization: Bearer $TOKEN"
Handling Errors in Code
Section titled “Handling Errors in Code”JavaScript/TypeScript:
async function handleApiCall(url: string, token: string) { const response = await fetch(url, { headers: { Authorization: `Bearer ${token}` }, });
if (!response.ok) { const error = await response.json();
// auth_action first: it is set on every 401/403/503 and says what to do without // knowing the code vocabulary. Fall through to `code` only for what it cannot // distinguish -- which resource was missing, which stream to re-open. switch (error.error.auth_action) { case 'refresh_and_retry': await refreshToken(); return handleApiCall(url, newToken); // one replay
case 'reauthenticate': // Credentials are gone or 2FA is required; the same token will not work again redirectToLogin(); break;
case 'retry_later': // The service is unavailable -- back off rather than failing the operation return retryWithBackoff(() => handleApiCall(url, token));
case 'terminal': showError(error.error.message); break; }
switch (error.error.code) { case 'SSE_TOKEN_REPLAYED': case 'SSE_TOKEN_EXPIRED': case 'SSE_SESSION_EXPIRED': // SSE tokens are single-use: mint a new one rather than reconnecting with this await reconnectEventStream(); break;
case 'NOT_FOUND': // Handle missing resource return null;
default: if (!error.error.auth_action) throw new Error(error.error.message); } }
return response.json();}Python:
import requestsfrom typing import Optional, Dict, Any
def handle_api_error(response: requests.Response) -> None: """Parse and handle API error responses.""" if response.status_code == 401: error = response.json() # auth_action, not code: it is set on every 401 and says what to do next if error['error'].get('auth_action') == 'refresh_and_retry': raise TokenExpiredError() raise AuthenticationError(error['error']['message'])
elif response.status_code == 403: raise PermissionError('Insufficient permissions')
elif response.status_code == 404: return None # Resource not found
elif response.status_code >= 500: raise ServiceError('Server error, please retry')
response.raise_for_status()Next Steps
Section titled “Next Steps”- Authentication Guide - Learn how to authenticate
- Pagination - Handle paginated responses
- Deployments API - Start using the API
